Hai phần trước dùng etcd như một dịch vụ khoá và bầu chủ đáng tin. Phần này hỏi: đáng tin tới đâu, và vì sao? Câu trả lời là Raft — thuật toán đồng thuận mà etcd dùng để ba node hành xử như một — và cách kiểm tra câu trả lời đó không phải đọc bài báo, mà là dừng từng node và đo. Bài này dừng một follower, giết leader giữa lúc ghi, dừng hai trong ba node, rồi bật lại, và ghi số cho từng bước.

Công cụ đo và cách điều phối

Ảnh chụp đoạn mã nền tối Raft trong etcd quorum là thứ được đo không phải được tin với cụm etcd 3.5.16 ba node etcd1 tới etcd3 trên mạng Docker bench Go dùng etcd client v3 và docker stop kill từ shell, công cụ đo bốn chế độ một client cầm cả ba endpoint gồm clientv3.New với Endpoints ba địa chỉ, chế độ write 300 lần Put timeout 1 giây mỗi lần đếm thành công p50 p99, chế độ read 300 lần Get linearizable mặc định qua leader xác nhận quorum và 300 lần Get serializable WithSerializable trả bản tại node đang nối không hỏi ai, chế độ cont ghi liên tục N giây timeout 500 ms in mốc chuyển OK và LỖI để đo cửa sổ không ghi được, chế độ until lặp Put tới khi thành công để đo thời gian khôi phục, điều phối từ shell tìm leader thật rồi dừng đúng node gồm hàm leader dùng etcdctl endpoint status awk cột 5 bằng true, docker stop FOLLOWER tức 2 trên 3 còn sống quorum còn, bench cont 8 giây chạy nền rồi sleep 2 rồi docker kill LEADER rồi wait để giết leader giữa chừng đo cửa sổ, docker stop hai node tức 1 trên 3 mất quorum ghi và đọc linearizable phải thất bại, docker start hai node đo bao lâu ghi lại được, vì sao 2 trên 3 đủ mà 1 trên 3 không một bản ghi chỉ commit khi đa số đã ghi xuống đĩa gồm client gửi Put tới leader, leader gửi AppendEntries song song tới follower1 follower2, leader chờ đa số kể cả mình fsync xong rồi commit rồi trả client với 3 node cần 2 và 5 node cần 3, còn 1 node không bao giờ đủ 2 nên không commit được gì còn đọc tại node vẫn trả bản cuối nó có

Hình 1: Bench Go bốn chế độ, điều phối bằng shell, và sơ đồ một lần ghi trong Raft — commit chỉ xảy ra khi đa số đã fsync.

Raft trong một đoạn: cụm có một leader, mọi ghi đi qua nó. Leader gắn bản ghi vào log của mình, gửi cho các follower, và chỉ commit — rồi mới trả lời client — khi đa số node (kể cả nó) đã ghi bản đó xuống đĩa bền. Ba node thì đa số là hai; năm node là ba. Khi leader biến mất, các follower chờ hết election timeout (etcd mặc định 1 giây) rồi tự ứng cử, và node nào gom được đa số phiếu thành leader mới. Mọi con số bên dưới là hệ quả trực tiếp của hai quy tắc "đa số" đó.

Đọc có hai kiểu: linearizable (mặc định) đi qua leader và xác nhận leader vẫn được đa số công nhận trước khi trả — nên bạn không bao giờ đọc được bản cũ; serializable trả thẳng bản node đang nối có — rẻ hơn, nhưng node đó có thể đang tụt lại.

Đo thật: sáu bước

