Skip to content

Distributed Consensus — Đồng thuận phân tán

Tháng 3 năm 2020, một cluster etcd 5 node phục vụ hạ tầng Kubernetes của một sàn thương mại điện tử lớn mất leader đúng lúc flash sale. Hệ thống mất khả năng ghi trong 47 giây — đủ lâu để hàng nghìn đơn hàng rơi vào trạng thái không xác định. Nguyên nhân gốc: operator cấu hình election timeout quá ngắn so với network latency thực tế, khiến cluster liên tục bầu leader mới mà không node nào giữ ghế đủ lâu để phục vụ request.

Consensus — bài toán để một nhóm node đồng ý về cùng một giá trị dù có node lỗi hoặc mạng gián đoạn — là xương sống của mọi hệ phân tán đáng tin cậy. Không có consensus, bạn không có replicated state machine, không có distributed lock, không có leader election, và chắc chắn không có strongly consistent database.

Raft ra đời năm 2014 với triết lý: understandability is a feature. Trong khi Paxos đúng nhưng khét tiếng khó hiểu, Raft cố tình chia bài toán thành ba sub-problem độc lập — leader election, log replication, safety — để engineer có thể implement đúng mà không cần bằng tiến sĩ.

Bức tranh tư duy

Hình dung một hội đồng thành phố gồm 5 thành viên cần biểu quyết ngân sách. Quy tắc: bất kỳ quyết định nào cũng cần quá bán — tức 3 phiếu đồng ý. Nếu 2 thành viên nghỉ ốm, 3 người còn lại vẫn đủ quorum để quyết. Nhưng nếu 3 người mất liên lạc? Hội đồng tê liệt — không ai dám quyết vì không biết phía bên kia đã quyết gì.

Đây chính xác là cách quorum hoạt động trong distributed consensus.

Leader nhận ACK từ Node 2 và Node 3 → cộng với chính nó = 3 phiếu → đạt quorum → entry được commit dù Node 4, 5 chưa phản hồi.

Phép tương đồng này đúng với crash fault — node hỏng thì im lặng. Nhưng nó không cover Byzantine fault — trường hợp node "nói dối", gửi thông tin sai lệch có chủ đích. Đó là lý do blockchain cần thuật toán riêng (PBFT, Tendermint) với chi phí cao hơn nhiều.

Cốt lõi kỹ thuật

FLP Impossibility

Năm 1985, Fischer, Lynch và Paterson chứng minh một kết quả gây sốc: không tồn tại thuật toán consensus deterministic nào đảm bảo termination trong hệ thống asynchronous nếu dù chỉ một process có thể crash. Đây là FLP Impossibility theorem.

Nghe có vẻ bi quan, nhưng thực tế các hệ thống production work around bằng hai cách:

Randomization — thêm yếu tố ngẫu nhiên (random timeout trong Raft) để phá vỡ symmetry, khiến thuật toán hội tụ "hầu như chắc chắn" dù không đảm bảo 100% trong mọi trường hợp.

Partial synchrony — giả định mạng "cuối cùng sẽ ổn định" sau một khoảng thời gian. Raft và Paxos đều hoạt động dưới mô hình này: khi mạng ổn định đủ lâu, leader sẽ được bầu và log sẽ được replicate.

FLP không nói rằng consensus bất khả thi — nó nói rằng bạn phải trade-off: chấp nhận xác suất (không deterministic hoàn toàn) hoặc giả định về timing.

Paxos

Leslie Lamport công bố Paxos năm 1989 (xuất bản 1998), dùng câu chuyện hội đồng đảo Paxos làm phép ẩn dụ. Thuật toán gồm ba vai trò:

Proposer — đề xuất giá trị, gửi request tới các acceptor.

Acceptor — biểu quyết chấp nhận hoặc từ chối proposal. Cần quá bán acceptor đồng ý.

Learner — quan sát kết quả và áp dụng giá trị đã được chọn.

Quy trình Paxos cơ bản gồm hai pha:

Phase 1 (Prepare): Proposer chọn proposal number n, gửi Prepare(n) tới tất cả acceptor. Mỗi acceptor hứa không chấp nhận proposal nào có number nhỏ hơn n.

