LECTURE 10 · 2026-03-12 · 复制与一致性

Raft 实验问答与调试

Raft Lab Q&A

这不是新算法,而是一次实现校准:把 Raft Figure 2 翻译成锁纪律、计时器、持久化点和可复现调试证据。

144 MIN进阶03 SOURCESFULL ARCHIVE

这讲要解决什么

开始前先确认
  • 能区分网络延迟、节点崩溃与部分失败
  • 会用状态机和不变量描述协议
  1. 解释从不变量倒推代码的核心问题
  2. 按协议顺序推演锁与 RPC 的标准形状
  3. 评估工程取舍:更丰富的日志提升可调试性,但必须结构化和可筛选,否则并发输出会淹没因果链。

调试 Raft 的目标不是解释最后一次超时,而是找到第一次非法状态

一个测试可能在 30 秒后报告“没有达成一致”,真正错误却发生在第 2 秒:旧 term 回复推进了 matchIndex,错误 commit 又触发 apply,后续选举只是连锁反应。日志越多,最终症状越容易淹没最早分叉。

先写不可违反的不变量:term 单调;每任期每节点至多投一票;同 index 已应用值唯一;commitIndex/lastApplied 不倒退;leader 只按当前 term 规则提交;snapshot 边界与服务状态一致。调试器和日志都围绕这些断言设计。

课堂 Q&A 和 guidance 强调可复现时间线。你需要把网络事件、节点状态和测试器故障注入放到同一逻辑序列中,而不是依赖不同机器的墙钟精确同步。

从不变量倒推代码

先写下关键不变量:term 单调增加、每 term 最多投一票、日志匹配、已提交索引单调、apply 顺序不回退。每个 handler 和回复处理都要说明自己保持了哪些不变量。若修复只是在某个测试上加 sleep 或特殊分支,却无法解释不变量,通常会在另一种调度下失败。

锁与 RPC 的标准形状

在锁内读取状态并构造不可变 RPC 参数,释放锁后发送,重新加锁后检查回复是否仍属于当前 term/角色。不要持锁等待网络;不要让两个 goroutine 无纪律地修改 nextIndex;不要把 applyCh 发送放在锁内。使用短临界区并集中状态转换,降低竞态组合。

计时器与陈旧事件

选举计时器应由有效 leader 通信或投票行为重置,并使用随机超时。旧 term 回复、旧 timer firing 和旧 leader goroutine 都可能在角色变化后到达;处理前必须再次检查 term 与状态。逻辑时钟条件比墙钟睡眠更可靠。

可复现调试

运行单个测试很多次并启用 race detector;记录 term、角色、日志边界、RPC 方向和 commit/apply 变化。官方 tester 的时间线可标注自定义事件,适合定位分区与崩溃交界。保留失败 seed 和短日志,先找到最早违反不变量的位置,而不是追最后一个报错。

先检查单调量,再检查消息时序

Raft 调试最有效的入口是列出永不倒退的量:currentTermcommitIndexlastApplied、每个 leader 的 matchIndex,以及快照基准。若日志截断,逻辑末尾可后退,但已提交前缀绝不能被覆盖;若发现单调量回退,通常比最终测试超时更接近根因。

第二组是不变量配对:同一 term 最多一个有效 leader;同 index 同 term 的日志前缀相同;只有已提交且按序的条目进入 applyCh;成功回复前相应持久状态已经保存;RPC 回复只能修改创建它的 term/role 上下文。

日志应记录状态变化而不是每次循环:选举超时、term/role 转换、投票决定与拒绝原因、日志冲突位置、commit 推进、snapshot 安装。每条带节点 ID、term、index 和逻辑事件号,才能把多节点输出合并成因果序列。

遇到测试挂住,不要先延长 timeout。抓 goroutine dump,查谁持锁、谁在 channel/RPC/Cond 等待,再问产生该事件的 goroutine 是否因同一锁阻塞。分布式死锁常是“持锁发 RPC—远端 handler 等本地另一事件—本地事件线程等锁”的跨节点等待环。

让一条日志能回答“谁在什么版本下改变了什么”

推荐事件字段:相对时间、node、term、role、event、peer、index/range、旧值→新值、request term。S2 T7 leader AE-ok S1 match 10→14 reqT7 比“append success”多不了多少,却能验证角色、任期和单调性。

