raft 共识架构与稳定性设计
本文回答三个问题:raft 实现的稳定性怎么保证(论文五条安全性质 + 活性的逐条实现映射)、 高性能从哪来(直排/批/下环/零分配/泵线程)、有序性怎么处理(提交全序/日志序/apply 序/ 重复容忍/乱序防御)。
与 net.md 的关系:net.md §6 是使用面(复制完成档/读面/成员变更怎么调用);
本文是实现面——一个外部使用者据本文判断"这套 raft 是否真的满足论文核心性质、边界在哪"。
代码引用为仓库相对路径(Raft/… = src/KernLab.Tier.Core.Net/Raft/…)。
论文 = Raft(Diego Ongaro & John Ousterhout,"In Search of an Understandable Consensus Algorithm")。 安全性质对应论文 §5.4:选举安全 / 领导权完整 / 日志匹配 / 状态机安全 / 领导权只追加。
1. 架构总览
1.1 组件与职责
┌──────────────────────────────────────────────────────┐
│ RaftStateMachine(共识引擎) │
│ ┌──────────────┐ ┌────────────────────────────┐ │
┌─────────────┐ │ │ 共识循环 │ │ 复制引擎 ReplicationProcess │ │
│ 节拍注册表 │──▶│ │ LoopAsync │◀──▶│ per-peer 复制 lane │ │
│ Deadline │Tick│ │ 泵线程:事件消费│ │ 泵线程:批发送/应答/commit │ │
│ Registry │ │ │ 心跳/选举定时 │ │ 推进(next/match/hint 回退)│ │
└─────────────┘ │ └──────┬───────┘ └────────────▲───────────────┘ │
│ │ append │ 复制应答/快照 │
│ ┌──────▼───────┐ ┌────────────┴───────────────┐ │
│ │ 入站分发 Rpc │ │ 出站 RPC(AppendEntries/ │ │
│ │ 直排:AE/Join │ │ Vote/ReadIndex 转发) │ │
│ │ 事件:Vote/等 │ │ │ │
│ └──────────────┘ └────────────┬───────────────┘ │
└───────────────────────────────────┼──────────────────┘
│ IProtocolTransport
┌───────────────┐ ┌────────────────────▼───────────────┐
│ 业务状态机 │◀────│ ApplyPipeline(提交→应用管道) │
│ IStateMachine │apply│ 单 worker 泵线程:按日志序 apply │
└───────────────┘ │ appliedIndex 节流落盘 / 配置条目分流 │
┌───────────────┐ └────────────────────▲───────────────┘
│ 存储端口 │◀─────────────────────────┘
│ IRaftStore │ append/prevLog 断言/截尾/双水位/快照点
│(可插拔:内存 / │
│ TierWal 适配)│
└───────────────┘
- 共识引擎
RaftStateMachine(Raft/RaftStateMachine*.cs,~8k 行/43 文件):actor 单 worker 事件循环——共识循环(Raft/RaftStateMachine.Loop.cs)+ 角色转换/选举(.Election.cs)+ 入站分发/RPC 处理(.Rpc.cs)+ 本地事件/快照(.Events.cs)。 - 复制引擎
ReplicationProcess(Raft/ReplicationProcess.cs):leader 侧全部——每 peer 一条 复制 lane(PeerLane),nextIndex/matchIndex/在途窗口/批发送/冲突回退/commit 推进(Figure 8)。 - 提交→应用管道
ApplyPipeline(Raft/ApplyPipeline.cs):与共识循环解耦的独立单 worker, 严格按日志序 apply、appliedIndex 节流落盘、配置条目分流。 - 存储端口
IRaftStore:raft 对持久化的全部需求(term/votedFor/日志追加/prevLog 断言/ 截尾/双水位/快照点)——实现它即可接入任意存储(内存/文件/TierWal)。 - 业务状态机
IStateMachine:ApplyAsync单 worker 调用,at-least-once 幂等契约 (业务状态 = 已提交日志前缀的确定性函数)。 - 节拍注册表
DeadlineRegistry(src/KernLab.Tier.Core/Execution/DeadlineRegistry.cs): tick 投递走专用线程 + WaitHandle 等待(零 TimerQueue——见 §4.5)。 - 传输
IProtocolTransport:TCP/QUIC/InProcess 三介质等价。
1.2 线程与泵模型(活性地板)
每节点协议面专用线程(不进线程池——2026-09-03 活性判例:池续体偶发不执行 = 心跳停发 = 全集群冻结):
| 线程 | 数量 | 职责 |
|---|---|---|
| 共识循环泵线程 | 1 | LoopAsync——事件消费、心跳/选举定时、本地 append |
| 复制 lane 泵线程 | 每 peer 1 | RunLaneAsync——批发送/应答/commit 推进(peer 间并行) |
| apply worker 泵线程 | 1 | WorkerAsync——按日志序 apply |
| 节拍线程 | 1(可共享) | DeadlineRegistry——tick 投递(封顶 ~100ms 扫描粒度) |
| 传输收发循环 | 按介质 | TCP 每链收发 / InProcess hub |
泵域语义(AsyncPump 单线程泵):域内 await 一律不写 ConfigureAwait(false)——续体经
PumpContext 回流泵线程,共识/复制/apply 关键路径零线程池依赖。协议专用线程是活性地板,
不受资源档位影响(见 raft-node.md 资源档位一节)。
1.3 消息路径(两类消息两条路径)
| 消息 | 路径 | 理由 |
|---|---|---|
| AppendEntries 请求(心跳/日志复制) | 直排:请求回调线程 → _followerAppendGate 串行 → 应答(Raft/RaftStateMachine.Rpc.cs:74-80,435) |
活性续约路径——收到即重置选举定时器,不依赖事件队列/循环线程;队列满或循环被饿时心跳续约不受阻 |
| Vote / InstallSnapshot / ReadIndex / TransferLeader 请求 | 事件化:入队 _events → 共识循环串行处理 + _clusterLock(Raft/RaftStateMachine.Rpc.cs:81-85) |
低频、需跨状态机状态裁决——循环线程串行 + 锁,避免每请求一线程 |
| AppendEntries 应答(follower → leader) | 复制 lane 请求任务回投 HandleRespAsync → 投槽/直跑(Raft/ReplicationProcess.cs:435-453) |
lane 单写者——应答由链消费,leader 侧无锁 |
| 本地事件(Replicate/ReadIndex/ConfigChanged/Tick) | 事件队列 → 共识循环 | 单 worker 串行——本地 append 单写者 |
心跳直排是活性地板:follower 靠"收到即处理"续命。若走事件队列,队列满或循环被饿时心跳 被阻塞会误触发选举(#504 挂死族根因之一,见 §2.6)。
2. 稳定性:论文核心性质如何实现
2.1 选举安全(Election Safety)——一个任期至多一个 Leader
- PreVote 先于真选举(
Raft/RaftStateMachine.Election.cs:174-199):预选举不抬集群 term, 分区节点无法借预票把集群 term 抬上去(论文 §9.6)。预票按日志新鲜度授予,拿到多数派预票 才落盘抬任期走真选举。 - 在位 leader 否决预票(
Raft/RaftStateMachine.Rpc.cs:328-340):本节点是 leader、或已知 leader 且其心跳在最近一个选举窗内仍在续约 = 集群有主 → 拒预票——防扰主真选举(term 空转)。 leader 真死后心跳停止续约,ElectionTimeoutMax内否决自动解除。 - 抬任期落盘先于应答/广播(
Election.cs:34-98StepDownAsync、StartRealElectionAsync:207): 契约①——WriteTermAndVoteAsync先于SetRole/应答。term 一旦写入即不可回退。 - 投票按日志新鲜度
IsUpToDate(Election.cs:349-353):term 优先,同 term 比 index—— 确保日志最新者当选(与领导权完整互推)。 - 每 term 至多一票:
votedFor落盘原子化(Rpc.cs:373-398)——split vote 后不自锁 (PreVote 不沿用 votedFor 门禁,票唯一性由真实投票门禁保证)。 - learner 不投票不自荐、witness 投票但不自荐(
Election.cs:154-155)——多数派口径 = voter + witness,learner 不占席位。
2.2 领导权完整(Leader Completeness)——已提交条目必在新 Leader 日志
- 上任即任期锚点 no-op(
Raft/RaftStateMachine.Election.cs:297-328BecomeLeaderAsync):commit 计数 只认本任期条目(论文 §8.2)——不追加则 commitIndex 停在前任期水位,ReadIndex 以其应答 = 破约。 - 锚点门:锚点未提交前 ReadIndex 轮次/租约快路径挂起(
_readIndexHoldIndex)——新 Leader 不得以旧任期水位应答线性读。 - commit 只经当前任期条目直接提交(
Raft/ReplicationProcess.cs:1145-1165TryAdvanceCommitAsync): Figure 8 约束——log[n].term == currentTerm才推进;旧任期条目随该次提交间接提交 (防"已提交条目被新 leader 覆盖",与日志匹配互推)。
2.3 日志匹配(Log Matching)——同 index 同 term 则内容相同,前序也相同
- prevLog 断言 + 冲突截尾一体:
IRaftStore.AppendAsync(prevIndex, …)的 prevIndex 前置断言 + 分叉尾随断言截断(TierWalRaftStore.cs:175-245适配器)——纯追加或断言截尾,无第三种。 - 已提交区保护(
Raft/RaftStateMachine.Rpc.cs:510-522):乱序迟到的旧批(prevLogIndex < 本地 commitIndex,重写区间含已提交条目)拒绝并指向 commit+1——leader 单调回退收敛,日志尾不被 截断。 - 冲突回退 hint 加速(
Raft/ReplicationProcess.cs:566-592):拒绝带 conflict term/index—— 一次跳到min(nextIndex-1, 该 term 首条)(或前向 hint 的ConflictIndex ≥ next快照衔接), min 公式保证单调递减;拒绝一次回退一格是正确性基石。 - 快照边界衔接:
nextIndex ≤ SnapshotIndex→ 转快照安装(ReplicationProcess.cs:675-682)—— 增量复制无法接续的边界,快照覆盖点 N₀ 后从 N₀+1 续传。
2.4 状态机安全(State Machine Safety)——同 index 在所有节点 apply 同一条
- commit 安全对
(commitIndex, term)复合原子(Raft/RaftStateMachine.cs:120-136,Atomic128<CommitPair>):推进瞬间 term 快照入对——期间换届则 CAS 失败重读放弃, 不能提交旧 term 条目(§5.4.2 语义不依赖 leader-term 不变式)。 - apply 单 worker 严格日志序(
Raft/ApplyPipeline.cs:201-283):[lastApplied+1, commitIndex]区间逐条 ApplyAsync——单消费者 FIFO 保序。apply 落后安全(只延迟读服务,不产生错误状态)。 - at-least-once 重放 + 业务幂等:重启重放同一前缀;
IStateMachine契约要求确定性幂等 (同 index 同命令 → 同状态)。apply 异常重入队有界重试,不杀 worker(ApplyPipeline.cs:203-230)。 - 快照推进竞态护栏(
ApplyPipeline.cs:234-238):快照把 SnapshotIndex 推过本地 applied 时 双源读不原子——重读拿新 N₀ + 重入队有界自愈(防 applied 静默停滞)。
2.5 领导权只追加(Leader Append-Only)——Leader 不覆写/删除自己日志
- 并入 §2.2/§2.3:Leader 侧
AppendEntriesAsync恒纯追加(Raft/RaftStateMachine.Events.cs:70-74); 截尾只发生在 follower 侧(响应 prevLog 断言失败);Figure 8 约束阻止经旧任期条目提交。 Leader 日志 = 追加单调序列(含快照导入后的续传)。
2.6 活性(Liveness)——及时选主、及时推进
- 心跳 ≪ 选举窗:心跳缺省 50ms、选举窗 150-300ms(
Raft/RaftOptions.cs:14-20)——丢 1-2 个 心跳不触发误选举,leader 真死 1-2 窗内完成换届。 - 泵线程隔离池饥饿(§1.2):共识/复制/apply 关键路径零池依赖。
- tick 去 TimerQueue(
Raft/RaftStateMachine.cs:391-440+ DeadlineRegistry):.NET 8 TimerQueue 高频触发窗口丢表项(孤儿判例)→ 拉取模型专用线程,tick 是共识循环唯一活性源。 - 调度饥饿鲁棒(#504 挂死族根修,
Raft/RaftStateMachine.Loop.cs:130-146+Raft/RaftStateMachine.cs周期观测订阅): 选举超时按观测时间而非墙钟计数——泵线程被饿期间循环未运行,CheckDeadline到期且gap = now - _lastLoopProgressTicks ≥ ElectionTimeoutMin时判定观测能力缺失,重掷选举截止 不触发选举(让被饿期间积压的 leader 心跳先被处理);周期观测订阅保证观测粒度 < 阈值 (区分"正常窗满"与"真被饿")。leader 真死时节点持续观测,窗口满即正常换届(活性保持)。 - 复制 lane 自愈:在途超时 = 重试窗口(
ReplicationProcess.cs:600-616,应答丢失持续重试 论文 §5.3);心跳 tick 兜底唤醒;冲突回退立即重试。 - 观察点(
Raft/RaftStateMachine.Loop.cs:54-68):每个事件处理后CheckDeadline+FlushPendingAppendsAsync——事件密集时心跳/选举/复制仍推进;空闲时节拍封顶 ~100ms 粒度触发。
3. 有序性:提交全序、日志序、apply 序
3.1 提交全序(commit 单调)
- 多数派 matchIndex 的第
MajorityThreshold大值 = 可提交位(ReplicationProcess.cs:1103-1118, 含自己 match = 本地 PersistedIndex;learner 不进数组;witness 计入)。 - 推进单点
AdvanceCommit(Raft/RaftStateMachine.Events.cs:290-304):复合 CAS 原子推进 + term 快照 + 投递 apply + 完成 committed 档等待者 + 事件——多 lane 并发上报安全,重复上报幂等跳过。
3.2 日志序(Leader 追加序 = follower 应用序)
- Leader 排空攒批(
Raft/RaftStateMachine.Events.cs:27-62):一次排空队列连续 Replicate 事件 → 一批一次AppendAsync→ 批内 index 连续 → 逐条注册完成源(applied/committed/LeaderLocal 档分流)。 批内 index =startIndex + i——命令顺序即日志顺序即返回 index 顺序。 - Follower gate 串行(
Raft/RaftStateMachine.Rpc.cs:467-504):_followerAppendGate内 term 重查 + prevLog 校验 + 追加 + 持久化 + 提交——检查序列不交错(请求回调线程并发到达, 门串行)。提交跟随 =AdvanceCommit(min(LeaderCommit, PersistedIndex))(Rpc.cs:624-627)。
3.3 apply 序(状态机安全的有序面)
ApplyPipeline单 worker 严格按[lastApplied+1, commitIndex]区间应用(ApplyPipeline.cs:239-283)—— 业务状态机的 apply 顺序与全集群提交顺序完全一致(同 index 同命令,见 §2.4)。- 配置条目分流(
ApplyPipeline.cs:244-255):配置切换 = apply 产物——_currentConfig更新 +PostConfigChanged回调状态机(spec-04)。配置条目的 apply 序同样严格。
3.4 重复容忍与乱序防御
- at-least-once:崩溃重启重放同一前缀(apply 幂等)。这是唯一的重复来源——正常运行时 单消费者 FIFO 恰好一次。
- 乱序防御:已提交区保护拒绝乱序迟到旧批(§2.3);冲突回退单调收敛。
- 完成源幂等:注册前检查"index ≤ 水位"立即完成(
Raft/RaftStateMachine.cs:716-739)—— 并发直排下重复完成不可能(slot 状态机 + 水位判定)。
4. 高性能设计
4.1 心跳直排(§1.3)
AppendEntries 请求经 _loops.SubmitFast 受控提交(请求回调线程),_followerAppendGate 串行——
不占事件队列、不占循环线程、每心跳零队列排队。高并发下事件积压不影响心跳活性路径。
4.2 批复制(攒批三维度 + 流水线)
- Leader 侧排空攒批(§3.2):一次 append 一批,摊薄 channel 读 + append 写锁往返 + 注册 ×N。
- 复制侧三维度攒批(
ReplicationProcess.cs:311-363):条数(BatchSize)/ 字节(MaxBatchBytes)/ 时间(BatchWindow,one-shot Timer 独立到期,不骑心跳 tick)——+ 微 linger (LingerMilliseconds,亚 tick 粒度,摊薄 per-RPC 开销)。 - 窗口 N 流水线(
ReplicationProcess.cs:606-616,743-749):per-peer 在途批上限(缺省 8)—— 批间不等待应答,发 A 时可发 B;乐观推进 nextIndex 防重复批。 - 心跳空批零 WAL 读(
ReplicationProcess.cs:694-710):无滞后且 prevLog 锚命中缓存 → 免WaitForPersistedAsync+ReadLogTermAsync——选举窗活性关键路径与 WAL 锁解耦。
4.3 W4 persist 下环(fsync 不阻塞循环)
组提交发起为在途任务、不内联等待(Raft/RaftStateMachine.Loop.cs:151-192):fsync 期间
事件循环照常消费(心跳/选举/复制不受阻);任务完工自投 PersistCompleted 唤醒,下一观察点收尾。
单写者不变量:commit 只在循环线程发起(在途门——同串 fsync 不并发)。
4.4 零分配/低分配工程
| 技术 | 位置 |
|---|---|
| 池化状态机(SendRpcState/PooledValueTaskSource/PooledBufferWriter/ArrayPool 帧) | ReplicationProcess.cs:805-1097 |
复用批缓冲(_leaderBatch/_followerBatch)与 lane 条目区直写(RegionWriter/TermsExact) |
Events.cs:44-51、ReplicationProcess.cs:717-728 |
| 缓存委托(AppendDirectWork.Run / lane.SendFrameFunc——免闭包 + display class) | Rpc.cs:33-54、ReplicationProcess.cs:812-813 |
| 事件池化(RaftEvent.Replicate.Rent/Return) | Raft/RaftEvent.cs |
| 无状态节拍事件单例(TickEvent/PersistCompletedEvent 零分配) | Raft/RaftStateMachine.cs:168-169 |
| CAS 热路径(CommitPair/LeaderSlot Atomic128——16B 原子读) | Raft/RaftStateMachine.cs:120-141 |
4.5 泵线程去池跳 + 拉取模型节拍
- 关键路径全部专用泵线程(§1.2)——省线程池调度跳 + 续体丢失风险。
- DeadlineRegistry 拉取模型(
DeadlineRegistry.cs):条目不存 deadline 值,持计算委托 + 唤醒回调;节拍线程每轮重扫全部条目,到期者回调。分片懒启、退订零查找、单条异常隔离不杀线程。
5. 读面与一致性语义
| 面 | 语义 | 位置 |
|---|---|---|
| 复制完成档·applied | 返回 = committed 且 applied(read-your-writes) | Raft/RaftStateMachine.cs:591-614 |
| 复制完成档·committed | 多数派提交即返(不等 apply,吞吐优先) | Raft/RaftStateMachine.cs:679-700 |
| 复制完成档·LeaderLocal | leader 本地持久化即返(异步一致——换届丢未复制尾部写) | Raft/RaftStateMachine.cs:628-636 |
| 线性读 ReadIndex | leader 向多数派确认身份后返回 commitIndex(分区不可答 = 不返回过期 index) | Raft/RaftStateMachine.cs:847-863 |
| 租约读(opt-in) | 多数派确认后 (选举窗下界 − 漂移界) 窗内零往返;窗外回落心跳确认 | Raft/RaftStateMachine.cs:851-857 |
| follower 转发读 | 转发 leader → leader 多数派确认 → 本端等 applied ≥ readIndex | Raft/RaftStateMachine.cs:876+ |
- 非 Leader 写入/读快速失败
NotLeaderException(携带当前已知 leader,客户端重路由标准模式)。 - 换届在途完成源统一以
NotLeaderException取消——客户端重试即可,不是错误。
6. 配置变更与引导
- single-server 配置变更(论文 §6 一次性成员变更):
ProposeConfigAsync追加配置条目 (Raft/RaftStateMachine.cs:786-799)→ apply 产物切换(Raft/RaftStateMachine.Events.cs:239+) → 复制引擎ApplyConfig重建 lane(新成员起链、移除成员停链,保留成员在途批不动) (ReplicationProcess.cs:206-236)。多数派 = 当时活动配置的多数派(Quorum.cs纯函数)。 - learner 引导/晋级:新节点以 learner 身份加入(
JoinAsync),追平自动晋级 voter—— 追赶期不占多数派席位,不扰动写入可用性(Membership.cs+RaftStateMachine.cslearner 晋级面)。 - witness:投票计多数派、无日志体(高水位断言流)、永不自荐(DDR-F2,三期面)。
7. 快照与恢复
- InstallSnapshot:nextIndex ≤ 快照覆盖点触发(§2.3);follower 导入重建(
RebuildCoordinator) 后从 N₀+1 续传增量(ReplicationProcess.cs:455-478)。 - 双源读(
ApplyPipeline.cs:19-21,239-283):applied < N₀ 的跨界区间由快照读面补齐—— 管道无需感知快照面;快照 = 可导出的日志前缀镜像。 - 快照压缩/发布/反熵由产品层宿主调度驱动(见 raft-node.md)。
8. 可观测性(稳定性取证面)
| 面 | 内容 | 位置 |
|---|---|---|
| 循环活性诊断 | LoopLagMs / TickLagMs / QueueDepth / TickWriteFailures / DeadlineInMs(判别:tick 链断 vs 循环卡 handler vs deadline 缺陷) | Raft/RaftStateMachine.cs:258-274 |
| 节拍注册表取证 | 每 shard:pacer 存活 / pacerLag / wakeLag / 每条目 last-wake(tick 链断判别) | Raft/RaftStateMachine.cs:276-278、DeadlineRegistry.cs:146-154 |
| 选举轨迹 | 环形记录 96 槽,收敛停滞时倾倒转换步骤 | Raft/RaftStateMachine.cs:154-158,469-485 |
| 共识指标 | raft.elections_started / leader_changes / commit_index / applied_index | Raft/RaftStateMachine.cs 各 _raftView 点 |
| 状态导出 | healthz/readyz/管理面(Role/Term/Commit/成员与复制进度) | Raft/RaftStateMachine.cs:431+ |
9. 边界与开放决策
- 时钟同步纪律:租约读要求 NTP 同步(漂移界 < 选举窗)——不满足则保持缺省关。
- LeaderLocal 档的丢写窗口:leader 宕机时未复制到任何成员的尾部写丢失(已复制部分经 最高日志者当选保留)——强一致需求用默认档/committed 档或 ReadIndex。
- 无跨节点恰好一次原语:raft 层提供有序复制 + 幂等重放;恰好一次(去重)由业务状态机 (IStateMachine 幂等契约 + 幂等键)承担——产品层实例见 raft-consistency.md。
- 快照安装传输面:引擎提供端口与触发,传输面由装配决定(单源流式 / 多源 swarm)。
10. 相关文档
- 使用面(怎么调用):net.md §6
- 产品装配(TierRaftNode/TierWAL 节点):raft-node.md
- 产品层论文核心保证(TierQueueReplica 实例):raft-consistency.md
- 调度饥饿鲁棒设计(#504 根修):见 §2.6(设计依据为内部设计稿,不随包发布)