Phase 2 (Accept): Nếu proposer nhận đủ promise (quorum), nó gửi Accept(n, value). Acceptor chấp nhận nếu chưa promise cho number cao hơn.

Vấn đề thực tiễn của Paxos: protocol chỉ đồng ý một giá trị duy nhất. Để xây replicated log (chuỗi các giá trị), cần Multi-Paxos — và Lamport không mô tả chi tiết phần này, dẫn đến mỗi implementation diễn giải khác nhau. Google Chubby, một implementation nổi tiếng, mất nhiều năm để ổn định.

Raft

Diego Ongaro và John Ousterhout thiết kế Raft với mục tiêu duy nhất: dễ hiểu hơn Paxos mà vẫn đúng. Raft chia consensus thành ba bài toán con:

Leader Election

Mỗi node ở một trong ba trạng thái: Follower, Candidate, Leader. Thời gian được chia thành term — mỗi term bắt đầu bằng một cuộc bầu cử.

Khi follower không nhận heartbeat trong khoảng election timeout (randomized 150-300ms), nó chuyển thành candidate, tăng term, vote cho chính mình, và gửi RequestVote RPC tới các node khác. Node nhận request sẽ vote cho candidate nếu: (a) chưa vote trong term này, và (b) log của candidate ít nhất "up-to-date" bằng log của mình.

Random election timeout là chìa khóa: nó giảm xác suất hai node cùng thành candidate — giải quyết split vote mà không cần thuật toán phức tạp.

Log Replication

Sau khi trở thành leader, node xử lý mọi client request bằng cách append entry vào log, rồi gửi AppendEntries RPC tới tất cả follower. Khi đa số follower xác nhận (ACK), entry được coi là committed và leader apply nó vào state machine.

Safety guarantee: Raft đảm bảo nếu một entry đã committed ở term T, mọi leader tương lai đều có entry đó trong log. Điều này được enforce qua Election Restriction — candidate phải có log ít nhất bằng voter để nhận phiếu.

Term Number

Term hoạt động như logical clock. Mỗi RPC mang theo term number hiện tại. Nếu node nhận RPC với term cao hơn, nó lập tức chuyển về follower. Nếu nhận RPC với term thấp hơn, nó reject. Cơ chế này đảm bảo at most one leader per term.

Byzantine Fault Tolerance

Paxos và Raft chỉ chịu được crash fault — node hỏng thì dừng hoạt động. Byzantine fault xảy ra khi node hành xử tùy ý: gửi dữ liệu sai, giả mạo message, hoặc cố tình phá hoại.

Practical Byzantine Fault Tolerance (PBFT), đề xuất bởi Castro và Liskov năm 1999, cần 3f + 1 node để chịu f node Byzantine (so với 2f + 1 cho crash fault). Chi phí: O(n²) message complexity cho mỗi consensus round.

Khi nào cần BFT? Khi các node không tin tưởng lẫn nhau — blockchain, multi-party computation, hệ thống liên ngân hàng. Trong hệ thống nội bộ một tổ chức, crash fault tolerance (Raft/Paxos) là đủ vì bạn kiểm soát tất cả node.

So sánh các hệ thống thực tế

Thuộc tínhPaxosRaftZAB (ZooKeeper)
Dễ hiểuThấp — paper gốc nổi tiếng khó đọcCao — thiết kế ưu tiên understandabilityTrung bình
Leader bắt buộcKhông (basic Paxos) / Có (Multi-Paxos)Có — mọi write qua leaderCó — leader xử lý mọi transaction
Độ khó implementRất cao — nhiều edge case không rõ ràngTrung bình — spec chi tiết, nhiều reference implCao — protocol phức tạp
Hệ thống nổi bậtGoogle Chubby, Spanneretcd, CockroachDB, TiKV, ConsulApache ZooKeeper, Kafka (metadata)
Membership changeKhó — cần protocol riêngJoint consensus (tích hợp sẵn)Dynamic reconfiguration từ 3.5+
Read consistencyTùy implementationLinearizable (qua leader) hoặc lease-basedLinearizable write, sequential read