Ảnh chụp output thật nền tối giết leader 1,07 giây không ghi được mất 2 trên 3 là 0 trên 20 với etcd 3.5.16 ba node Go 1.23 etcd client v3 loopback Docker đĩa ảo của VM, mục 1 cả 3 node sống ghi 300 trên 300 p50 0.38 ms p99 0.82 ms đọc linearizable 300 trên 300 p50 0.22 ms p99 0.44 ms qua leader xác nhận quorum đọc serializable 300 trên 300 p50 0.08 ms p99 0.28 ms tại node rẻ gần 3 lần, mục 2 dừng một follower etcd1 còn 2 trên 3 ghi 300 trên 300 p50 0.29 ms p99 0.66 ms không chậm đi vì đa số bằng 2 vẫn đủ đọc linearizable 300 trên 300 p50 0.25 ms đọc serializable 300 trên 300 p50 0.09 ms, mục 3 giết leader etcd2 ở giây thứ 2 ghi liên tục 8 giây timeout 500 ms cho thấy t 2.01 giây ghi lỗi error reading from server kết nối tới leader đứt, t 3.08 giây ghi OK trở lại cửa sổ không ghi được 1.07 giây tức election timeout mặc định 1 giây cộng bầu cộng nối lại, leader mới etcd1, mục 4 dừng hai node còn etcd1 một mình mất quorum ghi 0 trên 20 timeout 1 giây mỗi lần không đa số không commit, đọc linearizable 0 trên 20 cần xác nhận quorum không có, đọc serializable 20 trên 20 p50 0.21 ms vẫn trả bản cuối node này có, mục 5 khởi động lại hai node ghi được trở lại sau 0.45 giây leader sau khôi phục etcd2, mục 6 cái giá của fsync và của đồng thuận 300 lần ghi gồm 1 node fsync p50 0.15 ms p99 0.51 ms, 1 node không fsync p50 0.09 ms p99 0.31 ms đĩa ảo VM có write cache fsync rẻ bất thường, 3 node fsync p50 0.37 ms p99 1.08 ms đồng thuận thêm 1 lượt mạng cộng fsync ở 2 máy, nói thẳng mất 1 trên 3 không ảnh hưởng gì mất leader là khoảng 1 giây tối mất 2 trên 3 là hệ thống chỉ còn đọc được bản cũ và con số fsync ở đây là của đĩa ảo trên SSD thật là 0,5 đến 2 ms HDD khoảng 10 ms nhân lên theo từng lần commit

Hình 2: Kết quả thật qua sáu bước. Mỗi bước là một câu hỏi "chuyện gì xảy ra nếu" được trả lời bằng số thay vì bằng sơ đồ.

trạng thái cụm ghi đọc linearizable đọc serializable
3/3 sống 300/300, p50 0,38 ms 300/300, 0,22 ms 300/300, 0,08 ms
2/3 (mất follower) 300/300, 0,29 ms 300/300 300/300
giết leader 1,07 s không ghi được, rồi tự hồi — —
1/3 (mất hai node) 0/20 0/20 20/20
bật lại hai node ghi được sau 0,45 s — —

Mất một follower: không ảnh hưởng gì. 300/300 lần ghi thành công, p50 còn hơi nhanh hơn (0,29 so với 0,38 ms) vì leader chỉ cần chờ một follower thay vì chọn nhanh nhất trong hai. Đây là lời hứa cốt lõi của Raft ba node: một máy chết là chuyện bình thường, không phải sự cố. Và cũng là lý do cụm thường có số lẻ: bốn node vẫn chỉ chịu được một node chết (đa số là ba), tốn thêm một máy mà không thêm khả năng chịu lỗi.

Giết leader: 1,07 giây tối. Lúc 2,01 s kết nối tới leader đứt và lần ghi đầu tiên lỗi. Các follower chờ election timeout (1 giây mặc định) để chắc leader thật sự im lặng chứ không chỉ chậm — chính câu hỏi "chết hay chậm" của phần 3 — rồi bầu, etcd1 thắng, client nối lại, và lúc 3,08 s ghi OK. Cửa sổ 1,07 s gần như toàn bộ là election timeout; chỉnh nó xuống 200 ms là cửa sổ ngắn đi năm lần, với giá là mạng chậm 200 ms cũng gây bầu lại oan.

Mất hai node: không ghi được gì, và không đọc linearizable được gì. Node sót lại không thể gom đa số nên không commit — 0/20 lần ghi, mỗi lần chờ hết 1 giây timeout. Đọc linearizable cũng 0/20, vì nó cần leader xác nhận với đa số rằng mình còn là leader. Nhưng đọc serializable 20/20: node sót lại trả bản cuối cùng nó có, không hỏi ai. Đây là cách etcd — và mọi hệ Raft — hành xử khi mất quorum: từ chối ghi và từ chối hứa, nhưng vẫn cho đọc nếu bạn chấp nhận có thể cũ. Không có bản ghi nào bị mất hay bị tách đôi; hệ thống chọn dừng thay vì chọn sai. Phần 12 sẽ cắt mạng thật thay vì dừng tiến trình để xem lời hứa đó có giữ khi cả hai phía đều sống.

