尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ScyllaDB 强一致表持久化设计:Raft 元数据进系统表、Raft 日志进共享 Commitlog

ScyllaDB 强一致表持久化设计:Raft 元数据进系统表、Raft 日志进共享 Commitlog ScyllaDB 强一致表持久化设计Raft 元数据进系统表、Raft 日志进共享 Commitlog【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb本文基于 ScyllaDB 仓库中的设计文档docs/dev/strong_consistency.md与对应源码实现讲解强一致表strongly consistent tables特性的持久层设计Raft 组元数据如何通过专门的 CQL 系统表落在与 Raft 服务器同分片上、Raft 日志条目如何直接写入共享 commitlog 以摊薄 fsync 开销以及崩溃恢复、快照截断与优雅关闭时的句柄生命周期管理。读完后你可以理解 ScyllaDB 为何要为 tablet 级 Raft 组另建一套system.raft_groups*表并能对照 raft_groups_storage 与 raft_commitlog 的源码验证文档中描述的写入、回放与截断流程。持久层的总体分层一半进系统表一半进 CommitlogScyllaDB 的强一致表功能建立在既有的 Raft 实现之上见 docs/dev/raft-in-scylla.md。其持久层被明确拆成两部分CQL 系统表存放更新不频繁的元数据如 term、vote、快照描述符共享数据库 commitlog存放 Raft 日志条目高频写入、共享 fsync 批处理。这种按更新频率分治的思路贯穿整个设计低频元数据走 CQL 写路径换实现简洁高频日志条目走 commitlog 换吞吐与 durability 成本。┌─────────────────────────────────────────────────────────┐ │ Persistence split │ ├─────────────────────────┬───────────────────────────────┤ │ Commitlog │ CQL system tables │ │ (fast, fsync-batched) │ (infrequent updates) │ ├─────────────────────────┼───────────────────────────────┤ │ Raft log entries │ term / voted_for │ │ (mutations wrapped in │ commit_idx │ │ raft metadata) │ snapshot descriptor │ │ │ snapshot configuration │ └─────────────────────────┴───────────────────────────────┘为什么不能复用 Group0 的持久化方式持久化上下文Group0Raft group0的持久化把全部 Raft 状态都存放在 shard 0 上因为 group0 的所有成员Raft 服务器都位于 shard 0。但强一致表对应的 Raft 组不同每个 tablet 副本对应一个 Raft 组组成员可以分布在任意分片未来这些 Raft 组还会跟着 tablet 一起迁移如果沿用全部元数据压到 shard 0的做法该分片会成为写瓶颈且绝大多数强一致写都要做跨分片操作性能受损。因此设计目标是某个 Raft 组的元数据就存放在该组服务器所在的分片上避免跨分片操作并把写元数据的工作均匀摊到所有分片。这直接决定了下面三张表的分片键必须包含 shard 编号。三张 Raft 元数据系统表system.raft_groups系列强一致 tablet 使用一套独立的 Raft 系统表表名内容对应 group0 的表system.raft_groups非日志元数据term、vote、commit_idx、snapshot_id逻辑上对应system.raft但不含日志条目日志在 commitlogsystem.raft_groups_snapshots快照描述符idx、term、snapshot_id镜像system.raft_snapshotssystem.raft_groups_snapshot_config快照对应的 Raft 配置CURRENT/PREVIOUS、server_id、can_vote镜像system.raft_snapshot_config关键差异这些表的分区键是复合键(shard, group_id)而不是 group0 各表单纯的group_id。分区键如何保证落在指定分片要让(shard, group_id)属于 shard X在存储层成立ScyllaDB 配套了两个专门组件专用 partitionerservice::strong_consistency::raft_groups_partitioner把 shard 编号编码进 token专用 sharderservice::strong_consistency::raft_groups_sharder从 token 中把 shard 还原出来。两者定义在 dht/fixed_shard.hh其中头注释明确给出了 token 布局[shard: 16 bits][hash: 48 bits]shard_bits 16。效果是给定某个 Raft 组其持久化数据的读写都会被路由到该组 Raft 服务器实例运行的那个分片上。在 db/system_keyspace.cc 中可以看到三张表的 schema 注册注释直接说明了设计意图These tables have partition keys of the form (shard, group_id), allowing the data to be co-located with the tablet replica that owns the raft group.Token 编码细节partitioner 把目的分片编码在 token 的高位token 布局[shard: 16 bits][group_id_hash: 48 bits]shard 值必须能装进 schema 中的smallint列有符号 16 位且需要非负因此有效范围为[0, 32767]。这一点在 raft_groups_storage 构造函数 中有硬性校验若 shard 超过int16_t最大值直接on_internal_error低 48 位由group_idtimeuuid哈希得到。一个关键性质从 token 提取 shard 是纯位操作不依赖集群当前的分片数。这意味着扩缩容、分片数变化不会破坏已持久化的元数据路由。写入路径上raft_groups_storage的所有元数据读写都以int16_t(_shard)作为第一列参数执行内部 CQL例如见 raft_groups_storage.ccINSERT INTO system.raft_groups (shard, group_id, vote_term, vote) VALUES (?, ?, ?, ?); SELECT vote_term, vote FROM system.raft_groups WHERE shard ? AND group_id ? LIMIT 1; INSERT INTO system.raft_groups (shard, group_id, commit_idx) VALUES (?, ?, ?);由于同一对象的元数据写操作需要线性化所有写都通过execute_with_linearization_point()串行化——它用一个 promise/future 链_pending_op_fut保证同一存储实例上的写请求按序执行见 raft_groups_storage.cc#L228-L239。不支持双写迁移raft_groups_sharder::shard_for_writes()最多返回一个分片即不支持基于双写double writes的迁移。tablet 迁移时Raft 元数据的处理方式是从原位置删除、在新位置重新写入。这与常规表按 token 范围跨分片复制的模式不同是使用这套表时需要记住的约束。Raft 日志条目走共享 Commitlog动机为什么不用 CQL 表存日志如果像 group0 那样把 Raft 日志条目也写进 CQL 表每次写入都要经历序列化、schema 查找和完整 CQL 写路径对高频的 Raft 日志来说代价过高。改为直接写入数据库已经在用的共享 commitlog与 mutation 持久化共用收益有三fsync 批处理来自不同组的多个 Raft 条目共享同一次磁盘 fsync摊薄开销无 CQL 开销条目以紧凑二进制格式直接写入 commitlog 段复用基础设施不引入新文件、新后台任务直接复用 commitlog 的段回收与段管理。注意分工边界只有 Raft 日志条目本身进 commitlogterm、vote、commit index、快照描述符等低频元数据仍留在上面的 CQL 系统表中。写入路径当 Raft leader 把日志条目落盘或 follower 持久化复制来的条目时Raft engine │ ▼ raft_groups_storage::store_log_entries() │ ▼ raft_commitlog::store_log_entries() │ │ for each entry: │ 包装为 raft_commitlog_entry │ 写入共享 commitlogforce_sync yes │ 把返回的 rp_handle 存入 map[raft_index] │ ▼ commitlog segment on disk对应源码raft_groups_storage::store_log_entries()直接委托给 raft_commitlog::store_log_entries()——每个条目被包装为raft_commitlog_entrycommitlog 条目格式中新增的 variant与已有的 mutation variant 并列调用db::commitlog::add_raft_entries()见 db/commitlog/commitlog.hh#L244返回的rp_handlereplay position handle按 Raft 日志 index 存入raft_commitlog内部的std::deque_replay_positions。rp_handle的语义是句柄存活期间对应的 commitlog 段不能被回收。由于 Raft index 单调递增这个容器天然是有序的deque 两端恰好支撑两种删除——头部truncate_log_tail()、尾部truncate_log()。Apply 路径把 Raft 提交接到 memtable 刷盘状态机应用一个已提交的条目把 Raft 日志里的 mutation 变成表数据时必须确保对应 commitlog 段在 memtable 刷成 sstable 之前不被回收。做法是把rp_handle的所有权移交给 memtableState machine applies entry at index N │ ▼ acquire_replay_position_handles_for(entries) │ │ move rp_handle for index N out of the deque │ └──► handle → attached to memtable (keeps segment alive until flush)raft_commitlog::acquire_replay_position_handles_for() 实现中有一个值得注意的细节它只处理 command 类条目而 configuration、dummy 等非 command 条目也会占据_replay_positions中的 index所以扫描时要跳过不匹配的 index利用双序列均按 index 有序的 O(n) 归并式扫描找不到期望的 index 会触发on_internal_error。这条路径复用了普通 mutation用 replay position 把 commitlog 段生命周期绑定到 memtable flush的既有机制没有新增任何 GC 逻辑。崩溃恢复commitlog 回放启动时 commitlog replayer 读取所有段会遇到普通 mutation 条目与 Raft 日志条目两种Commitlog replayer (startup) │ ├── mutation entry → apply to memtable既有路径 │ └── raft_commitlog_entry → 按 shard 收集到 raft_commitlog_replay_bufferRaft 条目被路由到每分片一份的 raft_commitlog_replay_buffer 收集。回放完成后每个组的条目交给process_raft_replayed_items()处理Replayed entries for group G │ ▼ 从 CQL 表加载 commit_idx 与 snapshot │ ▼ 排序、去重处理 leader 变化 更高 term 更低 index → 丢弃旧的未提交尾部 │ ├── idx ≤ commit_idx已提交 │ 反序列化 mutation在内存中应用到 memtable │ 崩溃前可能尚未刷盘 │ └── idx commit_idx未提交 重写进新的 commitlog 为新会话获取新的 rp_handles处理完毕后带着有效rp_handle的恢复条目在 Raft 组启动时交给commitlog_persistence继续持有。回放缓冲与回放流程有专门的测试覆盖test/boost/commitlog_raft_replay_test.cc、test/cluster/test_strong_consistency_commitlog.py。快照与日志截断日志随运行不断变长快照一旦覆盖某些旧条目就可以释放它们持有的rp_handle从而让 commitlog 段可被回收Snapshot taken at index S │ ▼ store_snapshot_descriptor() → 写 CQL 表 │ ▼ truncate_log_tail(S - trailing) │ │ 擦除所有 index ≤ (S - trailing) 的句柄 │ 句柄析构 → 段 dirty 计数减一 │ → commitlog 可以回收这些段 │ ▼ 旧段变为可删除源码中raft_groups_storage::store_snapshot_descriptor() 在一个线性化点内完成先写system.raft_groups_snapshotssnapshot_id、idx、term再删除旧配置并插入system.raft_groups_snapshot_config的 CURRENT/PREVIOUS 成员行最后调用truncate_log_tail(snap.idx - preserve_log_entries)。preserve_log_entries对应 Raft 的 trailing log 设置决定快照后仍保留多少尾部条目。另一种截断truncate_log(idx)丢弃的是尾部index ≥ idx 的条目用于 leader 变化使未提交条目失效的场景实现见 raft_commitlog::truncate_log()——利用 deque 有序性做lower_bound后一次性擦除到末尾。优雅关闭句柄释放但不减计数干净关闭时map 中剩余的rp_handle通过handle.release()释放但不递减段的 dirty 计数见 raft_commitlog 析构函数。这是刻意的设计目的是让含未提交 Raft 条目的 commitlog 段在磁盘上存活下次启动回放时仍然可用如果在关闭时递减 dirty 计数commitlog 可能把还需要的段删掉未提交的 Raft 日志将丢失。源码与测试索引关注点位置设计文档docs/dev/strong_consistency.mdRaft 组元数据持久化CQL 表读写、线性化、快照service/strong_consistency/raft_groups_storage.hh / raft_groups_storage.ccRaft 日志持久化commitlog 条目、句柄管理、截断service/strong_consistency/raft_commitlog.hh / raft_commitlog.cc专用 partitioner / sharder 与 token 布局dht/fixed_shard.hh三张系统表的 schema 注册db/system_keyspace.cccommitlog 新增的 Raft 条目写入接口db/commitlog/commitlog.hh#L244 / db/commitlog/commitlog.cc启动回放缓冲db/commitlog/raft_commitlog_replay_buffer.cc系统表存储单测 / 集群测试test/raft/raft_sys_table_storage_test.cc / test/cluster/test_strong_consistency.py / test/cluster/test_strong_consistency_commitlog.py小结ScyllaDB 强一致表的持久层设计可以用三句话概括元数据就近落盘——(shard, group_id)复合分区键加上shard 进 token 高 16 位的专用 partitioner/sharder让每个 Raft 组的低频元数据与组服务器同分片避免 group0 式单分片瓶颈也支持 tablet 迁移时删旧写新而非双写迁移日志借道共享 commitlog——Raft 日志条目以紧凑二进制写入与 mutation 共用的 commitlog共享 fsync、复用段管理rp_handle精确追踪每个条目所在的段生命周期严格绑定——apply 时句柄移交给 memtable段存活至 flush快照后truncate_log_tail释放旧句柄允许段回收leader 变更时truncate_log丢弃未提交尾部优雅关闭时release()保段存活待回放。这套元数据走 CQL 系统表、日志走 commitlog、句柄串起三者生命周期的分层使强一致表在获得 Raft 强一致性保证的同时把持久化成本控制在与普通写路径共享基础设施的水平。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表