
sendAppendEntries()可以理解為針對某一個 Follower執行一次AppendEntries RPC然后把回復合并回 Leader 的 Raft 狀態。它不負責構造請求。請求由doHeartBeat()提前構造它只負責發送 RPC ↓ 等待結果 ↓ 檢查任期和 Leader 身份 ↓ 失敗回退 nextIndex 成功推進 matchIndex、nextIndex ↓ 嘗試推進 commitIndex源碼實現可見于該項目的raft.cpp。函數參數函數大致接收四個參數void Raft::sendAppendEntries( int server, std::shared_ptrAppendEntriesArgs args, std::shared_ptrAppendEntriesReply reply, std::shared_ptrint appendNums);含義分別是server 目標 Follower 的編號。 args doHeartBeat() 構造好的 AppendEntries 請求。 包含 term、prevLogIndex、prevLogTerm、entries、leaderCommit。 reply 保存 Follower 返回的響應。 appendNums 本輪心跳中共享的“成功節點數量”。 doHeartBeat() 創建它時通常初始化為 1代表 Leader 自己。使用shared_ptr是因為函數運行在detach()出去的線程中。doHeartBeat()返回后請求、回復和計數器仍必須存活。一、發送 RPC核心調用類似bool ok m_peers[server]-AppendEntries( args.get(), reply.get() );這里值得注意的是執行網絡 RPC 時沒有持有m_mtx。這是正確的鎖邊界設計。網絡請求可能超時或阻塞如果在 RPC 期間一直持有 Raft 主鎖當前節點將無法及時處理其他節點發來的 RPC接收更高任期處理客戶端請求更新選舉和心跳狀態。因此它采用無鎖執行網絡調用 ↓ RPC 返回 ↓ 加鎖處理回復但這也意味著 RPC 飛行期間當前節點的任期和身份可能發生變化所以后面必須重新校驗。二、網絡失敗直接返回if (!ok) { return; }ok false通常表示連接失敗、超時或者底層 RPC 沒有成功完成。這種情況下函數不會修改 nextIndex 修改 matchIndex 修改 currentTerm 增加 appendNums它也不會立即重試。后續的周期性doHeartBeat()會再次嘗試。這是合理的因為網絡失敗不代表日志不匹配不能因為一次超時就隨意回退nextIndex。三、重新獲得 Raft 鎖std::lock_guardstd::mutex lock(m_mtx);從這里開始回復處理期間的共享狀態修改都受同一把鎖保護包括m_currentTerm m_status m_votedFor m_nextIndex m_matchIndex m_commitIndex appendNums因此appendNums雖然只是普通int而不是原子變量但它的讀寫發生在m_mtx內當前實現下不會因為多個回復線程同時執行而產生直接的數據競爭。四、處理更大的任期if (reply-term() m_currentTerm) { m_status Follower; m_currentTerm reply-term(); m_votedFor -1; return; }這是 Raft 非常重要的一條規則任何節點只要發現其他節點的任期比自己大就必須更新任期并退回 Follower。例如當前節點認為 自己是 term6 的 Leader Follower 回復 term7這說明 term 6 已經過期集群至少已經進入 term 7。當前節點不能再繼續發送 term 6 的日志必須立即退位。這里同時清空m_votedFor -1;表示新任期中還沒有投票。不過這份實現此處分支沒有明顯調用persist()。由于currentTerm和votedFor屬于 Raft 的持久化狀態更嚴格的實現應該在釋放鎖或返回之前將它們持久化否則節點崩潰重啟后可能恢復出舊任期。五、丟棄較小任期的回復else if (reply-term() m_currentTerm) { return; }例如發送請求時 currentTerm 6 等待 RPC 期間 當前節點進入 term 7并且重新成為 Leader 舊請求返回 reply.term 6這個回復屬于過去的任期不能再修改當前狀態所以直接丟棄。隨后通常還有斷言assert(reply-term() m_currentTerm);經過前面兩個分支后繼續執行的回復原則上必須與當前任期一致。六、再次確認自己仍然是 Leaderif (m_status ! Leader) { return; }即使回復的任期等于m_currentTerm也不能說明當前節點仍然是 Leader。RPC 飛行期間可能發生Leader 發送 AppendEntries ↓ 收到合法的同任期 Leader 消息或狀態發生變化 ↓ 當前節點轉為 Follower ↓ 舊 AppendEntries 回復返回此時不能再更新 Leader 專屬的nextIndex[] matchIndex[] commitIndex所以需要獨立檢查m_status。七、處理日志匹配失敗if (!reply-success()) { if (reply-updatenextindex() ! -100) { m_nextIndex[server] reply-updatenextindex(); } }success false一般說明Follower 不存在 prevLogIndex或者Follower 在 prevLogIndex 位置的 term 和 Leader 給出的 prevLogTerm 不同Follower 會通過updateNextIndex告訴 Leader下次應該從哪里嘗試。例如Leader nextIndex[F] 8 本次發送 prevLogIndex 7 Follower 實際只有日志 14Follower 可以回復success false updateNextIndex 5Leader于是執行m_nextIndex[F] 5;下一輪請求變成prevLogIndex 4 entries [5, 6, 7, ...]這就是 Raft 的日志回退過程。-100是這份代碼使用的特殊哨兵值表示 Follower 沒有提供可用的新下標。工程上更清晰的方式是使用 Protobuf 的字段存在性或明確的狀態枚舉而不是魔法數字。八、成功時更新復制進度成功分支首先增加本輪成功數*appendNums *appendNums 1;然后計算這次請求能夠確認的最大日志下標int replicatedIndex args-prevlogindex() args-entries_size();例如prevLogIndex 5 entries [6, 7, 8] entries_size 3那么replicatedIndex 5 3 8意味著 Follower 已經確認擁有截至日志 8 的完整前綴。為什么更新matchIndex要用max代碼類似m_matchIndex[server] std::max( m_matchIndex[server], args-prevlogindex() args-entries_size() );原因是網絡回復可能亂序。假設同時存在兩個請求請求 A確認到日志 8 請求 B確認到日志 12如果 B 先回來matchIndex 12隨后舊請求 A 才回來。如果直接賦值就會錯誤地變成matchIndex 8使用max可以保證matchIndex 只能前進不能后退這是成功回復處理里做得比較穩妥的地方。更新nextIndexm_nextIndex[server] m_matchIndex[server] 1;兩個字段的關系是matchIndex[i] 已確認 Follower i 擁有的最后日志下標 nextIndex[i] 下一次應從哪個下標繼續發送如果已經確認 Follower 擁有到日志 8matchIndex 8 nextIndex 9下一次心跳如果 Leader 沒有新日志就會發送prevLogIndex 8 entries 空這就是純心跳。九、嘗試推進commitIndex當前實現使用if (*appendNums 1 m_peers.size() / 2) { *appendNums 0; if (args-entries_size() 0) { m_commitIndex std::max(m_commitIndex, m_matchIndex[server]); } }假設有 5 個節點多數派數量 1 5 / 2 3appendNums初始是 1因為 Leader 自己已經有日志。收到兩個 Follower 的成功回復后appendNums 3于是代碼認為獲得多數派可以推進提交位置。設置成0是為了避免本輪后續回復再次觸發提交邏輯。這里存在一個重要正確性問題appendNums只統計“RPC 成功了幾個”但不同 Follower 成功確認的日志位置可能不同。例如 5 節點集群Leader擁有日志到 10 Follower A成功確認到 5 Follower B成功確認到 10成功數量是Leader A B 3已經過半如果 B 的回復正好讓appendNums達到 3當前代碼可能執行commitIndex 10但日志 10 實際只有Leader Follower B只有兩個節點并沒有過半。Follower A 只擁有到日志 5。正確算法應該針對每個候選下標N統計有多少節點滿足 matchIndex[i] N只有滿足多數節點的 matchIndex N 并且 log[N].term currentTerm才能把commitIndex推進到N。這也是 Raft 論文描述的 Leader 提交規則。(usenix.org)該項目其實已經存在類似的leaderUpdateCommitIndex()回復成功后調用它會比appendNums更符合 Raft 語義。