Bật lại: 0,45 giây. Hai node khởi động, bắt kịp log từ node sót lại (hoặc ngược lại), bầu leader, nhận ghi. Nhanh hơn giết leader vì không phải chờ election timeout để phát hiện — node mới lên biết ngay là mình cần bầu.

Cái giá của đồng thuận. Một node etcd đơn ghi ở 0,15 ms; ba node ở 0,37 ms — thêm một lượt mạng tới follower và một fsync ở máy khác, nhân 2,5 lần. Tắt fsync trên node đơn chỉ đưa 0,15 xuống 0,09 ms: đĩa ảo của máy ảo Docker có write cache nên fsync gần như miễn phí ở đây. Trên SSD thật fsync là 0,5–2 ms, HDD khoảng 10 ms, và nó nằm trên đường găng của mỗi commit — đó là lý do etcd khuyến nghị SSD và cảnh báo khi fsync vượt 10 ms.

Đánh đổi cần cân nhắc

Ba hay năm node. Ba chịu được một node chết; năm chịu được hai, nhưng mỗi commit phải chờ ba fsync thay vì hai và leader phải gửi cho bốn follower. etcd khuyến nghị tối đa năm; bảy thì chậm hơn mà hiếm khi cần. Và số node chẵn là lãng phí thuần tuý.

Election timeout là nút chỉnh duy nhất cho "tối bao lâu". Mặc định 1 s (heartbeat 100 ms) hợp với mạng trong một trung tâm dữ liệu. Cụm trải nhiều vùng cần tăng lên (etcd gợi ý 5 s cho liên lục địa) — tức mất leader là 5 giây không ghi — vì hạ xuống là RTT cao kích hoạt bầu lại liên tục, và mỗi lần bầu là một lần tối.

Serializable read là đánh đổi có ý thức, không phải tối ưu hoá miễn phí. Rẻ gần ba lần và sống sót khi mất quorum, nhưng mục 4 cho thấy nó trả lời kể cả khi không ai dám hứa đó là bản mới nhất. Dùng cho dashboard, cache, cấu hình đọc nhiều; không dùng để quyết định "khoá còn của tôi không" — chuyện đó phải là linearizable, hoặc tốt hơn là Txn của phần 4.

Raft hứa an toàn, không hứa sẵn sàng. Mọi con số 0/20 ở mục 4 là Raft làm đúng việc: thà không phục vụ còn hơn phục vụ sai. Nếu ứng dụng của bạn cần "luôn ghi được, kể cả khi tách mạng" thì Raft là công cụ sai — phần 9 về CRDT là phía bên kia của đánh đổi đó.

Loopback, đĩa ảo, một máy. 0,38 ms ghi là trong mạng Docker trên cùng host; qua mạng thật là RTT cộng fsync thật, thường 2–10 ms. Các tỉ lệ (mất follower không chậm, mất leader tối ~election timeout, mất quorum = 0) không đổi.

Ba ý mang về

  1. Raft ba node coi một node chết là chuyện thường: mất follower vẫn 300/300 và không chậm đi; mất leader là đúng một election timeout tối (1,07 s) rồi tự hồi. Số node lẻ, và nút chỉnh "tối bao lâu" chính là election timeout.
  2. Mất quorum là dừng, không phải sai: 0/20 ghi, 0/20 đọc linearizable, nhưng 20/20 đọc serializable — hệ thống từ chối hứa thay vì hứa bừa, và vẫn cho đọc nếu bạn chấp nhận bản có thể cũ.
  3. Đồng thuận có giá bằng một lượt mạng và fsync ở đa số máy: 0,37 ms so với 0,15 ms ở đây, và fsync trên đĩa thật là 0,5–10 ms trên đường găng của mọi commit — SSD không phải tuỳ chọn cho etcd.

Nguồn

Phần sau rời đồng thuận để sang sao chép kiểu phổ biến hơn trong backend: PostgreSQL primary–replica. Không quorum, không bầu — chỉ một primary ghi và các replica chép theo, và câu hỏi là chép theo chậm bao nhiêu, đo bằng mili-giây, và chuyện gì xảy ra khi bạn đọc từ replica ngay sau khi ghi vào primary.