Case study — Cassandra ring & etcd Raft
Cassandra ghép consistent hashing + tunable quorum + gossip + Merkle tree ra sao; etcd dùng Raft cho config nhất quán mạnh thế nào. Vì sao một chọn AP, một chọn CP.
TL;DR: Cassandra và etcd đều là hệ phân tán nổi tiếng, nhưng chúng đặt cược khác nhau trên tam giác CAP. Cassandra ghép bốn thuật toán từ module này — consistent hashing phân mảnh dữ liệu lên ring, tunable quorum cho phép điều chỉnh mức nhất quán theo request, gossip lan truyền trạng thái membership, Merkle tree sửa chênh lệch giữa các replica — để đạt AP: luôn ghi được, chấp nhận eventual consistency. etcd chọn CP: Raft đảm bảo mọi đọc/ghi đều nhất quán mạnh, đổi lấy việc mất availability khi mất quorum. Bài học từ hai hệ thống: không có thiết kế "tốt hơn" — chỉ có thiết kế phù hợp với bài toán.
AP — consistent hashing + tunable quorum + gossip + Merkle tree
CP — Raft consensus cho config/service-discovery nhất quán mạnh
Phần A — Cassandra: bốn thuật toán ghép lại
1. Bài toán: dữ liệu người dùng ở quy mô Netflix
Apache Cassandra được thiết kế để lưu hàng trăm terabyte dữ liệu trải trên hàng trăm đến hàng nghìn node, không có single point of failure. Netflix dùng Cassandra để lưu viewing history, bookmark và user preferences — hệ thống phải luôn ghi được ngay cả khi một datacenter mất kết nối.
Yêu cầu:
- Phân mảnh — 100 TB dữ liệu phải phân bố đều trên hàng trăm node.
- Nhất quán — replica phải đồng bộ, phát hiện và sửa chênh lệch.
- Membership — mỗi node cần biết toàn bộ cluster đang có node nào, node nào đang chết.
- Fault tolerance — vẫn hoạt động khi một số node fail.
Cassandra giải quyết bốn yêu cầu này bằng bốn thuật toán từ module này:
2. Consistent hashing: token ring và virtual node
Cassandra dùng token ring — mỗi key được hash thành token trong không gian [0, 2^64), mỗi node sở hữu một khoảng token. Mỗi node vật lý được nhân bản thành nhiều virtual node (vnode) — mặc định 16 vnode mỗi node từ Cassandra 4.0 (bản cũ dùng 256) — để đảm bảo phân bố tải đều ngay cả khi node có phần cứng khác nhau.
-- Cassandra token assignment (đơn giản hóa)
-- Node A: owns tokens [100, 400), [700, 900) (2 vnode)
-- Node B: owns tokens [0, 100), [400, 700) (2 vnode)
-- Node C: owns tokens [900, 2^64) wrap [0, 0) (1 vnode)
function getCoordinator(partitionKey):
token <- murmur3Hash(partitionKey) -- Cassandra dùng Murmur3
return lookupRing(ring, token) -- binary search, O(log(N×vnode))
Khi thêm node mới vào Cassandra cluster, chỉ vnode của node mới cần streaming data từ node kế tiếp — phần còn lại không bị ảnh hưởng. Đây chính là tính chất K/N key di chuyển của consistent hashing từ bài 01 và mini-challenge vừa xong.
Replication factor (RF): Cassandra không chỉ lưu 1 bản — mặc định RF=3, nghĩa là mỗi key được lưu trên 3 node vật lý kế tiếp nhau trên ring. Khi node chết, 2 bản replica còn lại vẫn phục vụ được.
3. Tunable quorum: CAP tradeoff theo request
Cassandra cho phép client chỉ định consistency level cho từng request — đây là ứng dụng trực tiếp của quorum R+W>N từ bài 02:
| Consistency level | Ý nghĩa | R+W>N? | Tradeoff |
|---|---|---|---|
ONE | 1 replica xác nhận là đủ | Không đảm bảo | Nhanh nhất, có thể đọc stale |
QUORUM | Đa số replica xác nhận (N/2 + 1) | Có (với RF=3: R=W=2, R+W=4>3) | Cân bằng latency và consistency |
ALL | Tất cả replica xác nhận | Có | Nhất quán tuyệt đối, nhưng khi 1 node fail thì ghi/đọc fail |
LOCAL_QUORUM | Đa số replica trong 1 datacenter | Có trong DC | Dùng cho multi-datacenter |
Netflix thường dùng LOCAL_QUORUM cho đọc và LOCAL_QUORUM cho ghi — đảm bảo nhất quán trong mỗi datacenter nhưng không block khi một datacenter bị cô lập.
-- Write path với QUORUM (RF=3, cần 2 replica xác nhận)
coordinator <- getCoordinator(partitionKey)
replicas <- getReplicaNodes(ring, partitionKey, replicationFactor=3)
-- Ghi song song đến cả 3 replica
for each replica trong replicas:
sendWriteAsync(replica, mutation)
-- Chờ đủ quorum (2/3)
waitForAcks(count=2, timeout=10ms)
-- Nếu đủ 2 ack trong timeout: success
-- Nếu không: timeout error (replica thứ 3 vẫn được ghi async sau đó)
Đây chính là cơ chế AP của Cassandra: ghi thành công khi đủ quorum, ngay cả khi một số replica lag sau. Replica lag sẽ được sửa bởi anti-entropy.
4. Gossip: membership và failure detection
Mỗi node Cassandra cần biết toàn bộ cluster — node nào đang sống, node nào đang chết, load của từng node là bao nhiêu. Với cluster 1 000 node, broadcast toàn bộ mỗi giây là không khả thi.
Cassandra dùng Gossip protocol (bài 05) — mỗi giây, mỗi node chọn ngẫu nhiên 1-3 node và trao đổi thông tin về trạng thái của mọi node mà cả hai biết. Sau O(log N) vòng, thông tin lan khắp cluster:
-- Cassandra gossip cycle (mỗi giây trên mỗi node)
peers <- selectRandom(cluster, count=3) -- chọn 3 node ngẫu nhiên
for each peer trong peers:
myState <- localNodeState() -- version, load, tokens, status
peerGossip <- exchange(peer, myState)
mergeState(peerGossip) -- cập nhật bảng trạng thái local
-- Sau ~log2(1000) ≈ 10 vòng gossip: mọi node biết trạng thái mọi node khác
Gossip cũng chạy Phi Accrual Failure Detector — thay vì "timeout = dead", tính xác suất node thực sự chết dựa trên lịch sử heartbeat. Điều này tránh false positive khi network tạm thời chậm.
5. Merkle tree: anti-entropy repair
Dù có quorum, replica vẫn có thể chênh lệch theo thời gian (network partition tạm thời, node crash giữa write). Cassandra chạy anti-entropy repair định kỳ bằng Merkle tree (bài 03):
sequenceDiagram
participant N1 as Node 1
participant N2 as Node 2
Note over N1,N2: Anti-entropy repair
N1->>N1: Build Merkle tree<br/>cho token range mình sở hữu
N2->>N2: Build Merkle tree<br/>cho cùng token range
N1->>N2: Gửi Merkle root
N2->>N1: So sánh root: khác nhau!
N1->>N2: Gửi subtree trái
N2->>N1: Subtree trái trùng khớp
N1->>N2: Gửi subtree phải
N2->>N1: Subtree phải khác! Gửi diff
N1->>N1: Nhận diff, sync lại rows bị thiếuThay vì so sánh từng row (hàng triệu row), Merkle tree cho phép xác định đúng đoạn token range bị chênh lệch trong O(log N) so sánh hash, rồi chỉ sync dữ liệu ở đoạn đó.
Phần B — etcd: Raft cho config nhất quán mạnh
6. Bài toán: source of truth của Kubernetes
etcd là key-value store phân tán được dùng làm backing store cho Kubernetes — lưu toàn bộ trạng thái cluster: pod, deployment, service, configmap, secret. Mỗi thay đổi trong Kubernetes (thêm pod, scale deployment, cập nhật config) phải được ghi vào etcd và được mọi control plane node nhìn thấy nhất quán.
Yêu cầu khác hoàn toàn với Cassandra:
- Strong consistency bắt buộc: hai controller đọc config cùng lúc phải thấy cùng một giá trị — không thể có split-brain.
- No stale read: nếu một node write
replicas=5, node khác đọc ngay sau phải thấy5, không phải3. - Chấp nhận giảm availability: nếu mất quorum (mất hơn N/2 node), etcd từ chối phục vụ thay vì trả về data có thể stale.
7. Raft trong etcd: leader là single source of truth
etcd triển khai Raft (bài 06) — mọi write phải đi qua leader, được nhân bản vào log của đa số node trước khi commit:
-- etcd write path (Raft)
client gửi PUT /key value đến bất kỳ etcd node
if node không phải leader: redirect về leader
-- Leader xử lý:
leader.log.append(entry={term, index, key, value})
-- Gửi AppendEntries đến tất cả follower song song
for each follower trong cluster:
sendAppendEntries(follower, entry)
-- Chờ đa số xác nhận (3/5 node = quorum)
waitForAcks(count=majority)
-- Khi đủ quorum: commit entry, apply vào state machine
leader.commitIndex <- entry.index
leader.applyToStateMachine(entry)
-- Báo success cho client
Mọi read cũng đi qua leader (trong chế độ linearizable mặc định) để đảm bảo không có stale read. Đây là điểm khác với Cassandra: Cassandra có thể đọc từ bất kỳ replica nào với ONE, etcd luôn đọc từ leader.
sequenceDiagram
participant C as Client
participant L as Leader (etcd)
participant F1 as Follower 1
participant F2 as Follower 2
C->>L: PUT /config replicas=5
L->>L: Append to log (term=3, idx=42)
L->>F1: AppendEntries(term=3, idx=42)
L->>F2: AppendEntries(term=3, idx=42)
F1->>L: ACK
F2->>L: ACK
Note over L: 3/3 acks = quorum đủ → commit
L->>L: Apply to state machine
L->>C: OK, revision=428. Vì sao etcd chọn CP, không AP như Cassandra
Đây là ứng dụng trực tiếp của CAP theorem:
| Cassandra | etcd | |
|---|---|---|
| Use case | User data, session, activity log — stale vài giây chấp nhận được | Kubernetes config, service discovery — split-brain có thể gây deploy loop hoặc double scheduling |
| CAP chọn | AP (availability + partition tolerance) | CP (consistency + partition tolerance) |
| Khi mất quorum | Vẫn đọc/ghi được với ONE, data có thể stale | Từ chối phục vụ cho đến khi phục hồi quorum |
| Độ trễ | Thấp (gossip, async) | Cao hơn một chút (Raft roundtrip đến đa số) |
| Consistency model | Tunable: eventual đến strong | Linearizable (strong) by default |
Kubernetes không thể chịu đựng stale config: nếu scheduler đọc stale và tin rằng pod chưa được schedule trong khi thực ra đã được schedule rồi, nó sẽ schedule lại — dẫn đến duplicate pod. etcd chọn CP để loại trừ khả năng này hoàn toàn.
Cassandra vs etcd: vì sao một chọn AP, một chọn CP
graph TD
Q0["Bài toán hệ phân tán"] --> Q1{"Stale data có\nchấp nhận được?"}
Q1 -- "Có — vài giây lag OK" --> Q2{"Scale lớn,\ncần luôn ghi được?"}
Q2 -- "Có" --> CASS["Cassandra AP\nConsistent hashing +\nTunable quorum +\nGossip + Merkle"]
Q2 -- "Không" --> Q3["Cân nhắc strong DB\n(PostgreSQL, MySQL)"]
Q1 -- "Không — must be\nstrongly consistent" --> Q4{"Cluster size\nnhỏ (3-7 node)?"}
Q4 -- "Có" --> ETCD["etcd CP — Raft\nLinearizable reads/writes\nKubernetes, Consul"]
Q4 -- "Không" --> Q5["Spanner, CockroachDB\n(distributed SQL + Paxos)"]Cassandra AP phù hợp khi:
- Dữ liệu có thể được đọc lại (user preferences, activity log) — stale vài giây không gây hại.
- Scale cực lớn (hàng trăm node, petabyte) — Raft không scale tốt ở số node này.
- Cần geo-distribution với low latency writes ở mỗi datacenter.
etcd CP phù hợp khi:
- Config, locking, service discovery — bất kỳ inconsistency nào đều gây hành vi sai.
- Cluster nhỏ (thường 3-5 node) — Raft hoạt động tốt nhất ở quy mô này.
- Đổi được availability lấy correctness.
Liên hệ các bài khác
- Consistent hashing: token ring của Cassandra là ứng dụng trực tiếp — bài đó dạy cơ chế, case study này cho thấy cách Cassandra thêm RF và vnode vào.
- Quorum: tunable consistency level (ONE/QUORUM/ALL) của Cassandra là R+W>N ở các mức khác nhau. Bài đó chứng minh R+W>N đảm bảo overlap.
- Merkle tree: anti-entropy repair của Cassandra dùng Merkle tree để tìm đúng đoạn data bị chênh lệch mà không phải so toàn bộ.
- Gossip: Cassandra gossip protocol là ứng dụng chuẩn của epidemic broadcast — membership, failure detection, load balancing info.
- Raft: etcd là reference implementation Raft phổ biến nhất. Bài đó giải thích leader election và log replication — case study này cho thấy Kubernetes dùng nó ra sao.
Tự kiểm tra
Q1Cassandra dùng consistent hashing với RF=3. Khi Node B fail, Cassandra vẫn phục vụ key của Node B như thế nào?▸
Với RF=3, mỗi key được lưu trên 3 node vật lý kế tiếp nhau trên ring. Khi Node B fail, 2 replica còn lại (Node C và Node D, hai node kế tiếp sau B trên ring) vẫn có đầy đủ dữ liệu.
Client request được coordinator redirect đến replica còn sống. Với consistency level ONE, chỉ cần 1 replica trả lời — Node B chết không ảnh hưởng gì. Với QUORUM (cần 2/3), vẫn đủ vì còn 2 replica. Chỉ ALL mới fail khi một node chết.
Sau khi Node B hồi phục, Cassandra chạy hinted handoff (ghi tạm ở coordinator) và anti-entropy repair (Merkle tree) để đồng bộ lại dữ liệu bị bỏ lỡ.
Q2Tại sao etcd từ chối phục vụ khi mất quorum (mất hơn N/2 node), trong khi Cassandra vẫn tiếp tục với consistency level ONE?▸
etcd triển khai Raft với cam kết linearizable consistency: mọi read phải phản ánh write gần nhất. Raft chỉ commit entry khi đa số node xác nhận — nếu mất quorum, không thể đảm bảo điều này. Phục vụ trong trạng thái này có thể trả về stale data, vi phạm contract với client. etcd chọn từ chối (CP) thay vì vi phạm consistency.
Cassandra chọn AP: với consistency level ONE, chỉ cần 1 replica trả lời. Cassandra không đảm bảo linearizability — chỉ đảm bảo eventual consistency (data sẽ đồng bộ sau). Với use case như user preferences hay activity log, stale vài giây chấp nhận được và availability quan trọng hơn.
Đây là tradeoff CAP trực tiếp: cùng một scenario "mất node", Cassandra ưu tiên A, etcd ưu tiên C.
Q3Cassandra dùng Merkle tree để làm gì trong anti-entropy repair? Tại sao không so sánh từng row trực tiếp?▸
Anti-entropy repair cần xác định xem hai replica có chênh lệch không và chênh lệch ở đâu. So sánh từng row trực tiếp là O(N) với N là số row — với hàng tỷ row trên một node, chi phí network và I/O là không chấp nhận được nếu chạy định kỳ.
Merkle tree hash dữ liệu theo phân cấp: hash từng block nhỏ, rồi hash cặp block thành node cha, đến tận root. Khi hai replica so sánh Merkle root và thấy khác nhau, chỉ cần đi xuống cây theo nhánh khác nhau — O(log N) thay vì O(N) để tìm đúng đoạn chênh lệch.
Ví dụ: node có 1 tỷ row, chia thành 2^20 block. Nếu chỉ 1 block bị chênh lệch, Merkle tree tìm ra trong 20 bước so sánh hash thay vì 1 tỷ bước so sánh row.
Q4Trong Raft của etcd, tại sao mọi write phải đi qua leader? Điều gì xảy ra nếu cho phép follower nhận write trực tiếp?▸
Raft đảm bảo linearizability bằng cách có duy nhất một node (leader) quyết định thứ tự của mọi entry trong log. Nếu follower cũng nhận write, hai follower có thể nhận hai write đồng thời và append vào log theo thứ tự khác nhau — dẫn đến state machine diverge (hai node kết thúc ở trạng thái khác nhau dù xử lý cùng một tập write).
Leader đảm bảo serialization: mọi entry có index duy nhất và được nhân bản theo đúng thứ tự đó. Follower chỉ đơn giản là áp dụng log theo thứ tự mà leader đã quyết định.
Nếu client kết nối đến follower, follower redirect request về leader. etcd client library xử lý điều này tự động.
Q5Gossip protocol của Cassandra chỉ trao đổi với 1-3 node mỗi giây. Tại sao thông tin vẫn lan khắp cluster 1 000 node trong vài giây?▸
Gossip là epidemic broadcast: số node đã biết thông tin tăng theo cấp số nhân sau mỗi vòng. Sau vòng 1, 1 node ban đầu nói với 3 node → 4 node biết. Sau vòng 2, 4 node nói với 3 node mỗi người → tối đa 16 node biết. Sau k vòng, tối đa 4^k node biết.
Với cluster 1 000 node: 4^5 = 1024 → sau khoảng 5 vòng gossip (5 giây), về lý thuyết toàn bộ cluster đã biết. Trong thực tế có trùng lặp (node đã biết được gossip lại) nhưng thời gian lan truyền vẫn là O(log N).
Lưu ý hằng số: ở đây mỗi vòng node nói với k=3 peer (push-pull) nên base lớn (4^k), hội tụ ~5 vòng. Bài 05 dùng k=1 (base 2, 2^k) nên cần ~10 vòng cho cùng 1024 node — cùng O(log N), chỉ khác hằng số do số peer mỗi vòng.
Đây chính là sức mạnh của epidemic propagation: không cần coordinator tập trung, không có single point of failure trong việc lan truyền thông tin.
Bài tiếp theo: Module 4 — Tổng kết & cheat sheet
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên