Raft: чтение кода и логов
Читай обработчики Raft RPC, шаг state machine и реальный лог AppendEntries, затем выбирай поведение, баг или фикс с наибольшим рычагом, который senior-инженер делает первым.
Баги Raft прячутся в четырёх местах: consistency check в AppendEntries, продвижение commit index, правило выдачи голоса и state machine, применяющая лог. Читай каждый сниппет так, как читал бы на ревью или в инциденте, и выбирай то, что senior-инженер пометит первым.
Потренируйся читать протокол там, где он реально живёт — в обработчиках RPC и строках лога — и замечать off-by-one, отсутствующую проверку и сигнатуру расхождения, отделяющие корректный Raft от портящего данные.
Сниппет 1 — consistency check в AppendEntries
// follower обрабатывает AppendEntries
func (r *Raft) handleAppend(a AppendEntries) AppendReply {
if a.Term < r.currentTerm {
return AppendReply{Term: r.currentTerm, Success: false}
}
// БАГ ЗДЕСЬ: дописывает без проверки prevLog
r.log = append(r.log[:a.PrevLogIndex+1], a.Entries...)
r.fsync()
return AppendReply{Term: r.currentTerm, Success: true}
}
В этом обработчике follower'а нет одной проверки, на которой держится безопасность Raft. Какой именно и что ломается без неё?
Сниппет 2 — продвижение commit index
// лидер, после сбора matchIndex[] от follower-ов
func (r *Raft) advanceCommit() {
for n := r.commitIndex + 1; n <= r.lastIndex(); n++ {
count := 1 // считаем себя
for _, m := range r.matchIndex {
if m >= n {
count++
}
}
if count >= r.majority() {
r.commitIndex = n // закоммичено majority
}
}
}
Majority реплицировал запись n, поэтому код её коммитит. Но статья Raft добавляет ещё одно условие перед тем, как лидер может продвинуть commitIndex до n. Какое и почему?
Сниппет 3 — выдача голоса
// обработка RequestVote
func (r *Raft) handleVote(v RequestVote) VoteReply {
if v.Term < r.currentTerm {
return VoteReply{Term: r.currentTerm, Granted: false}
}
if r.votedFor == nil || r.votedFor == v.CandidateID {
r.votedFor = v.CandidateID
return VoteReply{Term: v.Term, Granted: true}
}
return VoteReply{Term: v.Term, Granted: false}
}
Этот обработчик голоса соблюдает один-голос-на-term, но пропускает проверку. Что теперь может сделать кандидат и какое свойство безопасности отказывает?
Сниппет 4 — реальный лог AppendEntries
15:42:08 INFO raft: AppendEntries -> D failure (mismatch at idx=400100 term=12 vs follower idx=400100 term=11)
15:42:09 INFO raft: AppendEntries -> D retry prevIdx=400050 (decremented)
15:42:09 INFO raft: AppendEntries -> D failure (mismatch at idx=400050 term=12 vs follower idx=400050 term=10)
15:42:10 INFO raft: AppendEntries -> D retry prevIdx=399000 (decremented)
15:42:10 INFO raft: AppendEntries -> D failure (mismatch at idx=399000 term=12 vs follower idx=399000 term=8)
15:42:11 WARN raft: D has diverged extensively; consider InstallSnapshot
Лидер раз за разом декрементирует nextIndex для follower'а D по одному шагу. Читая этот лог, какой диагноз и правильная операционная реакция?
Четыре обработчика несут безопасность Raft. AppendEntries обязан гейтить каждую вставку совпадением prevLogIndex/prevLogTerm, иначе логи тихо расходятся. Лидер вправе напрямую коммитить лишь запись своего текущего term — подсчёт реплик на записи прошлого term снова открывает перезапись figure-8. RequestVote обязан сравнивать lastLogTerm/lastLogIndex, иначе устаревший кандидат побеждает и ломает Leader Completeness. А длинная серия пошаговых декрементов nextIndex с падающими term’ами — сигнатура расхождения, означающая «переключайся на InstallSnapshot». Читай протокол там, где он живёт — в обработчиках и логах — и это ровно те строки, которые senior-инженер проверяет первыми.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.