给每次 RPC 记录 send 与 receive,并携带请求快照;给 persist、commit、apply、snapshot 单独事件。故障注入记录 partition/crash/restart/heal。这样可以从错误 apply 向前追到 commit,再追到哪组 matchIndex 证据和哪批 RPC 回复。

日志必须可筛选。正常心跳可以采样或只在状态变化时记录,避免每 100 ms 淹没关键信息。不要在持锁路径进行慢格式化或文件 I/O;先复制小状态摘要,锁外输出。

最有价值的日志往往是断言前的结构化快照。若发现 lastApplied > commitIndex,立即输出相关日志边界并失败,比等待测试最终 timeout 更接近根因。

每个 RPC 都有出生证和失效条件

发送 RequestVote/AppendEntries 前,在锁内构造不可变参数并记录发送时的 term、role 与 prev 边界,随后解锁执行网络调用。回复回来重新加锁,先处理更高 term,再检查自己仍处于原上下文;过期成功或失败回复都可能无效。

并行复制常见 bug 是失败回复把 nextIndex 大幅倒退,覆盖稍后已成功的进度。可为每个 follower 使用单一复制 goroutine,或令成功更新 matchIndex=maxnextIndex=max(match+1),失败只在回复对应当前探测边界时回退。不要让数组字段成为无版本的共享建议。

定时器也有生命周期。重置 Timer 前安全 drain channel,或采用“记录最近有效通信时间、周期检查”的简单结构;旧计时事件到达时重新验证期限。仅收到低 term/无效请求不应把 follower 永久压住选举。

Kill 后后台 goroutine 要尽快退出,阻塞等待应能被 done 信号唤醒。测试重复建群时,旧实例 RPC 或 ticker 若继续运行,会以合法节点 ID 向新测试注入消息,表现得像无法解释的协议错误。

逐个审查旧回复、重复回复和乱序回复

对每类 RPC 画生命周期:锁内构造参数与版本→解锁发送→网络延迟/丢失→远端处理→回复→本地重新加锁→验证→合并。每个箭头都插入一次本地 term/role 变化,问回复是否仍有效。

RequestVote 赞成票只在仍为同 term candidate 时计数;AppendEntries 成功只在仍为同 term leader 时推进,且 matchIndex 单调;失败回退不能覆盖更晚成功;InstallSnapshot 回复不能把 nextIndex 拉回 snapshot 之前。看到更大 reply term 始终先降级。

重复 RPC handler 要幂等。follower 重收相同 AppendEntries 不应重复追加或倒退 commit;applier 不应因条件多次唤醒重复发送同 index。持久化恢复后,旧网络消息也可能到达新进程,term/index 检查必须足以拒绝。

把这些规则写成合并回复的 guard,日志只记录 guard 通过的状态变化和被拒绝原因。你就能区分“网络很乱但协议正确”和“旧工作污染新状态”。

从单路径到故障矩阵的实现顺序

先在无丢包三节点上验证稳定选主与心跳,再验证 leader 隔离、少数派不得提交、重连后日志收敛。每加入一层功能都保留前一层测试:持久化阶段加入逐点重启,快照阶段同时覆盖落后 follower、乱序 snapshot 与恢复 apply。

测试失败要最小化场景:固定随机种子、缩短操作数、保留第一次不变量破坏前后的日志。重复一百次只得到“偶尔错”信息不够;目标是把故障压缩成几台节点、几个 term 和有限条日志的时间线。

性能测试不是最后装饰。过密 heartbeat、为每条日志启动无界 goroutine、持锁编码大快照都可能在可靠网络测试通过,却在 unreliable/long reordering 下触发超时。记录 RPC 数、字节量、goroutine 数和锁等待能帮助区分活性 bug 与纯粹低效。

不要根据某个公开测试写特判。测试名称只描述一个观察角度,真正要求来自 Figure 2 规则和状态机安全性。每次修复都用一句不变量解释;若只能说“加 sleep 后通过”,修复尚未建立正确因果。

DIAGRAM IN CONTEXT

把上面的机制落到消息、状态与失败路径中。

失败测试时间线将日志按 term/index 对齐,找第一条与协议不变量冲突的事件。
S1election / appendS2
S2persist / replyS1
Testerpartition / crash / healCluster
Appliercommit → applyService

一套从单测到长跑的固定调试流程

第一步固定随机种子并单独运行最小失败测试;第二步开启 race detector,先消除内存竞争;第三步用断言找到首个不变量破坏;第四步缩短时间线,关闭无关日志;第五步修复后用多个种子和高重复次数验证不是时序偶然。