Thực chiến

Leader Election trong Microservices

Trong kiến trúc microservices, nhiều service cần singleton behavior — chỉ một instance chạy cron job, chỉ một instance là primary cho partition nào đó. Leader election qua consensus là cách đáng tin cậy nhất.

Scenario: Bạn có 3 instance của Scheduler Service. Chỉ instance nào giữ lease trên etcd mới được trigger job.

Lease TTL là thông số quan trọng nhất:

  • TTL quá ngắn (2-3s): leader liên tục mất lease do network jitter → service flapping.
  • TTL quá dài (60s+): khi leader thực sự crash, phải chờ lâu mới failover → downtime kéo dài.
  • Khuyến nghị: TTL 10-15s, keepalive interval = TTL/3. Điều chỉnh theo network latency thực tế.

Lưu ý: leader election qua etcd không phải fencing — sau khi leader cũ mất lease, nó có thể vẫn đang xử lý request (process chưa biết mình mất lease). Cần fencing token hoặc check lease validity trước mỗi thao tác quan trọng.

Distributed Locks với Consul

Consul cung cấp session-based locking — client tạo session gắn với health check, rồi acquire lock trên KV key.

# Tạo session
PUT /v1/session/create
{
  "Name": "deploy-lock",
  "TTL": "15s",
  "LockDelay": "5s",
  "Behavior": "release"
}

# Acquire lock
PUT /v1/kv/locks/deploy?acquire=<session-id>

# Release lock
PUT /v1/kv/locks/deploy?release=<session-id>

LockDelay là tính năng quan trọng: khi session invalidate (node crash), Consul chờ thêm LockDelay trước khi cho phép session khác acquire. Điều này tránh trường hợp hai client cùng nghĩ mình giữ lock — client cũ có thời gian hoàn tất hoặc rollback thao tác đang dở.

So sánh với Redlock: Martin Kleppmann đã phân tích kỹ vì sao Redlock (distributed lock trên Redis) không an toàn khi client bị GC pause hoặc clock drift. Lock dựa trên consensus (Consul, etcd) mạnh hơn vì protocol đảm bảo linearizability — nhưng đổi lại latency cao hơn Redis. Nếu correctness quan trọng hơn throughput (tài chính, inventory), chọn consensus-based lock. Nếu chấp nhận occasional double-processing (analytics, cache warm), Redlock có thể đủ.

Sai lầm điển hình

1. Giả định mạng đáng tin cậy

Vấn đề: Engineer thiết kế hệ thống với giả định RPC luôn thành công, packet không bao giờ mất.

Cách làm sai: Không implement retry logic, không handle timeout, không có circuit breaker giữa service và consensus cluster.

Hậu quả production: Khi network partition xảy ra (và nó SẼ xảy ra — switch reboot, cable bị rút, cloud AZ gián đoạn), service treo cứng chờ response từ consensus cluster mà không bao giờ đến. Cascading failure lan ra toàn hệ thống.

Cách đúng: Mọi call tới consensus cluster cần timeout rõ ràng, retry với exponential backoff, và fallback behavior khi cluster không khả dụng. Client library của etcd/Consul đều hỗ trợ — hãy cấu hình đúng thay vì dùng default.

2. Dùng số node chẵn

Vấn đề: Deploy cluster 4 node "cho an toàn hơn 3 node".

Cách làm sai: Cluster 4 node, quorum = 3. Chịu được 1 node fail — giống hệt cluster 3 node (quorum = 2, chịu 1 fail).

Hậu quả production: Bạn trả thêm chi phí cho node thứ 4 nhưng không tăng fault tolerance. Tệ hơn, 4 node tăng xác suất split vote trong election (2-2 tie), kéo dài thời gian bầu leader.

Cách đúng: Luôn dùng số lẻ: 3 node (chịu 1 fail), 5 node (chịu 2 fail), 7 node (chịu 3 fail). Công thức: cluster N node chịu được ⌊(N-1)/2⌋ failure. Thêm node chỉ có ý nghĩa khi tăng từ 3→5 hoặc 5→7.

