Khi xây dựng các hệ thống lưu trữ phân tán hiện đại, việc duy trì sự đồng nhất dữ liệu giữa các node luôn là một thách thức lớn, và đó chính là lúc bài toán và thuật toán Raft thể hiện sức mạnh vượt trội của mình. Được giới thiệu bởi Diego Ongaro và John Ousterhout vào năm 2013 như một lựa chọn thay thế dễ hiểu hơn cho Paxos, Raft đã nhanh chóng trở thành tiêu chuẩn công nghiệp cho các hệ thống đòi hỏi tính sẵn sàng cao và sự đồng thuận tuyệt đối.
Tại sao chúng ta cần bài toán và thuật toán Raft?
Để hiểu rõ bài toán và thuật toán Raft, trước hết chúng ta cần nhìn nhận nó dưới lăng kính của bài toán đồng thuận (Consensus). Trong một cụm máy chủ (cluster), làm cách nào để các máy tính riêng biệt đồng ý với nhau về một giá trị duy nhất, ngay cả khi mạng bị chập chờn hoặc một số node bị hỏng?
Trước khi Raft ra đời, Paxos là thuật toán thống trị. Tuy nhiên, Paxos nổi tiếng là khó hiểu và cực kỳ phức tạp khi triển khai thực tế. Raft giải quyết vấn đề này bằng cách phân rã bài toán đồng thuận thành các bài toán con độc lập: Bầu chọn Leader (Leader Election), Sao chép Log (Log Replication), và Tính an toàn (Safety).
Trong thực tế, bài toán và thuật toán Raft vận hành dựa trên giả định về mô hình lỗi crash-recovery. Điều này có nghĩa là các node có thể ngừng hoạt động và sau đó khôi phục lại, nhưng chúng ta tin tưởng hoàn toàn vào mã nguồn đang chạy (không có lỗi Byzantine – nơi một node cố tình gửi dữ liệu sai lệch để phá hoại).
Kiến trúc cốt lõi của bài toán và thuật toán Raft
Kiến trúc của bài toán và thuật toán Raft được thiết kế theo hướng dễ hiểu (Understandability), chia nhỏ các quy trình phức tạp thành các module độc lập. Một hệ thống Raft thông thường sẽ bao gồm một số lẻ các node (thường là 3, 5 hoặc 7) để đảm bảo luôn có thể đạt được đa số (Quorum).
Architecture
Các trạng thái của Node
Mỗi node trong cụm luôn tồn tại ở một trong ba trạng thái duy nhất tại bất kỳ thời điểm nào:
- Follower (Người theo dõi): Trạng thái mặc định khi khởi động. Follower hoàn toàn thụ động, họ không tạo ra yêu cầu mà chỉ phản hồi các RPC (Remote Procedure Call) từ Leader hoặc Candidate. Nếu một Follower không nhận được tin nhắn từ Leader trong một khoảng thời gian (Election Timeout), họ sẽ tự chuyển mình thành Candidate.
- Candidate (Ứng viên): Trạng thái tạm thời để bầu chọn Leader mới. Candidate sẽ gửi yêu cầu bỏ phiếu cho các node khác để tìm kiếm sự ủng hộ.
- Leader (Lãnh đạo): Node duy nhất điều phối mọi giao dịch của cluster. Leader nhận request từ Client, ghi vào Log và chỉ đạo các Follower sao chép dữ liệu đó.
Khái niệm Term và Logical Clock
Trong bài toán và thuật toán Raft, khái niệm Term đóng vai trò như một đồng hồ logic (logical clock) giúp phân định các khoảng thời gian. Mỗi Term bắt đầu bằng một cuộc bầu chọn và kéo dài cho đến khi Leader của Term đó ngừng hoạt động hoặc bị mất kết nối.
Term
Số thứ tự của Term (Term Number) là một số nguyên tăng dần. Khi các node giao tiếp, chúng trao đổi Term hiện tại của mình. Nếu một node nhận thấy Term của mình nhỏ hơn của node khác, nó sẽ lập tức cập nhật Term của mình và chuyển về trạng thái Follower. Đây là cơ chế quan trọng để loại bỏ các Leader “hết thời” sau khi phục hồi từ lỗi mạng.
Cơ chế bầu chọn Leader trong bài toán và thuật toán Raft
Cơ chế bầu chọn Leader là xương sống của bài toán và thuật toán Raft, đảm bảo cluster luôn có một “nhạc trưởng” điều phối. Quy trình này bắt đầu khi nhịp tim (heartbeat) từ Leader hiện tại bị gián đoạn.
Quy trình RequestVote và Quorum
Khi một Follower trở thành Candidate, nó sẽ thực hiện các bước:
- Tăng giá trị
currentTermcủa bản thân. - Tự bỏ phiếu cho chính mình.
- Gửi RPC
RequestVotesong song đến tất cả các node khác.
Một Candidate sẽ trở thành Leader nếu nhận được phiếu bầu từ quá bán (N/2 + 1) các node trong cụm. Con số này đảm bảo rằng trong một Term, chỉ có tối đa một Leader được bầu chọn (Safety Property).
Kỹ thuật Randomized Election Timeout
Một lỗi phổ biến mà các lập trình viên thường mắc phải khi tự code thuật toán đồng thuận là để tất cả các node có cùng thời gian chờ timeout. Điều này dẫn đến tình trạng “Split Vote” – nhiều node cùng trở thành Candidate một lúc và không ai giành được đa số.
Raft giải quyết vấn đề này tuyệt vời bằng cách sử dụng Randomized Election Timeout. Mỗi node sẽ chọn ngẫu nhiên một khoảng thời gian chờ (ví dụ: 150ms đến 300ms). Node nào có thời gian chờ ngắn nhất sẽ hết hạn trước, trở thành Candidate và kêu gọi bầu chọn trước khi các node khác kịp phản ứng. Theo kinh nghiệm thực tế triển khai hệ thống etcd, kỹ thuật này cực kỳ hiệu quả trong việc giảm thiểu xung đột bầu chọn.
Hiện thực hóa bài toán và thuật toán Raft bằng ngôn ngữ Go
Khi chúng ta bắt tay vào cài đặt bài toán và thuật toán Raft, việc quản lý trạng thái máy (State Machine) là bước quan trọng nhất. Dưới đây là cấu trúc cơ bản của một node Raft triển khai bằng Go (phiên bản 1.19+), tập trung vào cấu trúc dữ liệu và logic bầu chọn.
package main import ( "fmt" "math/rand" "sync" "time" ) // Định nghĩa các trạng thái của Node type NodeState int const ( Follower NodeState = iota Candidate Leader ) // RaftNode đại diện cho một instance trong cluster type RaftNode struct { mu sync.Mutex id int peers []int state NodeState currentTerm int votedFor int // ID của node mà node này đã bỏ phiếu trong term hiện tại // Timer để quản lý bầu chọn electionTimer time.Timer } // RequestVoteArgs là tham số cho RPC RequestVote type RequestVoteArgs struct { Term int CandidateID int LastLogIndex int LastLogTerm int } // RequestVoteReply là phản hồi từ RPC RequestVote type RequestVoteReply struct { Term int VoteGranted bool } // startElection bắt đầu quy trình bầu chọn func (rf RaftNode) startElection() { rf.mu.Lock() rf.state = Candidate rf.currentTerm++ rf.votedFor = rf.id fmt.Printf("Node %d starts election for Term %dn", rf.id, rf.currentTerm) term := rf.currentTerm votesReceived := 1 rf.mu.Unlock() for _, peerID := range rf.peers { if peerID == rf.id { continue } // Trong thực tế, đây sẽ là một thủ tục gọi qua network (RPC) go func(id int) { reply := rf.sendRequestVote(id, RequestVoteArgs{Term: term, CandidateID: rf.id}) if reply.VoteGranted { rf.mu.Lock() votesReceived++ // Kiểm tra nếu đạt được Quorum (ví dụ cluster 3 node, cần 2 phiếu) if votesReceived > len(rf.peers)/2 && rf.state == Candidate { rf.becomeLeader() } rf.mu.Unlock() } }(peerID) } } func (rf RaftNode) becomeLeader() { rf.state = Leader fmt.Printf("Node %d became Leader for Term %dn", rf.id, rf.currentTerm) // Bắt đầu gửi Heartbeats (AppendEntries với log trống) } // Giả lập gửi RPC func (rf RaftNode) sendRequestVote(peerID int, args RequestVoteArgs) RequestVoteReply { // Giả lập độ trễ mạng và logic phản hồi time.Sleep(time.Duration(rand.Intn(50)) time.Millisecond) return RequestVoteReply{Term: args.Term, VoteGranted: true} } func main() { rand.Seed(time.Now().UnixNano()) node := &RaftNode{ id: 1, peers: []int{1, 2, 3}, state: Follower, } node.startElection() time.Sleep(1 time.Second) }
Lưu ý: Đoạn code trên là bản giản lược tập trung vào logic chuyển đổi trạng thái. Trong production, bạn cần xử lý thêm việc persist dữ liệu xuống đĩa cứng để phục hồi sau khi crash (đặc biệt là currentTerm và votedFor).
Phân tích độ phức tạp và các trường hợp biên
Trong quá trình phân tích thuật toán, xét về mặt hiệu năng, bài toán và thuật toán Raft tối ưu hóa số lượng thông điệp trao đổi để đạt được sự đồng thuận nhanh nhất.
- Độ phức tạp về thời gian (Time Complexity): Trong điều kiện mạng ổn định, một Leader mới có thể được bầu chọn trong vòng chưa đầy một giây (tùy thuộc vào nhịp tim và timeout cấu hình). Thời gian hội tụ log tỷ lệ thuận với số lượng Follower nhưng diễn ra song song nên độ trễ là $O(1)$ RPC round-trips.
- Độ phức tạp về không gian (Space Complexity): Mỗi node cần lưu trữ toàn bộ Log. Tuy nhiên, Raft giới thiệu cơ chế Snapshot để giải quyết vấn đề log phình to, giới hạn không gian lưu trữ ở mức $O(DataSize)$ thay vì $O(HistorySize)$.
Các Edge Case cần lưu ý
- Mạng bị phân mảnh (Network Partition): Giả sử cụm 5 node bị chia cắt thành 2 cụm nhỏ (2 và 3 node). Cụm 3 node sẽ bầu được Leader mới vì đạt Quorum. Cụm 2 node sẽ liên tục bầu chọn nhưng thất bại. Khi mạng kết nối lại, Leader ở cụm 2 (nếu có) sẽ thấy Term của mình thấp hơn và phải từ bỏ quyền lực ngay lập tức.
- Lỗi khởi động nhanh (Fast-restart): Nếu một node crash và restart quá nhanh mà không kịp lưu lại việc mình đã bầu cho ai, nó có thể bầu cho hai người khác nhau trong cùng một Term. Đây là lý do tại sao việc ghi file (fsync) trước khi phản hồi RPC là bắt buộc.
Ứng dụng thực tế của bài toán và thuật toán Raft
Các hệ thống như etcd hay Consul đã chứng minh tính ổn định tuyệt vời của bài toán và thuật toán Raft trong môi trường production thực tế.
- Etcd: Đây là “bộ não” của Kubernetes, lưu trữ toàn bộ cấu hình cụm và trạng thái vận hành. Một lỗi nhỏ trong thuật toán đồng thuận của etcd có thể làm sụp đổ toàn bộ hệ thống cloud của một doanh nghiệp.
- TiKV: Một database phân tán hỗ trợ giao dịch, sử dụng Raft để đảm bảo dữ liệu không bao giờ bị mất (Zero Data Loss) và luôn nhất quán (Strong Consistency).
- CockroachDB: Database SQL phân tán cực kỳ mạnh mẽ cũng sử dụng mô hình bầu chọn và replication dựa trên nguyên lý của Raft để quản lý các range dữ liệu.
Một trong những tình huống oái oăm nhất khi vận hành bài toán và thuật toán Raft là hiện tượng chia cắt mạng (Network Partition). Khi đó, một kịch bản “Stale Read” có thể xảy ra: Client đọc dữ liệu từ một cựu Leader nằm ở phân mảnh mạng nhỏ. Để khắc phục, Raft yêu cầu Leader phải xác nhận được nhịp tim với đa số node trước khi trả lời kết quả đọc, hoặc sử dụng cơ chế “Lease”.
Hy vọng qua bài viết này, bạn đã nắm vững cách thức vận hành của bài toán và thuật toán Raft để áp dụng vào các dự án của mình. Việc hiểu sâu về đồng thuận không chỉ giúp bạn trở thành một lập trình viên senior mà còn giúp bạn thiết kế được những hệ thống phân tán có khả năng tự phục hồi và chịu lỗi cực cao. Bước tiếp theo, bạn hãy thử tìm hiểu về cơ chế Sao chép Log (Log Replication) để thấy được cách dữ liệu thực sự được nhân bản xuyên suốt mạng lưới như thế nào.
Tham khảo:
- Ongaro, D., & Ousterhout, J. (2014). In Search of an Understandable Consensus Algorithm. USENIX Annual Technical Conference.
- Official Raft Documentation: https://raft.github.io/
Cập nhật lần cuối 02/03/2026 by Hiếu IT