不要用增加 sleep 作为修复。sleep 只改变调度,可能降低复现概率;协议需要等待状态谓词或使用正确 timer。不要无限调大超时,除非已经用时间线证明实现只是合理地慢,而非丢失唤醒或阻塞持锁。

按 Lab 阶段隔离:3A 只看 term/vote/timer;3B 看 log/next/match/commit/apply;3C 看回复前 persist 与 restart;3D 看逻辑 index 和 snapshot 边界。跨阶段日志太早混合会扩大搜索空间。

最后保存一个最小反例说明:初始状态、故障操作、关键事件、违反的不变量、修复后的 guard。长期看,这比只留下通过的代码更能建立分布式调试能力。

教案覆盖地图

100%教师材料入库
11中文教学单元
01机制 / 板书图
03一手资料

覆盖口径:教师 notes/讲义原文逐行完整保留;中文教学单元覆盖课堂机制、失败路径与工程取舍;1 幅辅助机制图;论文另设“问题—机制—证据—边界”阅读导航。覆盖不是用摘要替代原文,任何细节都可在页面末尾回查。

教师教案notes/l-raft-QA.txt

75 行 · 310 词 · 完整可搜索文本

课堂配套labs/guidance.html

103 行 · 597 词 · 完整可搜索文本

课堂配套labs/vis.html

16 行 · 13 词 · 完整可搜索文本

展开中文教学单元映射(11 项)
  1. 01调试 Raft 的目标不是解释最后一次超时,而是找到第一次非法状态
  2. 02从不变量倒推代码
  3. 03锁与 RPC 的标准形状
  4. 04计时器与陈旧事件
  5. 05可复现调试
  6. 06先检查单调量,再检查消息时序
  7. 07让一条日志能回答“谁在什么版本下改变了什么”
  8. 08每个 RPC 都有出生证和失效条件
  9. 09逐个审查旧回复、重复回复和乱序回复
  10. 10从单路径到故障矩阵的实现顺序
  11. 11一套从单测到长跑的固定调试流程

论文要读到哪里

READING TARGETnotes/l-raft-QA.txt + labs/guidance.html + labs/vis.html
核心问题

怎样从失败测试的时间线定位最早的不变量破坏?

机制主线

给日志加 term、role、index、peer 与事件类型;用可视化器重建选举、复制、提交和 apply 的因果链。

必读证据

从首个异常状态向前查,而非盯着最后一次超时;同一随机种子重复并用单测隔离阶段。

适用边界

增加 sleep 或无限日志只能改变时序并淹没证据,不能修复竞态。

把直觉校准成不变量

误区

增加 sleep 能从根本上修复 Raft 偶发失败。

sleep 只改变调度概率;应定位竞态、陈旧回复或不变量破坏,并用状态条件修复。

误区

只记住正常路径就足以实现协议。

分布式协议的正确性主要由超时、重试、重排、崩溃恢复和旧消息路径决定。

知识检查

处理 RPC 回复前最重要的重新检查是什么?

下列哪项最准确概括本讲的主要工程取舍?

为什么“增加 sleep 能从根本上修复 Raft 偶发失败。”是错误的?

离开本讲前,你应能复述

  • 先写下关键不变量:term 单调增加、每 term 最多投一票、日志匹配、已提交索引单调、apply 顺序不回退。
  • 更丰富的日志提升可调试性,但必须结构化和可筛选,否则并发输出会淹没因果链。
  • sleep 只改变调度概率;应定位竞态、陈旧回复或不变量破坏,并用状态条件修复。

完整官方资料附录

以下是本讲对应官方材料的可搜索离线文本。中文精读负责解释;资料附录保留原始细节、例子、问答与代码,不以摘要替代原文。

课堂讲义notes/l-raft-QA.txt75 行 · 310 词 · 完整收录
6.5840 Lecture 10: lab 3 A+B Q&A

Two different structures of Raft library
  multi-threaded with locks
  single-threaded state-machine
  neither one is strictly better than the other
  other plans are possible

Correctness in distributed systems:
 safety: never returning an incorrect result
 liveness: eventually returning a result
 Q: give an example of a safety and liveness bug in your solution

Getting started on a lab
  make first test case work
   fill out VoteRequest structs
   do an RPC
  then much easier to get the lay of the land
  other strategy: read all guides, read test code, etc.