3. Bỏ qua split-brain trong leader election

Vấn đề: Tự implement leader election bằng database row lock hoặc Redis SETNX thay vì consensus protocol.

Cách làm sai: Hai service instance cùng nghĩ mình là leader vì lock mechanism không có fencing.

Hậu quả production: Hai cron scheduler cùng chạy → duplicate job execution. Hai payment processor cùng xử lý → charge khách hàng hai lần. Đây là split-brain — và hậu quả từ mất tiền đến mất khách hàng.

Cách đúng: Sử dụng leader election được back bởi consensus protocol thực sự (etcd, ZooKeeper, Consul). Nếu tự implement, phải có fencing token — monotonically increasing number mà downstream service kiểm tra để reject request từ leader cũ.

4. Không xử lý leader failover trong application code

Vấn đề: Application code giả định leader luôn là cùng một endpoint.

Cách làm sai: Hardcode leader address, không listen leader change event, không retry trên node khác khi leader fail.

Hậu quả production: Leader crash → application gửi request vào node đã chết → timeout → user nhìn thấy lỗi. Hệ thống đã bầu leader mới nhưng application không biết.

Cách đúng: Sử dụng client library có automatic leader discovery (etcd client, ZooKeeper client tự handle). Nếu dùng HTTP/gRPC trực tiếp, implement service discovery hoặc watch leader key để cập nhật endpoint realtime.

5. Nhầm lẫn consensus với consistency

Vấn đề: Nghĩ rằng "dùng Raft = dữ liệu luôn consistent ở mọi node mọi lúc".

Cách làm sai: Đọc từ follower và kỳ vọng thấy dữ liệu mới nhất ngay lập tức.

Hậu quả production: Follower có thể lag vài millisecond (hoặc lâu hơn dưới tải nặng). Client đọc từ follower thấy stale data → hiển thị sai cho user → bug khó tái tạo.

Cách đúng: Consensus đảm bảo committed entries giống nhau trên mọi node, nhưng follower apply log không đồng bộ. Muốn linearizable read, phải đọc qua leader (hoặc dùng ReadIndex/LeaseRead optimization). etcd mặc định serializable read — cần chỉ định --consistency=l cho linearizable.

Under the Hood

Raft vs Paxos — bản chất khác biệt

Raft và Paxos giải quyết cùng bài toán nhưng tiếp cận khác nhau ở mức triết lý:

Paxos chứng minh correctness từ nguyên lý đầu tiên — mỗi property được derive từ invariant toán học. Ưu điểm: chứng minh chặt chẽ. Nhược điểm: gap giữa paper và implementation rất lớn, dẫn đến mỗi implementation "Paxos" thực tế là một variant khác nhau.

Raft thiết kế ngược — bắt đầu từ "engineer cần hiểu gì để implement đúng", rồi chứng minh correctness sau. Strong leader (mọi thay đổi phải qua leader) đơn giản hóa reasoning nhưng tạo bottleneck tại leader node.

Hệ quả thực tiễn: nếu bạn cần implement consensus protocol (hiếm khi khuyến khích), Raft cho bạn con đường rõ ràng hơn. Nếu bạn nghiên cứu hoặc cần flexibility (leaderless operation), Paxos family (EPaxos, Flexible Paxos) mạnh hơn.

Quorum Math

Với cluster N node, quorum size Q = ⌊N/2⌋ + 1. Cluster chịu được f = N - Q = ⌊(N-1)/2⌋ failure.

N (total nodes)Q (quorum)f (max failures)Ghi chú
321Production minimum
431Lãng phí — cùng fault tolerance như 3
532Recommended cho production
743Large-scale, cross-region

Tại sao quorum hoạt động? Vì bất kỳ hai quorum nào đều overlap ít nhất 1 node. Trong cluster 5 node, quorum = 3. Hai nhóm 3 node bất kỳ luôn có ít nhất 1 node chung — node này đảm bảo thông tin từ decision trước được truyền tới decision sau. Đây là invariant cốt lõi ngăn split-brain.

Split-brain Analysis

Xem xét cluster 5 node bị network partition chia thành hai phần:

Partition A (3 node): Leader vẫn có quorum → tiếp tục phục vụ read/write bình thường.

Partition B (2 node): Node 4 hoặc 5 có thể timeout và bắt đầu election, nhưng chỉ có 2 node — không đủ quorum (cần 3). Election không thành công → partition B không có leader → không thể ghi. Đây chính xác là behavior mong muốn: thà từ chối ghi còn hơn chấp nhận ghi mà hai partition có dữ liệu khác nhau.

Khi network heal, Node 4 và 5 phát hiện leader ở term cao hơn → tự sync log từ leader → cluster hội tụ.

CAP Theorem Connection

Consensus protocol chọn CP trong tam giác CAP:

  • Consistency (C): Mọi read đều thấy write mới nhất (linearizability).
  • Partition tolerance (P): Hệ thống tiếp tục hoạt động khi network partition xảy ra.
  • Availability (A): Bị hy sinh — partition phía thiểu số không thể phục vụ write.

Đây là trade-off có chủ đích. Nếu bạn cần AP (always available, eventually consistent), hãy xem eventual consistency models (Dynamo-style, CRDTs) thay vì consensus.

Lưu ý: CAP theorem thường bị đơn giản hóa quá mức. Thực tế, hệ thống consensus vẫn available khi không có partition (majority partition vẫn available). "Chọn CP" nghĩa là khi partition xảy ra, system ưu tiên consistency trên availability — không phải lúc nào cũng unavailable.

Checklist ghi nhớ

✅ Checklist triển khai

Thiết kế cluster

  • [ ] Luôn dùng số lẻ node: 3 (dev/staging), 5 (production), 7 (cross-region)
  • [ ] Đặt node ở các failure domain khác nhau (rack, AZ, region)
  • [ ] Sizing: SSD cho WAL, đủ RAM cho toàn bộ working set, network latency < 10ms giữa các node
  • [ ] Cấu hình election timeout phù hợp network latency (ít nhất 10x RTT)

Xử lý failure

  • [ ] Test failure scenario trước production: kill leader, partition network, disk full
  • [ ] Implement fencing token cho mọi leader election pattern
  • [ ] Client code có retry với backoff và automatic leader rediscovery
  • [ ] Không bao giờ dùng lock mechanism thiếu consensus (DB row lock, Redis SETNX) cho critical section

Monitoring & operations

  • [ ] Alert trên: leader change frequency, commit latency p99, log apply lag
  • [ ] Monitor disk usage — WAL và snapshot cần compaction định kỳ
  • [ ] Backup snapshot thường xuyên — consensus cluster không thay thế backup strategy
  • [ ] Có runbook cho: leader stuck, quorum loss, node replacement, emergency compaction

Application integration

  • [ ] Sử dụng client library chính thức — không gọi HTTP API trực tiếp
  • [ ] Cấu hình timeout và retry policy phù hợp SLA
  • [ ] Phân biệt linearizable read vs serializable read — chọn đúng cho use case
  • [ ] Lease TTL phù hợp: không quá ngắn (flapping), không quá dài (slow failover)

Bài tập luyện tập

Bài 1: Quorum Calculation

🧠 Quiz

Cluster etcd gồm 7 node được deploy cross-region. Trong một sự cố, 3 node đồng thời mất kết nối.

Hỏi: Cluster có tiếp tục phục vụ write request được không?

A. Có — chỉ cần 1 node sống là đủ

B. Có — 4 node còn lại đạt quorum (⌊7/2⌋ + 1 = 4)

C. Không — mất quá nửa cluster

D. Tùy — phụ thuộc vào node nào là leader

Đáp án: B

Giải thích: Quorum = ⌊7/2⌋ + 1 = 4. Với 4 node còn lại, cluster đủ quorum để bầu leader (nếu cần) và commit entry. Nếu leader nằm trong 3 node mất kết nối, cluster sẽ bầu leader mới từ 4 node còn lại — có thể mất vài giây nhưng vẫn recovery tự động. Đáp án D sai vì dù leader cũ chết, election tự động sẽ chọn leader mới.

