
ScyllaDB Seastar Raft 共识算法库解析可插拔架构、协议实现与快照机制实战指南【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb本文深入解析 ScyllaDB 内置的 Seastar Raft 共识算法库位于仓库raft/目录。它不是一个独立应用而是为 Seastar 高性能服务端框架量身定制的可嵌入共识组件通过rpc、persistence、state_machine、failure_detector四类抽象接口实现协议核心与运行环境的完全解耦。读完本文你将掌握该库的术语体系、四大可插拔接口的契约、server门面类的启动与配置流程、联合共识joint consensus配置变更、Multi-Raft 与预投票pre-voting扩展以及日志压缩与快照传输Snapshot API的完整实现机制。库定位为 Seastar 而生的 Raft 实现Seastar 是一个用 C 编写的高性能服务端应用框架采用基于共享无锁shared-nothing分片模型与 future/promise 异步编程范式。本文所述的 Raft 库正是构建于 Seastar 之上为分布式状态机提供高效、可扩展的复制能力。它遵循 Raft 博士学位论文Raft PhD参见 https://raft.github.io/ 的公开资料描述的共识算法并在此基础上加入了若干面向工程实践的扩展。该库在 ScyllaDB 中承担着强一致元数据复制如 schema 变更、group 0 管理见 service/raft/ 目录下的raft_group0.hh、raft_group_registry.hh等等核心职责而raft/目录本身则是与具体业务解耦的通用协议库任何 Seastar 应用都可以复用它来复制自己的状态机。术语体系区分库内部状态机与用户状态机Raft 论文中的术语在业界被广泛采用本库及其文档也沿用同一套概念。理解这套术语时最关键的是分清两个状态机用户状态机user state machine指被 Raft 复制的用户应用程序本身。库把用户的业务状态抽象为state_machine接口apply()按分布式日志中的顺序把已提交条目应用到业务状态上。库内部协议状态机fsm指库自己实现的、维护 Raft 协议状态的有限状态机——类fsm定义于 raft/fsm.hh。它管理 leader / follower / candidate 三态转换、term 推进、投票与日志复制推进与用户的业务状态机完全无关两者切勿混淆。从 raft/fsm.hh 可以看到fsm内部用std::variantfollower, candidate, leader _state表达协议角色并维护_current_term当前任期、_voted_for当前任期投票对象、_commit_idx已提交日志最高索引与_log日志等核心字段其中 term、vote 与 log 均交由persistence持久化。库在设计上允许替换/插拔以下关键组件这正是 raft/README.md 强调的可扩展性组件职责说明rpc节点间通信实现rpcAPI负责与集群内其他 Raft 实例交换协议消息persistence协议状态落盘通过persistence类把库的私有状态term、vote、日志、快照描述符持久化到磁盘failure_detector共享故障检测由调用方提供自定义故障检测器替代 Raft 默认的心跳式故障探测state_machine用户状态机传入state_machine类实例承载真正被复制的业务状态实现状态已支持的功能清单依据 raft/README.md 的 Implementation status 章节该库已实现日志复制包括对无响应服务器的节流throttling控制用户状态机快照管理创建、加载、丢弃、传输领导者选举含预投票pre-voting算法非投票成员learners支持即config_member中的can_vote为is_voter::no的节点基于联合共识joint consensus的配置变更读屏障read barriers用于实现线性一致读命令转发到领导者forwarding commands to the leader。这些能力与 raft/raft.hh 中定义的完整 RPC 消息类型一一对应rpc_message变体包含了append_request、append_reply、vote_request、vote_reply、install_snapshot、snapshot_reply、timeout_now、read_quorum、read_quorum_reply九种消息raft/raft.hh覆盖了论文中的 AppendEntries、RequestVote、InstallSnapshot 以及本文库扩展的读屏障与领导者转移协议。使用方式三步搭起一个 Raft 节点首次使用First usage要使用本库应用必须提供定义在 raft/raft.hh 中的三类实现rpc、persistence与state_machine。文档指出这些类的语义与保证的完整描述维护在类注释以及示例实现测试代码中以下要点是实现者必须牢记的契约RPC 的语义应模拟一个异步、不可靠的网络模型——消息可能丢失、乱序、被重传多次但不允许损坏特别地把消息投递给错误的 Raft 服务器属于错误行为raft/README.md。raft/raft.hh 还进一步规定rpc 实现丢弃任何消息都是安全的send_*函数返回的错误会被忽略所有send_*函数可并发调用返回的 future 仅用于背压控制。persistence 的语义必须提供跨状态机重启仍存活、且不损坏其状态的持久化存储。存储应包含两个部分一个高效的、以追加为主mostly-appended-to的 Raft 日志区容纳成百上千乃至数十万条日志条目以及一个小的、寄存器式的内存区用于保存 Raft 的 term、vote 以及最新的快照描述符snapshot descriptor。apply 的语义库对已在多数服务器上可靠提交的日志条目调用state_machine::apply()。虽然apply()按条目在分布式日志中的序列化顺序被调用但不保证恰好调用一次——例如服务器从持久化状态重启时可能会重新应用部分已应用过的日志条目因此用户状态机必须对重复应用具备幂等性。整个使用流程raft/README.md 的 In a nutshell为创建rpc、persistence、state_machine三个实例将它们传给raft::server实例——它是本节点上通向 Raft 集群的门面facade调用server::start()启动服务器对集群中的每个节点重复上述步骤使用server::add_entry()提交新条目条目被集群提交后state_machine::apply()即被调用。测试代码 test/raft/replication.hh 的create_server()给出了上述流程的直接示例为每个节点构造state_machine、rpc、persistence、failure_detector然后调用raft::create_server(uuid, std::move(mrpc), std::move(sm), std::move(mpersistence), std::move(fd), state.server_config)raft/server.hh 声明了该工厂函数创建服务器。随后start_all()test/raft/replication.hh先并行启动 follower 节点并发布其 RPC 端点最后启动候选 leader 节点配合 fast bootstrap 机制让最小 server_id 的节点立即发起首轮选举。后续使用Subsequent usages后续启动流程与首次使用类似区别在于内部start()会通过persistence::load_term_and_vote()、persistence::load_log()、persistence::load_snapshot_descriptor()加载由该服务器上一次运行持久化的协议与状态机状态raft/README.md。对应的加载接口定义在 raft/raft.hh 的persistence类中且文档明确这些 load 函数仅在 Raft 服务器初始化期间被调用不会与 store 函数并行执行。Shard 亲和性约束Seastar 的执行模型是每个对象在给定 shard物理 OS 线程内使用是安全的。Raft 库遵循同样的模式——对 Raft API 的调用在单一 shard 内是安全的不支持在 shard 之间移动库实例raft/README.md。在多 shard 部署中这通常意味着每个 shard 独立持有自己的 Raft 实例。门面类 server 与其配置项raft::serverraft/server.hh是用户面对的主要门面。它的configuration结构体提供了一组对生产至关重要的调优参数下表整理了默认值、含义与约束raft/server.hh配置项默认值作用与约束snapshot_threshold1024应用完这么多条目后自动对状态机做快照snapshot_threshold_log_size2 MB日志内存占用超过该字节数时自动快照必须小于max_log_size建议不超过其一半snapshot_trailing200快照后日志中保留的条目数snapshot_trailing_size1 MB快照尾部条目占用的字节上限必须小于snapshot_threshold_log_size建议不超过其一半append_request_threshold100000单次 AppendEntries 请求携带条目的最大字节数max_log_size4 MB内存日志字节上限超过后停止接纳新请求直到快照收缩日志须满足max_command_size max_log_size - snapshot_trailing_size保证尾部条目不会阻塞新命令且至少一条命令可入日志enable_prevotingtrue是否启用选举预投票阶段enable_forwardingtrue是否把 follower 收到的条目/配置请求自动转发给 leader开启后add_entry()/modify_config()永不抛not_a_leader但更可能超时max_command_size100 KB单条命令最大字节数超限的add_entry抛command_is_too_big_errorfast_bootstrap_seed0新集群中初始 leader 的选择种子选seed % 投票者数排名按 server_id 升序的投票成员为初始 leader0 表示最小 id管理大量 group如每 tablet 一个的调用方可从 group id 派生该值以分散领导权leaseguard无可选启用后 leader 在租约有效期内可在本地直接服务线性一致读并在被废黜 leader 的租约过期前延迟提交写操作依赖有界不确定时钟见 raft/bounded_clock.hh此外还有on_background_error回调内部后台活动因错误停止时触发与tag日志中用于区分多个共享同一 server_id 的 Raft 实例的可读标签。server提供的主要操作接口包括add_entry()提交命令可指定wait_type为committed或applied、set_configuration()/modify_config()配置变更、read_barrier()读屏障、stepdown()领导者主动让位、trigger_snapshot()手动触发快照与日志截断、get_current_term()、current_leader()、is_leader()、tick()推进逻辑时钟与wait_for_state_change()/wait_for_leader()等。异常语义同样在头文件中有详细注释例如add_entry()可能抛出commit_status_unknownleader 变更导致条目被替换或丢失追踪、dropped_entry条目因 leader 变更被替换、request_aborted、stopped_error、not_a_leader未开启转发时非 leader 提交等raft/server.hh。架构要点之一基于联合共识的配置变更Raft 的配置变更历来是正确性难点。Seastar Raft 实现支持任意配置变更可以在一次变更中增加或移除一个或多个节点甚至把 Raft 组整体迁移到完全不同的服务器集合。实现采纳了原版 Raft 论文描述的两步算法raft/README.md先提交联合配置条目joint configuration该联合配置同时包含旧服务器集合与新服务器集合。一旦某服务器得知新配置它立即采纳因此联合配置一旦提交leader 需要同时获得旧集合与新集合两个多数派two majorities才能提交新条目。再追加最终配置条目一旦多数服务器持久化了联合条目库向日志追加一条仅含新配置的最终条目完成过渡。若 leader 在配置变更期间被废黜deposed新 leader 会继续把过渡从联合配置推进到最终配置。任意时刻不允许两个配置变更并发——若前一个变更仍在进行leader 会拒绝新的变更请求。这一点在代码层面对应conf_change_in_progress异常raft/raft.hh以及configuration结构体中的current/previous双集合设计与is_joint()判定raft/raft.hh。server::modify_config()的注释还说明成功完成后会追加一条 dummy 条目以确保离开联合状态并且 dummy 条目的提交可能延长不确定性窗口因此即使配置变更成功也可能返回commit_status_unknownraft/server.hh。架构要点之二Multi-Raft 多实例支持支持单个物理服务器承载多个 Raft 协议实例是 Seastar Raft 的设计目标之一库通过以下措施实现raft/README.md 的 Multi-Raft 章节全局唯一标识 可携带连接信息class server_address用于标识 Raft 服务器实例集群的一个参与者使用全局唯一标识符同时提供额外的server_info字段存放网络地址或连接凭据。这样多个 Raft 实例可以共享同一传输RPC层由共享 RPC 层依据 server UUID 把从共享网络通道收到的消息正确路由到对应的 Raft 服务器。代码中server_info是bytes类型随配置条目通过常规日志复制在集群成员间传播收到后经on_configuration_change()传给 RPC 模块解析连接信息raft/raft.hh。共享故障检测取代周期性心跳每个 Raft 组不再每 0.1 秒向每个 follower 发送一次 Raft RPC而是依赖外部输入。由于单个物理服务器可能承载多个 Raft 组故障检测 RPC 可以在网络对等端层面运行一次而不是为每个 Raft 实例单独运行。库期望合规实现提供准确的failure_detector实例——该接口仅含一个is_alive(server_id)方法默认每 tick每秒 10 次被调用一次raft/raft.hh。主动寻主ping leader扩展由于 leader 不再每 0.1 秒发送 RPC当 leader 空闲时 follower 可能长时间不知道 leader 是谁。为此库增加了扩展允许 follower 通过向所有投票者发送特制的 append reply RPC主动寻找 leaderleader 收到后会回以一条空 append 消息作为应答。代码中对应fsm::ping_leader()与send_ping_messages()raft/fsm.hh置位_ping_leader后立即向所有对端发送 ping 消息并在后续 tick 中持续 ping直到找到 leader。架构要点之三预投票与防破坏性领导者文档对预投票给出的结论性建议是tl;dr——不要关闭预投票do not turn pre-voting OFF。库实现了 Raft 论文中描述的预投票算法它增加了一个额外的投票步骤要求每个候选者在更新自身 term 之前先从 follower 收集选票。这防止了term 竞态和不必要的 leader 下台——例如一个与集群隔离的 follower 提高自己的 term、成为候选者然后干扰现有 leader 的情形。预投票扩展默认开启enable_prevoting true。除非是测试或调试库本身不要关闭它。破坏性领导者防护protection against disruptive leaders论文建议的另一个扩展要求 follower 在听到有效 leader 之后的选举超时窗口内扣留选票。但库的实践表明在预投票开启且使用共享故障检测器的情况下该扩展不仅不必要反而会降低活性liveness因此已从实现中移除。关闭预投票的代价作为缺点若预投票处于关闭状态已被移出当前配置的旧服务器若仍然存活会破坏集群活性disrupt cluster liveness。预投票在协议消息层面体现为vote_request结构体中的is_prevote标志raft/raft.hh。在 raft/fsm.hh 的step()中可以看到精细的 term 处理逻辑收到 prevote 请求时不更新 term收到 prevote 回复且投票被授予时也不更新 term因为授予投票的节点用了未来 term只有 prevote 被拒绝时才以新 term 转为 follower。架构要点之四RPC 模块地址映射Raft 实例需要在配置变化时更新 RPC 子系统使 RPC 能把消息投递给新加入配置的节点并弃用旧节点即不再属于最新配置的节点。文档明确了时序raft/README.md新节点在配置变更提交之后、实例向对端发送消息之前被加入 RPC 配置。在消息成功投递给至少多数旧节点且收到回执之前旧节点的映射必须保持完整此后被移除节点的 RPC 映射不再有用可以立即弃用。然而还有一个棘手问题在 Raft 中实例可能需要与当前配置之外的节点通信。例如某 follower 与多数派失联随后配置变更发生且当选的新 leader 不在旧配置中——此时旧配置中的节点必须能联系上这位新 leader。解决方案是引入可过期expirable更新的概念当 RPC 从未知对端收到消息时它把该对端的返回地址以TTL加入地址映射以便将来需要响应时地址已知向未配置对端的出站通信则是不可能的不建立主动连接。该机制的测试覆盖可见于 test/raft/replication.hh 中针对check_rpc_added、check_rpc_removed、check_rpc_config的断言型 updatetest/raft/replication.hh以及 raft_server_test.cc 中对 RPC 配置变更时序的验证。快照 API日志压缩与状态追赶的基石快照snapshot是用户状态机状态的紧凑表示。快照的结构与拍摄细节对库不透明——库只使用snapshot_id类本质上是 UUID来标识状态机快照raft/raft.hh。快照在两种场景下被使用raft/README.md管理 Raft 日志长度当日志过大时可截断日志。为此库拍摄新的状态机快照并擦除快照之前的大多数旧日志条目引导新成员 / 追赶落后 follower当 follower 落后太多、仅靠 leader 日志无法追上时库指示 leader 上的用户状态机把其快照以 snapshot id 标识传输给特定 follower以raft::server_id标识。状态机有责任把自己的紧凑状态完整传输给对端。snapshot_descriptor 结构snapshot_descriptor是库用于装载快照 id 及相关元数据的容器raft/README.md 给出的结构与 raft/raft.hh 一致struct snapshot_descriptor { // Index and term of last entry in the snapshot index_t idx index_t(0); term_t term term_t(0); // The committed configuration in the snapshot configuration config; // Id of the snapshot. snapshot_id id; };快照描述符涉及的 API 一览来自 raft/README.md与头文件定义一致futuresnapshot_id state_machine::take_snapshot(); void state_machine::drop_snapshot(snapshot_id id); future state_machine::load_snapshot(snapshot_id id); futuresnapshot_reply rpc::send_snapshot(server_id server_id, const install_snapshot snap, seastar::abort_source as); future persistence::store_snapshot_descriptor(const snapshot snap, size_t preserve_log_entries); future persistence::load_snapshot_descriptor();生命周期语义状态机必须在两种情况下保存快照库调用state_machine::take_snapshot()意图随后截断 Raft 日志时或经rpc::send_snapshot()发起 leader 到 follower 的快照传输时。后一种情况下leader 的状态机需要主动联系 follower 的状态机并把快照发过去。当 Raft 想用某个快照状态初始化状态机时以相应快照 id 调用state_machine::load_snapshot()。当 Raft 不再需要某个快照时用state_machine::drop_snapshot()通知状态机可以丢弃该 id 的快照。Raft 通过persistence::store_snapshot_descriptor()持久化当前使用的快照描述符。没有单独的 API 显式丢弃旧描述符该调用允许直接覆盖。成功后库会调用state_machine::drop_snapshot()丢弃被旧描述符引用的快照。快照状态必须跨重启存活因此应在take_snapshot()中、或在持久化描述符时persistence::store_snapshot_descriptor()中写入磁盘。崩溃与并发快照文档明确指出以下鲁棒性设计崩溃窗口可能发生新建快照后、丢弃旧快照前就崩溃或停止的情况。此时persistence中只保留最新快照描述符。库从不使用超过一个快照因此状态机重启后除描述符中 id 对应的那个快照外其余快照都可安全丢弃。快照操作非瞬时take_snapshot()与快照传输返回future可能耗时较长。可能出现的极端场景是状态机已有快照、又被要求拍新快照拍摄过程中 leader 变更新 leader 向某 follower 启动快照传输更罕见的是又选出新 leader再次启动自己的快照传输……如此反复。于是一个服务器可能同时在拍本地快照并运行多个传输。全部完成后库会自动选择 term 与 index 最新的快照把其 id 持久化进描述符并以该 id 调用load_snapshot()其余多余快照由库丢弃除非服务器崩溃。再次强调为清理崩溃残留合规实现应在重启时删除除描述符中 id 所引用快照外的所有快照。persistence::store_snapshot_descriptor(snap, preserve_log_entries)的第二个参数preserve_log_entries正好对应文档所述的日志截断语义——持久化快照的同时从日志开头丢弃除指定数量之外的所有条目raft/raft.hh。该函数只能在先前调用完成后再次调用即调用方需串行化但可与store_log_entries()并行。内部状态机设计回调式 vs 事件驱动raft/fsm.hh 的注释阐述了该库在设计上与许多其他 Raft 库的差异多数库通过向环境数据库、写前日志、对端 RPC暴露回调 API 来与实现解耦但回调式设计有若干缺点部分回调可能以阻塞模型定义如写日志条目到磁盘、持久化当前 term而 Seastar 没有阻塞 I/O需要用 fiber 模拟API 调用散布在状态机实现中使并发正确性的推理更困难多用户并发访问哪些需要同步回调失败时状态机是否正确处理错误虽然回调便于无网络无磁盘测试但仍需为大部分 API 实现有意义的 mock反而复杂化测试。因此 Seastar Raft 改为把每个 Raft 实例实现为内存状态机 兜底 APIstep(message)step()处理任意输入并完成所需的状态转换输出通过get_output()获取用has_output()检查是否有新输出构造函数传入的sm_events条件变量condition variable在有新输出可能产生时被通知get_output()产生一个fsm_output对象封装了在下次get_output()之前必须执行的动作列表term/vote 持久化、日志条目写入、发往对端的消息、待应用条目、快照描述符、待丢弃快照等见 raft/fsm.hh时间用逻辑定时器表示客户端负责周期调用tick()推进状态机时间使其能跟踪选举或心跳超时等事件。这一设计让协议核心成为纯函数式的状态转换器便于在 fsm_test.cc 中脱离网络与磁盘做确定性单元测试。测试体系验证正确性的手段文档推荐在 test/raft/replication_test.cc 中查看首次使用的完整示例。该测试文件基于声明式的replication_test框架定义于 test/raft/replication.hh每个测试用例由test_case声明式描述.nodes节点数、.total_values追加条数、.initial_term初始任期、.initial_states各服务器初始日志、.initial_snapshots初始快照.updates一系列更新操作entries{x}向当前 leader 追加 x 条、new_leader{x}选举 x 为新 leader、partition{...}网络分区、set_config{...}配置变更、check_rpc_config{...}校验 RPC 地址映射、isolate隔离节点等test/raft/replication.hh每个用例通过RAFT_TEST_CASE宏自动派生四个变体原版、20% 随机丢包版、预投票版、预投票丢包版从而把网络不可靠性纳入常规回归test/raft/replication_test.cc。测试框架内部实现了完整的state_machine、persistence、failure_detector与rpc四类接口的内存版test/raft/replication.hh例如rpc支持丢包drop_packet()约 20% 概率、网络延迟、单向屏蔽与双向断连是对真实网络行为的高保真模拟状态机用哈希hasher_int累加已应用值最终校验所有服务器哈希一致从而验证复制收敛性。此外仓库还提供了 randomized_nemesis_test.cc随机化混沌测试、failure_detector_test.cc故障检测器测试、raft_server_test.ccserver 门面测试等共同构成对该库正确性的多层次验证。小结Seastar Raft 是一个把协议核心fsm与运行环境rpc、persistence、failure_detector、用户state_machine彻底解耦的工程化实现。它完整覆盖了 Raft 论文的日志复制、领导者选举与配置变更并在此基础上加入预投票、Multi-Raft 共享故障检测、主动寻主、读屏障、命令转发与领导者转移timeout_now等生产级扩展快照机制则通过不透明的snapshot_id与严谨的生命周期协议同时服务于日志压缩与落后节点追赶两大目标。对于希望在 Seastar 之上构建分布式复制应用的开发者raft/目录及其 test/raft/ 测试套件既是一份可直接复用的共识组件也是一份高质量的参考实现与使用范本。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考