十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Garnet 存储引擎 Tsavorite 架构深度指南:从 FASTER 分叉而来的分层存储核心

Garnet 存储引擎 Tsavorite 架构深度指南:从 FASTER 分叉而来的分层存储核心 Garnet 存储引擎 Tsavorite 架构深度指南从 FASTER 分叉而来的分层存储核心【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnetTsavorite 是 Garnet 的存储层源自微软开源项目 FASTER为 Garnet 提供了线程级可扩展性、内存/SSD/云存储分层存储、非阻塞检查点与恢复、操作日志持久化、多键锁与事务以及改进的内存管理与空间复用能力。本文以官方开发文档website/docs/dev/tsavorite/intro.md为主体骨架结合仓库源码libs/storage/Tsavorite/cs/与同目录系列技术文档逐层拆解 Tsavorite 的核心设计帮助读者理解 Garnet 高性能背后的存储原理并掌握各子系统的配置与工作机制。Tsavorite 是什么Garnet 的存储层Garnet 是一个远程缓存存储remote cache-store其底层存储引擎 Tsavorite 从微软此前的开源项目 FASTER 分叉而来。website/docs/dev/tsavorite/intro.md明确指出Tsavorite 继承并强化了 FASTER 的存储能力形成以下核心特性集合线程可扩展性thread scalability通过 LightEpoch 无锁懒同步机制 减少跨线程同步频率使读写路径在高并发下依然保持扩展性分层存储tiered storage同一份数据可以驻留在内存hybrid log、SSD 与云存储上由统一抽象管理快速非阻塞检查点fast non-blocking checkpointing检查点过程中不阻塞主操作路径恢复recovery从检查点与日志中恢复出完整一致的状态操作日志持久化operation logging for durability所有写操作落盘保证崩溃后的持久性多键锁与事务支持multi-key locking and transaction support详见 锁定机制文档改进的内存管理与空间复用memory management and space reuse通过 Revivification记录复活/空间复用 降低高删除场景下的日志膨胀。从代码结构看Tsavorite 位于libs/storage/Tsavorite/cs/目录包含src/核心实现、test/测试与benchmark/基准测试并有独立的Tsavorite.slnx解决方案文件在 Garnet 服务端存储层通过libs/server/Storage/下的StoreWrapper、GarnetDatabase等类型向上层暴露 API。Garnet 官方命令文档中将 Tsavorite 定位为“Garnet 存储层”其核心类型是TsavoriteKVTKey, TValue, TStoreFunctions, TAllocator。核心设计思想混合日志Hybrid Log与就地更新Tsavorite 沿用了 FASTER 的混合日志模型主存储是一个混合日志hybrid loghlog记录按时间顺序追加在日志尾部TailAddress旧记录从日志头部HeadAddress逐页回收或刷盘。每个键的多个版本通过RecordInfo.PreviousAddress组成哈希链hash chain哈希表桶HashBucketEntry指向链头。这一设计带来两个关键能力就地更新In-Place UpdateIPU与读-拷贝-更新Read-Copy-UpdateRCU当记录仍在内存可变区mutable region且值能容纳新版本时直接在原地写入否则追加新记录并回填PreviousAddress。RCU 过程中源记录会被标记Sealed以避免与其他线程的 IPU 竞争见 锁定机制文档。日志即数据库持久化只需将日志尾部追加内容写入磁盘检查点则捕获索引与日志的关键状态二者配合实现崩溃恢复。与之配套的还有可选的读缓存Read Cache一个固定大小的内存循环日志readcacheBase位于主混合日志之前缓存从磁盘读出的“热”记录避免重复磁盘 IO。读缓存记录是主日志数据的冗余副本因此可以随时丢弃、下次读取时重新提升详见 读缓存设计文档。线程可扩展性LightEpoch 无锁懒同步Tsavorite 之所以能支撑高并发核心在于LightEpoch提供的无锁懒同步latch-free lazy synchronization机制其实现位于 LightEpoch.cs。常规的 Mutex/信号量要求线程频繁互相同步代价高昂Epoch 保护机制则降低了跨线程同步的频率。工作模型10,000 英尺视角写路径不需要阻塞当前线程而是被封装为回调动作callback action交给LightEpoch线程通过“我在当前 epoch 中处于活动状态”来保护一个 epochepoch 是一个计数器操作结束时撤销保护当某个会改变共享变量如HeadAddress的操作要执行时通过bump epoch增加计数器并等待没有其他线程再使用旧值变量在 bump 之前设置因此任何看到新计数器值的线程也必然看到更新后的变量从而保证“操作到当前HeadAddress是安全的”。关键实现细节系统维护一个全局的 LightEpoch 线程表条目数N max(128, ProcessorCount * 2)每个线程加入时在线程表中占一个条目并将线程本地 epoch 初始化为当前全局 epoch每次“acquire”epoch 时线程会认领一个 epoch 计数器后来的线程只能认领更新的 epoch当所有线程都越过某个安全 epoch对每个线程 T 满足SafeEpoch 线程本地 Epoch 全局 Epoch时可以执行注册的触发器动作保证动作恰好执行一次且无并发代码在执行。常用公共方法website/docs/dev/tsavorite/epochprotection.md列出了 5 个核心方法及其用途方法用途ThisInstanceProtected判断调用线程当前是否持有 epoch 表条目是否参与 Epoch 保护ProtectAndDrain将当前线程标记为更新后 epoch 的持有者并排空该时刻之前的待执行动作Resume内部会调用它常用于循环中渐进排空Suspend释放 epoch 所有权若调用线程是系统中最后一个活动线程则触发挂起的动作/写操作Resume让线程查看刷新到“所有线程均认为安全”的最新共享变量状态是应用挂起动作/写入的时间边界BumpCurrentEpoch(Action)调度一个写操作/动作在安全的 temporal boundary 执行调用过程中可能顺带排空可排空的动作从源码结构看LightEpoch相关的线程表管理拆分为LightEpoch.EntryTable.cs与LightEpoch.TestHooks.csepoch 表按 64 字节缓存行对齐tableAligned这是针对现代处理器 L1–L3 缓存的优化。Epoch 保护不仅服务于哈希链遍历还承担了读缓存、日志页回收等场景的内存回收职责见下文。多键锁与事务按哈希桶加锁的两种模式Tsavorite 的锁定“始终开启”其粒度是**哈希索引桶HashIndex bucket**而非单个键键被哈希到桶索引桶内含 7 个条目的 tag 向量与一个溢出桶指针桶的 tag 位15 位共享锁 1 位独占锁即锁状态。因此锁定一个桶可能同时锁定“哈希到该桶的所有键”——这是为了换取极低的加锁开销。根据会话类型的不同存在两种自动选择的加锁模式详见 锁定机制文档手动锁定Manual由 Garnet 处理层在事务开始时调用TransactionalContext/TransactionalUnsafeContext的Lock方法传入有序的键数组事务结束时调用Unlock。Tsavorite 不再为单个操作加锁。瞬时锁定TransientTsavorite 为单个键在数据操作Read / Upsert / RMW / Delete统称InternalRUMD期间自动获取与释放锁。所有锁通过Interlocked.CompareExchangeThread.Yield()自旋获取且限制自旋次数以避免死锁——若超时未获得锁则释放已持锁并让操作以RETRY_LATER重试重试会刷新 epoch从而让OnPagesClosed等动作得以排空。四种 ContextClientSession上按名称暴露了 4 个*Context全部为struct以支持内联BasicContext等价于ClientSession提供安全的 epoch 管理每次调用获取/释放 epoch与瞬时锁UnsafeContext : IUnsafeContext提供瞬时锁但 epoch 由客户端通过BeginUnsafe()/EndUnsafe()手动管理TransactionalContext : ITransactionalContext提供安全 epoch 管理但要求通过BeginTransactional/EndTransactional手动加锁TransactionalUnsafeContext : ITransactionalContext, IUnsafeContext手动 epoch 管理 手动加锁的组合。此外还支持TryLock尝试获取一组键的锁全部成功才返回 true否则释放已获取的锁与TryPromoteLock将单个键的锁从读锁提升为独占锁。示例手动锁事务以下示例浓缩自TransactionalUnsafeContextTests.cslibs/storage/Tsavorite/cs/test/var luContext session.TransactionalUnsafeContext; luContext.BeginUnsafe(); luContext.BeginTransaction(); var keys new[] { new FixedLengthTransactionalKeyStruct(readKey24, LockType.Shared, luContext), // 源共享锁 new FixedLengthTransactionalKeyStruct(readKey51, LockType.Shared, luContext), // 源共享锁 new FixedLengthTransactionalKeyStruct(resultKey, LockType.Exclusive, luContext), // 目标独占锁 }; // 对键排序以防止死锁 luContext.SortKeyHashes(keys); Assert.IsTrue(luContext.TryLock(keys)); luContext.Read(key24, out var value24); luContext.Read(key51, out var value51); luContext.Upsert(resultKey, value24 value51); luContext.Unlock(keys); luContext.EndTransaction(); luContext.EndUnsafe();要点手动加锁必须按确定顺序加锁、按相反顺序解锁事务处理层中锁的源码级结构HashEntryInfo、RecordSourceTKey, TValue、OperationStackContext等均在栈上组织配合哈希链遍历与 CAS 更新。空间复用Revivification 与记录复活高删除负载会导致日志中出现大量 Tombstone墓碑记录造成空间浪费。Tsavorite 的 Revivification 机制复活revivify已删除记录以及 RCU 的源记录最小化日志增长。其完整设计见 Revivification 文档核心包括两种形式链内复活In-ChainTombstone 记录留在哈希链中后续同一键的 Upsert/RMW 若值长度足够则直接复用该记录空闲列表FreeList维护一个按 2 的幂分桶RevivificationBin的循环缓冲集合处于哈希链尾部被哈希表直接指向的 Tombstone 记录从链中 CAS 摘除并放入空闲列表供后续分配复用。第三种复用路径是CreateNewRecordXxx返回RETRY时的重试复用它始终开启与 Revivification 相互独立。配置项RevivificationSettings配置含义EnableRevivification是否启用至少启用链内复活FreeRecordBins非空则启用 FreeList数组元素为RevivificationBin含RecordSize记录最大尺寸、NumberOfRecords桶容量、BestFitScanLimit最优适配扫描上限NumberOfBinsToSearch首选桶无记录时额外搜索的更高尺寸桶数量RevivifiableFraction限定可复用记录为TailAddress下方该比例内存内的记录语义同LogSettings.MutablePercent不能大于它RestoreDeletedRecordsIfBinIsFull桶满时是否将待入桶记录恢复进哈希链对反复增删同键的应用建议置 trueUseFreeRecordPoolForCopyToTail显式 CopyToTail压缩、不可变区读取、磁盘 IO是否允许从 FreeRecordPool 分配GarnetServer 命令行参数GarnetServer.exe对应的命令行开关源码中对应RevivificationSettings解析逻辑--reviv按默认 2 的幂尺寸分桶启用 Revivification--reviv-bin-record-sizes按递增顺序指定各桶的记录尺寸取代默认--reviv不能与--reviv-in-chain-only同用--reviv-bin-record-counts各桶记录数默认RevivificationBin.DefaultRecordsPerBin可单值统一也可多值按桶分别指定--reviv-fraction可变区内可复用的日志空间比例--reviv-search-next-higher-bins最佳适配桶无法满足时搜索更高尺寸桶的数量--reviv-bin-best-fit-scan-limitfirst-fit 后继续扫描做 best-fit 的记录数UseFirstFit直接用首个适配地址BestFitScanAll扫描整桶找最优其他数值则限长扫描--reviv-in-chain-only仅链内复活不使用 FreeList。底层实现要点填充字FillerWords记录在RecordDataHeaderRDH中以 8 位计数器记录值末尾的 8 字节填充字数值的增长/收缩通过“移动填充与值之间的空间”实现分配尺寸不变日志完整性定义记录长度的全部字段位于单个 8 字节 RDH 字内长度变更先构建局部副本再以一次原子字写入发布保证并发扫描者永远看到一致长度回调传达ISessionFunctions的 writer/updater 回调通过RecordSizeInfo获知AllocatedInlineRecordSize、MaxInlineValueSize与IsRevivifiedRecord从而知晓是否写入复用的记录FreeRecordPool 设计FreeRecordPool按尺寸选桶→FreeRecordBin按尺寸分段的循环缓冲→FreeRecord一个 long48 位地址 16 位尺寸。分段按 8 字节对齐的尺寸区间划分使 Take 大概率落在目标尺寸附近兼顾 first-fit 与 best-fitCheckEmptyWorker以每秒一次的独立 Task 扫描各桶并置空标志避免在热路径维护计数器。分层存储与直接 IOSector-Aligned Buffer PoolTsavorite 对磁盘执行直接、无缓冲 IOLinux 的O_DIRECTWindows 的FILE_FLAG_NO_BUFFERING操作系统要求 IO 缓冲区按扇区对齐512 B 或 4 KB。为此设计了默认的SectorAlignedBufferPool实现见 BufferPool.OriginReturn.cs公共缓冲区类型在BufferPool.cs旧版池在BufferPool.Legacy.cs其完整设计文档为 Sector-Aligned Buffer Pool。核心洞察是“origin return归还原主”Garnet 的实际访问模式中分配缓冲区的线程几乎从不负责释放它缓冲区通常由随机的 IO 完成线程归还。若所有线程共享一个队列将导致严重的缓存行乒乓吞吐量随核数增加反而崩溃文档记录的单共享队列方案在 64 线程时从约 35 Mops/s 跌至 1 Mops/s 以下。因此每个(pool, thread, size-class)组合有一个私有ThreadShard→BucketBucket维护两条位于不同 64 字节缓存行上的链表——仅属主线程的localHead无原子、无锁的热路径与锁自由的 MPSCcrossThreadHead外来线程归还缓冲区的入口属主线程一次 CAS 批量认领整条链规避 ABA共享后备池Depot按尺寸类做lock-striping条带数 2 × ProcessorCount向上取 2 的幂下限 8、上限 64用于线程本地缓存溢出与跨线程再分配work-stealing大尺寸类 256 KB完全绕过线程本地层直接入 Depot因为大缓冲区的“同线程连续复用”模式不成立每个池有字节预算ManagedBudgetBytes默认 1 GiB由--buffer-pool-memory-budget配置置 0 则禁用池化并拆分为 small/large 两个独立BudgetState默认四分之一给 small每个缓冲区在“出生”时获取一次 permit、死亡时释放一次permit 在各级缓存间流转时不再触碰预算计数器。用户可在启动参数中使用--use-legacy-buffer-pool选择旧版池、--buffer-pool-memory-budget设置字节预算详见 Managing memory usage 文档即仓库中 memory.md。读缓存无锁的哈希链前缀读缓存将磁盘上的“热”记录保留在内存中以避免磁盘 IO。它是可选的、固定大小的内存循环日志位于主混合日志之前记录是已提交数据的冗余副本可以随时丢弃。其无锁正确性建立在单一设计规则上详见 读缓存设计文档所有结构性 compare-and-swap 只针对哈希表条目hash-table entry这一个字。没有任何操作通过修改现有读缓存记录的PreviousAddress来发布新值。由此产生两个推论链内指针发布后不可变哈希条目 CAS 是唯一的线性化点。链结构如下|------- read-cache prefix -------| |-------- main log --------| hashEntry - rcN - ... - rc2 - rc1 - mM - ... - m1 - 0读缓存记录通过RecordInfo.kIsReadCacheBitMask48 位地址的最高位标记LogAddress.IsReadCache/AbsoluteAddress负责测试与剥离读缓存记录构成链头的连续前缀且从前缀内向下地址严格递减——循环日志的淘汰eviction总是回收最低最旧地址即主日志边界的记录先被回收读提升read promotionTryCopyToReadCache分配新读缓存记录并 CAS 插入链头是“保留值”的尽力而为操作改值更新Upsert/RMW/Delete通过一次哈希条目 CAS 整体分离detach读缓存前缀——新主日志记录先以“前缀之下第一个主日志地址”为PreviousAddressCAS 成功后原子地提交新值并孤立整个前缀无需逐条失效“旁观键”的缓存副本一并被丢弃下次读取时重新提升淘汰eviction在 epoch 保护下从OnPagesClosedWorker执行通过把最低幸存记录的PreviousAddressCAS 前移来跳过被淘汰记录是唯一允许修改既有记录PreviousAddress的位置——因为淘汰是“保留值”的读者无论看到旧链还是新链都读到同一值 V值变更 vs 值保留的分界线更新是值变更操作需要“一次 CAS 分离”的单一线性化点淘汰是值保留操作链内 CAS 安全无锁。测试方面仓库的libs/storage/Tsavorite/cs/test/下覆盖了确定性链/淘汰场景ReadCacheChainTests.cs、多线程压力ReadCacheStressTests.cs以及堆追踪器平衡性断言ReadCacheHeapSizeTrackerReturnsToZeroAfterUpdateAndEvict。检查点与恢复非阻塞的持久化基础检查点checkpoint与操作日志共同构成 Tsavorite 的持久化与恢复机制。相关类型位于 CheckpointSettings.cs 与 Checkpoint.cs索引检查点index checkpoint捕获哈希索引状态。SkipReadCacheBucket在索引检查点期间遍历每个桶页的拷贝将磁盘上的条目直接指向第一个主日志记录——读缓存地址永不持久化这依赖“读缓存必须是连续前缀”的不变量混合日志检查点hybrid log checkpoint捕获日志的尾部状态配合BeginAddress、HeadAddress等地址边界实现恢复。由于 epoch 机制与非阻塞设计检查点过程不会阻塞常规读写路径即文档所称 fast non-blocking checkpointing恢复recovery先加载索引检查点再重放日志到Recovery逻辑Recovery.cs确定的恢复点使哈希链与日志重新一致。Garnet 将这一能力暴露为GarnetCheckpointManagerGarnetCheckpointManager.cs支持定期检查点配置分布式场景下集群迁移Migration与复制Replication也建立在日志与检查点机制之上。记录模型LogRecord、分配器与回调架构Tsavorite 的记录与分配架构通过ISessionFunctions回调与两类分配器向业务层开放细节见 LogRecord 文档、StoreFunctions 与分配器包装文档 与 ObjectAllocator 文档。LogRecord统一的记录抽象LogRecord结构体将原先散落的ref key/ref value/ref recordInfo参数合并为单一参数并把 ETag、Expiration 提升为一等属性不再编码进 Value还自动管理FillerLength以便记录原地伸缩。要点所有键在 Tsavorite 层都是ReadOnlySpanbyteGarnet 处理层先用PinnedSpanByte在 GarnetApi/StorageApi 边界转换为ReadOnlySpanbyteTsavorite 只有两个分配器SpanByteAllocator字符串/内联值与ObjectAllocator对象值也是 Garnet 的“统一分配器”BlittableAllocator已更名为TsavoriteLogAllocator仅供TsavoriteLog使用ObjectAllocator的ObjectIdMap日志记录中只存 4 字节ObjectIdMultiLevelPageArray槽位索引为 .NET 对象提供 GC 根并管理其生命周期空闲槽位由SimpleConcurrentStack空闲链表回收溢出键/值Overflow超过内联上限的大键值对作为byte[]单独分配记录中存ObjectId以控制对象页粒度与内存预算RecordDataHeaderRDH单字管理可变长度布局——指示字节、Namespace、RecordType、RecordLength、KeyLength、ExtendedNamespace、键、值/溢出/对象 ID、可选字段ETag → Expiration → ObjectLogPosition与 Filler不直接存储 Value 长度而是由不可变字段推算以保证扫描时记录长度始终可得RecordSizeInfo分配前由IVariableLengthInputGetUpsertFieldInfo/GetRMWInitialFieldInfo/GetRMWModifiedFieldInfo填充键值尺寸再由分配器PopulateRecordSizeInfo补齐内联/溢出判定贯穿ISessionFunctions回调。DiskLogRecord与迁移/复制DiskLogRecord是磁盘记录的ISourceLogRecord容器绑定SectorAlignedMemory缓冲区与值对象回收器键迁移与无盘复制在发送端将记录序列化为DiskLogRecord接收端通过接受TSourceLogRecord的Upsert重载落库——序列化模拟写盘流程但目标是一块容纳内联部分及后续离线段的大块网络缓冲离线段容量受单网络缓冲限制仓库有“chunked 输出”的待办项。StoreFunctions与分配器包装面向内联的类型参数为最大化 JIT 内联TsavoriteKVTKey, TValue, TStoreFunctions, TAllocator新增两个类型参数同样作用于*ContextTStoreFunctions存储级回调集合键比较、值对象序列化器工厂、记录回收、检查点完成回调提供StoreFunctions结构体实现以换取内联——Tsavorite 刻意不提供类形式的StoreFunctionsBase类类型参数无法内联TAllocator分配器包装。SpanByteAllocator/ObjectAllocator现在是包装结构体实现非泛型IAllocator含AllocatePage/FreePage/PopulateRecordSizeInfo/GetPageOfAddress等热路径内部持有XxxAllocatorImpl类实例继承AllocatorBase实现泛型IAllocatorTStoreFunctions。TsavoriteKV内部同时保留hlog包装结构体类型与hlogBaseAllocatorBase前者用于需内联的调用后者用于其余调用TsavoriteKV构造函数简化为 3 个参数KVSettingsTKey, TValue、TStoreFunctions实例、TAllocator工厂FuncAllocatorSettings, TStoreFunctions。结语从文档到源码的阅读路径本文梳理了 Tsavorite 的六大支柱——混合日志、Epoch 无锁同步、哈希桶锁与事务、Revivification 空间复用、扇区对齐缓冲池与读缓存前缀模型——它们共同支撑起 Garnet 的高吞吐与低延迟。如需继续深入可按以下路径在仓库中对照阅读官方系列文档Tsavorite 开发文档目录intro.md为入口另有locking.md、reviv.md、epochprotection.md、readcache.md、buffer-pool.md、logrecord.md、object-allocator.md、storefunctions.md核心实现Tsavorite/cs/srcEpoch 位于core/Epochs/分配器位于core/Allocator/检查点位于core/Index/Checkpointing/测试与基准Tsavorite/cs/test 与 Tsavorite/cs/benchmark其中测试覆盖锁定、读缓存链、Revivification 与记录生命周期等场景存储层的 Garnet 侧封装GarnetCheckpointManager.cs 与 StoreWrapper.cs。从源码结构看上述机制的设计目标高度一致把同步点压缩到单个原子的字写入CAS或单个缓存行内用 epoch 边界替代细粒度锁以换取多核扩展性——这正是 Tsavorite 作为 Garnet 存储层的核心工程哲学。【免费下载链接】garnetGarnet is a remote cache-store from Microsoft Research that offers strong performance (throughput and latency), scalability, storage, recovery, cluster sharding, key migration, and replication features. Garnet can work with existing Redis clients.项目地址: https://gitcode.com/GitHub_Trending/garnet4/garnet创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表