Bài 2: Thiết kế Leader Election cho Task Scheduler

Đề bài: Bạn có hệ thống Task Scheduler với 3 instance. Yêu cầu:

  • Chỉ 1 instance được trigger job tại bất kỳ thời điểm nào
  • Failover trong vòng 15 giây khi leader crash
  • Không duplicate job execution

Thiết kế leader election mechanism sử dụng etcd.

💡 Gợi ý

Cân nhắc các thành phần:

  1. Lease TTL bao nhiêu để đảm bảo failover < 15s?
  2. KeepAlive interval bao nhiêu?
  3. Làm sao để instance biết mình không còn là leader?
  4. Khi leader mới lên, làm sao tránh duplicate job mà leader cũ đang chạy dở?
✅ Hướng giải

1. Lease configuration:

  • TTL = 10s (< 15s failover requirement)
  • KeepAlive interval = 3s (TTL/3 — đủ 2 lần retry trước khi expire)

2. Election flow:

  • Mỗi instance tạo lease, rồi PUT /scheduler/leader với IF key NOT EXISTS.
  • Instance thắng trở thành leader, bắt đầu KeepAlive loop.
  • Instance thua watch key /scheduler/leader — khi key bị xóa (lease expire), retry acquire.

3. Leader self-check:

  • Trước mỗi job trigger, leader gọi GET /scheduler/leader verify value vẫn là mình.
  • Nếu lease đã expire (network issue), dừng ngay — không trigger thêm job.

4. Tránh duplicate execution:

  • Mỗi job execution ghi trạng thái vào etcd: /jobs/{job-id}/last-run = {timestamp, leader-id}.
  • Leader mới check trạng thái trước khi run — nếu job đã chạy trong window, skip.
  • Idempotent job design: job nên tự kiểm tra "đã xử lý chưa" thay vì phụ thuộc hoàn toàn vào scheduler.

Bài 3: Phân tích Split-brain Scenario

Đề bài: Hệ thống payment processing dùng Consul cluster 5 node để distributed lock trước khi charge credit card. Đột ngột, network partition chia cluster thành 2 nhóm: {Node1, Node2} và {Node3, Node4, Node5}.

Phân tích:

  1. Payment service kết nối với Node1 có thể acquire lock không?
  2. Payment service kết nối với Node3 có thể acquire lock không?
  3. Nếu không dùng Consul mà dùng Redis single-node cho lock, kịch bản sẽ khác thế nào?
  4. Đề xuất kiến trúc tốt hơn để tránh charge khách hàng hai lần.
💡 Gợi ý
  • Quorum của cluster 5 node = 3.
  • Phía nào có quorum mới có thể elect leader và xử lý write.
  • Redis single-node không có partition tolerance — partition ảnh hưởng khác.
  • Nghĩ về fencing token và idempotency key cho payment.
✅ Hướng giải

1. Không. {Node1, Node2} chỉ có 2 node — không đủ quorum (cần 3). Không thể elect leader → không thể xử lý lock acquire → payment service bên này bị block. Đây là behavior đúng — thà block còn hơn double-charge.

2. Có. {Node3, Node4, Node5} có 3 node = đủ quorum. Consul elect leader mới (nếu leader cũ ở partition kia) → payment service acquire lock bình thường.

3. Redis single-node: không có quorum concept. Nếu Redis nằm ở partition A, service ở partition B mất kết nối → không acquire được lock (giống Consul). Nhưng nếu service và Redis cùng partition, lock hoạt động bình thường — vấn đề là Redis không có consensus, nên lock có thể bị mất khi Redis restart hoặc failover (data chưa sync tới replica).

4. Kiến trúc recommended:

  • Consul/etcd lock làm distributed mutex — prevent concurrent charge.
  • Fencing token: lock response trả về monotonic token, payment gateway check token > last seen token mới accept.
  • Idempotency key: mỗi charge request gắn unique key (order ID). Payment gateway deduplicate — dù gọi 2 lần, chỉ charge 1 lần.
  • Hai lớp bảo vệ: consensus lock (prevent) + idempotency (detect & recover).

Liên kết học tiếp