Debugging
  Run with race detector
  Log all action/messages in easy searchable way
   standard format: src, dst, opcode, raft state,..
  Run test case
    if ok:
      next test case
    if fail:
      repeat:
        study test case
        formulate hypothesis about what might be wrong
        study log and figure 2, run with race detector
        modify code and try test again

Multi-threaded Raft library
  many threads (Start() thread, thread reading from applych)
  many RPC threads to talk to peers in parallel
  many RPC handler threads (started by RPC package)
  raft state with lock
  one thread writing to applych
    use condvar to signal it

Locking
  Raft lock serializes operations
  RPC handlers hold Raft lock so are atomic
    little parallelism
  Threads don't hold lock during RPC
    risk: deadlock if locks are held during RPC

Single-threaded state-machine
  observation: little parallelism anyway
    main use: sending RPCs in parallel
  one thread runs Raft protocol
    receives input events (e.g., tick, RPC request, reply)
    process event, updating raft structure, without locks
      collecting output events (persist, send RPCs)
    performs output events
      first persist, then RPC requests
  one thread for the applier
    need to separate state between applier and state-machine
    protect shared state with lock (e.g., the log)

Code tour
  Raft struct
  Ticker
  Election timeout
  Start election
  VoteRequest handling
  becomeLeader
  send appends
  AppendEntries request handling
  AppendEntries reply handling
  commit
  applier
  Start()
网页讲义labs/guidance.html103 行 · 597 词 · 完整收录
Lab guidance


Lab guidance

Hardness of assignments


Each lab task is tagged to indicate
roughly how long we expect the task to take:

    Easy: A few hours.

    Moderate: ~ 6 hours (per week).

    Hard: More than 6 hours (per week). If
    you start late, your solution is unlikely to pass all tests.



Most of the labs require only a modest amount of code
(perhaps a
few hundred lines per lab part), but can be conceptually difficult
and may require a good deal of thought and debugging.
Some of the tests are difficult to pass.

Don't start a lab the night before it is due; it's more
efficient to do the labs in several sessions spread over multiple
days. Tracking down bugs in distributed systems is difficult,
because of concurrency, crashes, and an unreliable network.

Tips


Do the Online Go tutorial and
  consult
  Effective Go.
    See Editors to
    set up your editor for Go.

The lab Makefiles are set up to use
Go's race detector.
Fix any races it reports.

Advice on locking in labs.

Advice on structuring your Raft lab.

This Diagram of Raft interactions may
help you understand code flow
between different parts of the system.

Learn about Go's Printfformat strings:
Go format strings.

 To learn more about git, look at the
Pro Git book or the
git user's manual.

Debugging

Efficient debugging takes experience. It
helps to be systematic: form a hypothesis about a possible cause of the
problem; collect evidence that might be relevant; think about the
information you've gathered; repeat as needed. For extended debugging
sessions it helps to keep notes, both to accumulate evidence and to
remind yourself why you've discarded specific earlier hypotheses.

The most effective debugging technique is often to add print
statements to your code, run the test that is failing and collect the
print output in a file, and then look through the output file to
identify the point at which things start to go wrong. You may need to
iterate, adding more print statements as you learn more about what is
going wrong.

Concurrency among different peers and among the threads in a single
peer can cause actions to be interleaved in unexpected ways. For
example, it's quite possible for a Raft peer to be elected leader
while the previous leader still thinks it is the leader, or for a
leader to send an RPC but receive the reply after it has lost
leadership. Adding print statements may help you spot such situations.

Feel free to examine the test code (mr/mt_test.go,
raft1/raft_test.go, &c) to understand what the tests are
exploring. You can add print statements to the tests to help you
understand what they are doing and why they are failing, but be sure
you code passes with the original test code before submitting.

The Raft paper's Figure 2 must be followed fairly exactly. It is easy
to miss a condition that Figure 2 says must be checked, or a state
change that it says must be made. If you have a bug, re-check that all
of your code adheres closely to Figure 2.

As you're writing code (i.e., before you have a bug), it may be worth
adding explicit checks for conditions that the code assumes to be
true, perhaps using Go's
panic. Such checks may
help detect situations where later code unwittingly violates the
assumptions.

The TAs are happy to help you think about your code during office
hours, but you're likely to get the most mileage out of limited office
hour time if you've already dug as deep as you can into the situation.
网页讲义labs/vis.html16 行 · 13 词 · 完整收录
Porcupine





        Clients

        Time



        Valid LP

        Invalid LP
        [ jump to first error ]