storage/value_store/ssd_static_layout_value_store.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <algorithm> | ||
| 4 | #include <atomic> | ||
| 5 | #include <cstdint> | ||
| 6 | #include <cstring> | ||
| 7 | #include <memory> | ||
| 8 | #include <array> | ||
| 9 | #include <shared_mutex> | ||
| 10 | #include <sstream> | ||
| 11 | #include <stdexcept> | ||
| 12 | #include <unordered_map> | ||
| 13 | #include <vector> | ||
| 14 | |||
| 15 | #include "base/factory.h" | ||
| 16 | #include "storage/io_backend/io_backend.h" | ||
| 17 | #include "storage/ssd_layout/ssd_layout.h" | ||
| 18 | #include "storage/value_store/value_store.h" | ||
| 19 | |||
| 20 | class SsdStaticLayoutValueStore : public ValueStore { | ||
| 21 | public: | ||
| 22 | struct Stats { | ||
| 23 | uint64_t logical_rows = 0; | ||
| 24 | uint64_t physical_copies = 0; | ||
| 25 | uint64_t page_reads = 0; | ||
| 26 | uint64_t requested_rows = 0; | ||
| 27 | uint64_t effective_rows = 0; | ||
| 28 | uint64_t bytes_read = 0; | ||
| 29 | }; | ||
| 30 | |||
| 31 | 32 | explicit SsdStaticLayoutValueStore(const BaseKVConfig& config) { | |
| 32 | 32 | const auto& value = config.json_config_.at("value"); | |
| 33 | 32 | const auto& layout = value.at("ssd_layout"); | |
| 34 | 32 | const std::string strategy_name = layout.at("strategy").get<std::string>(); | |
| 35 | 32 | const auto strategy = ssd_layout::ParseStrategy(strategy_name); | |
| 36 | 32 | if (strategy != ssd_layout::Strategy::kBandana && | |
| 37 | strategy != ssd_layout::Strategy::kMaxEmbed) { | ||
| 38 | 2 | throw std::invalid_argument( | |
| 39 | 4 | "SSD_STATIC_LAYOUT_VALUE_STORE requires bandana or maxembed"); | |
| 40 | } | ||
| 41 | const uint32_t row_size = static_cast<uint32_t>( | ||
| 42 | 30 | value.at("default_value_size_hint").get<uint64_t>()); | |
| 43 | artifact_ = | ||
| 44 | 30 | ssd_layout::LoadArtifact(layout.at("mapping_path").get<std::string>()); | |
| 45 | 30 | ssd_layout::CheckArtifact(artifact_, strategy, row_size); | |
| 46 | 30 | if (artifact_.page_size != PAGE_SIZE) { | |
| 47 | ✗ | throw std::invalid_argument("SSD layout page size must equal PAGE_SIZE"); | |
| 48 | } | ||
| 49 | 200 | for (uint32_t index = 0; index < artifact_.keys.size(); ++index) { | |
| 50 | 170 | key_to_row_.emplace(artifact_.keys[index], index); | |
| 51 | } | ||
| 52 | 200 | for (const auto& row : artifact_.copies) { | |
| 53 | 170 | copy_count_ += row.size(); | |
| 54 | } | ||
| 55 | |||
| 56 | const json io = | ||
| 57 | 30 | value.contains("io") | |
| 58 | 28 | ? value.at("io") | |
| 59 | 32 | : value.value("ssd_allocator", json::object()) | |
| 60 | 62 | .value("io", json::object()); | |
| 61 | 30 | BaseKVConfig io_config = config; | |
| 62 | 30 | auto& json = io_config.json_config_; | |
| 63 | const uint64_t base_offset = | ||
| 64 | 30 | io.value("base_offset_bytes", static_cast<uint64_t>(0)); | |
| 65 | 30 | if (base_offset % PAGE_SIZE != 0) { | |
| 66 | ✗ | throw std::invalid_argument( | |
| 67 | ✗ | "SSD layout base offset must be page aligned"); | |
| 68 | } | ||
| 69 | 30 | page_id_offset_ = base_offset / PAGE_SIZE; | |
| 70 | 30 | json["io_backend_type"] = io.value("type", "IOURING"); | |
| 71 | 30 | json["file_path"] = value.at("path").get<std::string>(); | |
| 72 | 30 | json["queue_cnt"] = io.value("queue_depth", 512); | |
| 73 | 30 | json["page_id_offset"] = page_id_offset_; | |
| 74 | using IOF = base::Factory<IOBackend, const BaseKVConfig&>; | ||
| 75 | 30 | io_backend_.reset(IOF::NewInstance(json.at("io_backend_type"), io_config)); | |
| 76 | 30 | io_backend_->init(); | |
| 77 | 40 | } | |
| 78 | |||
| 79 | ssd_layout::Strategy strategy() const { return artifact_.strategy; } | ||
| 80 | uint64_t checksum() const { return artifact_.checksum; } | ||
| 81 | 10 | Stats stats() const { | |
| 82 | 10 | Stats out; | |
| 83 | 10 | out.logical_rows = artifact_.keys.size(); | |
| 84 | 10 | out.physical_copies = copy_count_; | |
| 85 | 10 | out.page_reads = page_reads_.load(std::memory_order_relaxed); | |
| 86 | 10 | out.requested_rows = requested_rows_.load(std::memory_order_relaxed); | |
| 87 | 10 | out.effective_rows = effective_rows_.load(std::memory_order_relaxed); | |
| 88 | 10 | out.bytes_read = bytes_read_.load(std::memory_order_relaxed); | |
| 89 | 10 | return out; | |
| 90 | } | ||
| 91 | |||
| 92 | 2 | uint64_t Alloc(size_t) override { return RejectAlloc(); } | |
| 93 | 2 | uint64_t AllocAndWrite(const void*, size_t) override { return RejectAlloc(); } | |
| 94 | std::vector<uint64_t> | ||
| 95 | ✗ | BatchAllocAndWrite(const std::vector<WriteSpec>&) override { | |
| 96 | ✗ | throw std::invalid_argument( | |
| 97 | ✗ | "SSD_STATIC_LAYOUT_VALUE_STORE placement is fixed"); | |
| 98 | } | ||
| 99 | 14 | uint64_t AllocAndWrite(uint64_t key, const void* data, size_t size) override { | |
| 100 | 14 | const uint64_t handle = HandleForKey(key); | |
| 101 | 14 | Write(handle, data, size); | |
| 102 | 14 | return handle; | |
| 103 | } | ||
| 104 | std::vector<uint64_t> | ||
| 105 | ✗ | BatchAllocAndWrite(const std::vector<uint64_t>& keys, | |
| 106 | const std::vector<WriteSpec>& specs) override { | ||
| 107 | ✗ | if (keys.size() != specs.size()) { | |
| 108 | ✗ | throw std::invalid_argument("static SSD layout batch size mismatch"); | |
| 109 | } | ||
| 110 | ✗ | std::vector<uint64_t> handles; | |
| 111 | ✗ | handles.reserve(keys.size()); | |
| 112 | ✗ | for (uint64_t key : keys) { | |
| 113 | ✗ | handles.push_back(HandleForKey(key)); | |
| 114 | } | ||
| 115 | ✗ | BatchWrite(handles, specs); | |
| 116 | ✗ | return handles; | |
| 117 | ✗ | } | |
| 118 | 16 | ValueStore::LayoutId GetLayoutId() const override { | |
| 119 | 16 | return {static_cast<uint32_t>(artifact_.strategy), artifact_.checksum}; | |
| 120 | } | ||
| 121 | |||
| 122 | 470 | void Write(uint64_t handle, const void* data, size_t size) override { | |
| 123 | 470 | const auto& copies = CopiesOf(handle); | |
| 124 | 466 | if (size != artifact_.row_size) { | |
| 125 | 2 | throw std::invalid_argument("static SSD layout value size mismatch"); | |
| 126 | } | ||
| 127 | 464 | std::vector<uint64_t> pages; | |
| 128 | 464 | pages.reserve(copies.size()); | |
| 129 | 956 | for (const auto& copy : copies) { | |
| 130 | 492 | pages.push_back(copy.page_id); | |
| 131 | } | ||
| 132 | 464 | auto locked = LockPages(pages, true); | |
| 133 | 956 | for (const auto& copy : copies) { | |
| 134 | 492 | UpdatePage(copy, data, size); | |
| 135 | } | ||
| 136 | 464 | } | |
| 137 | |||
| 138 | 4 | void BatchWrite(const std::vector<uint64_t>& handles, | |
| 139 | const std::vector<WriteSpec>& specs) override { | ||
| 140 | 4 | if (handles.size() != specs.size()) { | |
| 141 | ✗ | throw std::invalid_argument("static SSD layout batch size mismatch"); | |
| 142 | } | ||
| 143 | 28 | for (size_t i = 0; i < handles.size(); ++i) { | |
| 144 | 24 | Write(handles[i], specs[i].data, specs[i].size); | |
| 145 | } | ||
| 146 | 4 | } | |
| 147 | |||
| 148 | 20 | size_t Read(uint64_t handle, void* out_buf, size_t buf_size) override { | |
| 149 | 20 | const auto& copies = CopiesOf(handle); | |
| 150 | 20 | if (buf_size < artifact_.row_size) { | |
| 151 | ✗ | throw std::invalid_argument("static SSD layout read buffer is too small"); | |
| 152 | } | ||
| 153 | 20 | const auto& copy = copies.front(); | |
| 154 | 40 | auto locked = LockPages({copy.page_id}, false); | |
| 155 | 20 | ReadPage(copy, out_buf); | |
| 156 | 20 | requested_rows_.fetch_add(1, std::memory_order_relaxed); | |
| 157 | 20 | effective_rows_.fetch_add(1, std::memory_order_relaxed); | |
| 158 | 20 | return artifact_.row_size; | |
| 159 | 20 | } | |
| 160 | |||
| 161 | 12 | void BatchRead(const std::vector<uint64_t>& handles, | |
| 162 | std::vector<ReadResult>& out_results) override { | ||
| 163 | 12 | out_results.resize(handles.size()); | |
| 164 | 12 | std::vector<char> done(handles.size(), 1); | |
| 165 | std::vector<const std::vector<ssd_layout::Location>*> copies( | ||
| 166 | 12 | handles.size()); | |
| 167 | 12 | size_t remaining = 0; | |
| 168 | 66 | for (size_t i = 0; i < handles.size(); ++i) { | |
| 169 | 54 | if (handles[i] == kValueHandleNone) { | |
| 170 | ✗ | out_results[i].data.clear(); | |
| 171 | ✗ | continue; | |
| 172 | } | ||
| 173 | 54 | copies[i] = &CopiesOf(handles[i]); | |
| 174 | 54 | done[i] = 0; | |
| 175 | 54 | ++remaining; | |
| 176 | 54 | out_results[i].data.resize(artifact_.row_size); | |
| 177 | } | ||
| 178 | 12 | requested_rows_.fetch_add(remaining, std::memory_order_relaxed); | |
| 179 | // ponytail: rescan candidate pages each round. Invert page->request if a | ||
| 180 | // batch profile shows this scan above the SSD read. | ||
| 181 | 26 | while (remaining > 0) { | |
| 182 | 14 | std::unordered_map<uint64_t, uint32_t> cover; | |
| 183 | 80 | for (size_t i = 0; i < done.size(); ++i) { | |
| 184 | 66 | if (done[i]) { | |
| 185 | 6 | continue; | |
| 186 | } | ||
| 187 | 150 | for (const auto& copy : *copies[i]) { | |
| 188 | 90 | ++cover[copy.page_id]; | |
| 189 | } | ||
| 190 | } | ||
| 191 | 14 | uint64_t best_page = UINT64_MAX; | |
| 192 | 14 | uint32_t best_cover = 0; | |
| 193 | 40 | for (const auto& item : cover) { | |
| 194 | 26 | if (item.second > best_cover || | |
| 195 | 8 | (item.second == best_cover && item.first < best_page)) { | |
| 196 | 22 | best_page = item.first; | |
| 197 | 22 | best_cover = item.second; | |
| 198 | } | ||
| 199 | } | ||
| 200 | 28 | auto locked = LockPages({best_page}, false); | |
| 201 | 14 | char* page = ReadRawPage(best_page, true); | |
| 202 | 14 | uint32_t hit = 0; | |
| 203 | 80 | for (size_t i = 0; i < done.size(); ++i) { | |
| 204 | 66 | if (done[i]) { | |
| 205 | 6 | continue; | |
| 206 | } | ||
| 207 | 90 | for (const auto& copy : *copies[i]) { | |
| 208 | 84 | if (copy.page_id != best_page) { | |
| 209 | 30 | continue; | |
| 210 | } | ||
| 211 | 54 | std::memcpy(out_results[i].data.data(), | |
| 212 | 54 | page + copy.byte_offset, | |
| 213 | 54 | artifact_.row_size); | |
| 214 | 54 | done[i] = 1; | |
| 215 | 54 | --remaining; | |
| 216 | 54 | ++hit; | |
| 217 | 54 | break; | |
| 218 | } | ||
| 219 | } | ||
| 220 | 14 | io_backend_->FreeBuffer(page); | |
| 221 | 14 | effective_rows_.fetch_add(hit, std::memory_order_relaxed); | |
| 222 | 14 | } | |
| 223 | 12 | } | |
| 224 | |||
| 225 | 2 | void Free(uint64_t) override {} | |
| 226 | 38 | size_t SlotCapacity(uint64_t handle) const override { | |
| 227 | 38 | CopiesOf(handle); | |
| 228 | 36 | return artifact_.row_size; | |
| 229 | } | ||
| 230 | |||
| 231 | ✗ | void ResetStats() override { | |
| 232 | ✗ | io_backend_->ResetReadStats(); | |
| 233 | ✗ | page_reads_.store(0, std::memory_order_relaxed); | |
| 234 | ✗ | requested_rows_.store(0, std::memory_order_relaxed); | |
| 235 | ✗ | effective_rows_.store(0, std::memory_order_relaxed); | |
| 236 | ✗ | bytes_read_.store(0, std::memory_order_relaxed); | |
| 237 | ✗ | } | |
| 238 | |||
| 239 | ✗ | std::string ExtraResultFields() const override { | |
| 240 | ✗ | const Stats snapshot = stats(); | |
| 241 | ✗ | const double rows_per_page = | |
| 242 | ✗ | snapshot.page_reads == 0 | |
| 243 | ✗ | ? 0.0 | |
| 244 | ✗ | : static_cast<double>(snapshot.effective_rows) / | |
| 245 | ✗ | snapshot.page_reads; | |
| 246 | ✗ | std::ostringstream out; | |
| 247 | ✗ | out << " layout_strategy=" << ssd_layout::StrategyName(artifact_.strategy) | |
| 248 | ✗ | << " layout_logical_rows=" << snapshot.logical_rows | |
| 249 | ✗ | << " layout_physical_copies=" << snapshot.physical_copies | |
| 250 | ✗ | << " layout_page_reads=" << snapshot.page_reads | |
| 251 | ✗ | << " layout_requested_rows=" << snapshot.requested_rows | |
| 252 | ✗ | << " layout_effective_rows=" << snapshot.effective_rows | |
| 253 | ✗ | << " layout_rows_per_page=" << rows_per_page | |
| 254 | ✗ | << " layout_bytes_read=" << snapshot.bytes_read; | |
| 255 | ✗ | return out.str(); | |
| 256 | ✗ | } | |
| 257 | |||
| 258 | private: | ||
| 259 | struct PageLock { | ||
| 260 | std::vector<std::shared_mutex*> mutexes; | ||
| 261 | bool exclusive = false; | ||
| 262 | 498 | PageLock() = default; | |
| 263 | PageLock(const PageLock&) = delete; | ||
| 264 | PageLock& operator=(const PageLock&) = delete; | ||
| 265 | PageLock(PageLock&& other) noexcept : exclusive(other.exclusive) { | ||
| 266 | mutexes.swap(other.mutexes); | ||
| 267 | } | ||
| 268 | 498 | ~PageLock() { | |
| 269 | 1024 | for (auto it = mutexes.rbegin(); it != mutexes.rend(); ++it) { | |
| 270 | 526 | if (exclusive) { | |
| 271 | 492 | (*it)->unlock(); | |
| 272 | } else { | ||
| 273 | 34 | (*it)->unlock_shared(); | |
| 274 | } | ||
| 275 | } | ||
| 276 | 498 | } | |
| 277 | }; | ||
| 278 | |||
| 279 | 4 | static uint64_t RejectAlloc() { | |
| 280 | 4 | throw std::invalid_argument( | |
| 281 | 8 | "SSD_STATIC_LAYOUT_VALUE_STORE placement is fixed"); | |
| 282 | } | ||
| 283 | |||
| 284 | 14 | uint64_t HandleForKey(uint64_t key) const { | |
| 285 | 14 | if (key > ssd_layout::kMaxKey) { | |
| 286 | ✗ | throw std::invalid_argument("static SSD layout key is out of range"); | |
| 287 | } | ||
| 288 | 14 | return key + 1; | |
| 289 | } | ||
| 290 | |||
| 291 | 582 | uint64_t DecodeKey(uint64_t handle) const { | |
| 292 | 582 | if (handle == kValueHandleNone || handle > ssd_layout::kMaxKey + 1) { | |
| 293 | 2 | throw std::invalid_argument("static SSD layout handle is invalid"); | |
| 294 | } | ||
| 295 | 580 | return handle - 1; | |
| 296 | } | ||
| 297 | |||
| 298 | 582 | const std::vector<ssd_layout::Location>& CopiesOf(uint64_t handle) const { | |
| 299 | 582 | const auto found = key_to_row_.find(DecodeKey(handle)); | |
| 300 | 580 | if (found == key_to_row_.end()) { | |
| 301 | 4 | throw std::invalid_argument("static SSD layout key is not mapped"); | |
| 302 | } | ||
| 303 | 1152 | return artifact_.copies[found->second]; | |
| 304 | } | ||
| 305 | |||
| 306 | 498 | PageLock LockPages(std::vector<uint64_t> pages, bool exclusive) { | |
| 307 | 498 | std::vector<std::shared_mutex*> mutexes; | |
| 308 | 498 | mutexes.reserve(pages.size()); | |
| 309 | 1024 | for (uint64_t page : pages) { | |
| 310 | 526 | mutexes.push_back(&page_locks_[page % page_locks_.size()]); | |
| 311 | } | ||
| 312 | 498 | std::sort(mutexes.begin(), mutexes.end()); | |
| 313 | 498 | mutexes.erase(std::unique(mutexes.begin(), mutexes.end()), mutexes.end()); | |
| 314 | 498 | PageLock locked; | |
| 315 | 498 | locked.exclusive = exclusive; | |
| 316 | 498 | locked.mutexes = mutexes; | |
| 317 | 1024 | for (auto* mutex : locked.mutexes) { | |
| 318 | 526 | if (exclusive) { | |
| 319 | 492 | mutex->lock(); | |
| 320 | } else { | ||
| 321 | 34 | mutex->lock_shared(); | |
| 322 | } | ||
| 323 | } | ||
| 324 | 996 | return locked; | |
| 325 | 498 | } | |
| 326 | |||
| 327 | 526 | char* ReadRawPage(uint64_t page_id, bool count_read) { | |
| 328 | 526 | char* buffer = io_backend_->AllocateBuffer(); | |
| 329 | 526 | io_backend_->BatchReadPages({{page_id + page_id_offset_, buffer, 1}}); | |
| 330 | 526 | if (count_read) { | |
| 331 | 34 | page_reads_.fetch_add(1, std::memory_order_relaxed); | |
| 332 | 34 | bytes_read_.fetch_add(PAGE_SIZE, std::memory_order_relaxed); | |
| 333 | } | ||
| 334 | 526 | return buffer; | |
| 335 | } | ||
| 336 | |||
| 337 | 20 | void ReadPage(const ssd_layout::Location& copy, void* out) { | |
| 338 | 20 | char* page = ReadRawPage(copy.page_id, true); | |
| 339 | 20 | std::memcpy(out, page + copy.byte_offset, artifact_.row_size); | |
| 340 | 20 | io_backend_->FreeBuffer(page); | |
| 341 | 20 | } | |
| 342 | |||
| 343 | void | ||
| 344 | 492 | UpdatePage(const ssd_layout::Location& copy, const void* data, size_t size) { | |
| 345 | 492 | char* page = ReadRawPage(copy.page_id, false); | |
| 346 | 492 | std::memcpy(page + copy.byte_offset, data, size); | |
| 347 | 492 | io_backend_->BatchWritePages({{copy.page_id + page_id_offset_, page, 1}}); | |
| 348 | 492 | io_backend_->FreeBuffer(page); | |
| 349 | 492 | } | |
| 350 | |||
| 351 | ssd_layout::Artifact artifact_; | ||
| 352 | std::unordered_map<uint64_t, uint32_t> key_to_row_; | ||
| 353 | std::unique_ptr<IOBackend> io_backend_; | ||
| 354 | uint64_t page_id_offset_ = 0; | ||
| 355 | uint64_t copy_count_ = 0; | ||
| 356 | std::array<std::shared_mutex, 64> page_locks_; | ||
| 357 | std::atomic<uint64_t> page_reads_{0}; | ||
| 358 | std::atomic<uint64_t> requested_rows_{0}; | ||
| 359 | std::atomic<uint64_t> effective_rows_{0}; | ||
| 360 | std::atomic<uint64_t> bytes_read_{0}; | ||
| 361 | }; | ||
| 362 | |||
| 363 | FACTORY_REGISTER(ValueStore, | ||
| 364 | SSD_STATIC_LAYOUT_VALUE_STORE, | ||
| 365 | SsdStaticLayoutValueStore, | ||
| 366 | const BaseKVConfig&); | ||
| 367 |