Tuần 20: Design Distributed Key-Value Store (Capstone)

“Nếu em hiểu cách xây một distributed key-value store từ đầu đến cuối, em hiểu 80% cốt lõi của distributed systems. Mọi thứ khác chỉ là biến thể.”

Tags: system-design distributed-systems key-value-store capstone alex-xu Student: Hieu Prerequisite: Tuan-01-Scale-From-Zero-To-Millions · Tuan-02-Back-of-the-envelope · Tuan-07-Database-Sharding-Replication · Tuan-10-Consistent-Hashing Liên quan: Tuan-03-Networking-DNS-CDN · Tuan-05-Load-Balancer · Tuan-06-Cache-Strategy · Tuan-08-Message-Queue · Tuan-09-Rate-Limiter · Tuan-11-Microservices-Pattern · Tuan-13-Monitoring-Observability · Tuan-14-AuthN-AuthZ-Security · Tuan-15-Data-Security-Encryption


Tại sao đây là Capstone Week?

Hieu, tuần này là tuần tổng hợp — nơi mọi thứ em đã học từ Tuần 01 đến Tuần 19 hội tụ lại trong một bài toán duy nhất. Một distributed key-value store (KV store) đụng chạm đến:

Khái niệmTuần đã họcVai trò trong KV Store
ScalingTuan-01-Scale-From-Zero-To-MillionsHorizontal scaling qua partitioning
EstimationTuan-02-Back-of-the-envelopeTính storage per node, throughput
NetworkingTuan-03-Networking-DNS-CDNCross-DC replication latency
API DesignTuan-04-API-Design-REST-gRPCput/get API, gRPC inter-node
Load BalancerTuan-05-Load-BalancerRequest routing to coordinator
CacheTuan-06-Cache-StrategyMemtable chính là in-memory cache
DB Sharding & ReplicationTuan-07-Database-Sharding-ReplicationConsistent hashing + replication factor
Message QueueTuan-08-Message-QueueHinted handoff queue
Rate LimiterTuan-09-Rate-LimiterProtect nodes from overload
Consistent HashingTuan-10-Consistent-HashingData partitioning core
MonitoringTuan-13-Monitoring-ObservabilityCluster health, compaction lag
SecurityTuan-14-AuthN-AuthZ-Security · Tuan-15-Data-Security-EncryptionEncryption at rest, inter-node TLS

Các hệ thống KV store nổi tiếng trong thực tế: Amazon DynamoDB, Apache Cassandra, Riak, Voldemort, etcd (CP), Redis Cluster.


Step 1 — Understand the Problem & Establish Design Scope

1.1 Functional Requirements (Yêu cầu chức năng)

OperationMô tảVí dụ
put(key, value)Lưu cặp key-value. Nếu key đã tồn tại → ghi đèput("user:1001", "{name: Hieu}")
get(key)Trả về value tương ứng với keyget("user:1001")"{name: Hieu}"
delete(key)Xoá key (thực tế: đánh tombstone)delete("user:1001")

Constraints:

  • Key: string, tối đa 256 bytes
  • Value: blob, tối đa 1 MB (thường < 10 KB)
  • Không hỗ trợ range query (khác biệt với sorted KV store)

1.2 Non-Functional Requirements (Yêu cầu phi chức năng)

Yêu cầuMục tiêuGiải thích
High Availability (Tính sẵn sàng cao)99.99% uptimeHệ thống vẫn hoạt động kể cả khi một số node chết
High Scalability (Khả năng mở rộng)Hàng triệu key, hàng trăm nodeTự động scale khi data tăng
Tunable Consistency (Tính nhất quán có thể điều chỉnh)Strong → EventualClient chọn mức consistency phù hợp use case
Automatic ScalingThêm/bớt node không downtimeConsistent hashing giúp rebalance tự động
Low Latencyp99 < 10ms cho read/writeCần in-memory structures
Fault Tolerance (Chịu lỗi)Survive node/rack/DC failureReplication + failure detection

1.3 Capacity Estimation (Ước lượng năng lực)

Assumptions:

Thông sốGiá trịGiải thích
Total keys1 Billion (1B)Hệ thống quy mô lớn
Average value size10 KBJSON document trung bình
Read:Write ratio10:1Read-heavy
DAU (applications)10,000 servicesMicroservices gọi vào KV store
Reads/service/day1,000,0001M reads mỗi service
Writes/service/day100,000100K writes mỗi service
Replication factor (N)3Mỗi key lưu trên 3 node
Number of nodes100Cluster ban đầu

QPS Calculation:

Per-node QPS (100 nodes, đều tải nhờ consistent hashing):

Nhận xét: Mỗi node chỉ handle ~3,800 QPS tổng. Một node Cassandra trung bình handle 10K–50K ops/s → dư sức. Nhưng phải tính cả replication write amplification.

Write Amplification do Replication:

Mỗi write từ client phải được persist tại N=3 replicas. Tổng writes trong cluster:

Phân bổ đều trên 100 nodes:

Cách hiểu trực quan: Mỗi node đảm nhiệm 2 vai trò cho các write khác nhau — (1) coordinator cho ~360 write/s mà nó nhận trực tiếp từ client, (2) replica cho ~720 write/s được forward từ coordinator khác (vì với RF=3, mỗi key có 3 replicas, mỗi node nằm trong tập replica của ~2/100 phần key space của các coordinator khác). Tổng vẫn là 1,080 writes/s/node.

Với LSM-tree compaction, disk write amplification thêm 5-10x → thực tế disk I/O ~5K-10K writes/s/node. Đây là lý do KV store cần NVMe SSD.

Storage Estimation:

Overhead thực tế (index, bloom filter, commit log, compaction temp space):

Rule of thumb: Nhân raw storage với 2x–3x cho overhead. Cassandra khuyến nghị giữ disk usage < 50% để compaction có đủ chỗ.

Bandwidth Estimation:

Mỗi node cần NIC tối thiểu 1 Gbps, khuyến nghị 10 Gbps cho headroom.

Replication Network Overhead:

Tóm tắt Estimation:

MetricValue
Read QPS (peak)~348K/s
Write QPS (peak)~36K/s
Total raw data10 TB
Total with replication (N=3)30 TB
Storage per node (with overhead)~750 GB
Recommended disk per node1.5 TB
Bandwidth per node~364 Mbps
Cluster size100 nodes

Step 2 — High-Level Design

2.1 CAP Theorem Deep Dive

CAP Theorem là gì?

CAP Theorem (Eric Brewer, 2000) phát biểu rằng trong một hệ thống phân tán, ta chỉ có thể đảm bảo tối đa 2 trong 3 tính chất:

Tính chấtTiếng ViệtÝ nghĩa
Consistency (Tính nhất quán)Nhất quánMọi node đều trả về cùng data tại cùng thời điểm
Availability (Tính sẵn sàng)Sẵn sàngMọi request đều nhận được response (không bị reject)
Partition Tolerance (Chịu phân vùng)Chịu lỗi mạngHệ thống vẫn hoạt động khi mạng giữa các node bị đứt

Quan trọng: Trong thực tế, P luôn phải có vì network partition là không thể tránh khỏi. Vậy lựa chọn thực sự là giữa CPAP.

CP vs AP — Lựa chọn thực tế

flowchart LR
    subgraph "CAP Theorem"
        C["🔒 Consistency"]
        A["✅ Availability"]
        P["🌐 Partition Tolerance"]
    end

    subgraph "CP Systems"
        CP1["etcd / ZooKeeper"]
        CP2["HBase"]
        CP3["MongoDB (default)"]
        CP4["Google Spanner"]
    end

    subgraph "AP Systems"
        AP1["Cassandra"]
        AP2["DynamoDB"]
        AP3["Riak"]
        AP4["CouchDB"]
    end

    C --- CP1
    C --- CP2
    C --- CP3
    C --- CP4
    A --- AP1
    A --- AP2
    A --- AP3
    A --- AP4

    style C fill:#e53935,color:#fff
    style A fill:#43a047,color:#fff
    style P fill:#1e88e5,color:#fff
Đặc điểmCP SystemAP System
Khi network partitionReject writes để giữ consistencyAccept writes dù data có thể inconsistent
Use caseBanking, inventory count, config mgmtShopping cart, social feed, session store
Trade-offDowntime khi partitionStale/conflicting data khi partition
RecoveryTự động khi partition healCần conflict resolution
Ví dụ thực tếetcd (Kubernetes config), ZooKeeperDynamoDB (Amazon shopping cart), Cassandra

Tại sao KV Store thường chọn AP?

Amazon DynamoDB được sinh ra từ bài toán: “Giỏ hàng của khách không bao giờ được mất, kể cả khi data center sập.” Mất availability = mất tiền. Inconsistency tạm thời thì có thể resolve sau.

Aha Moment: Trong thực tế, AP system không có nghĩa là “không có consistency”. Mà là consistency được nới lỏng (eventual consistency) và có thể điều chỉnh (tunable consistency). Đây chính là sức mạnh của hệ thống KV store hiện đại.

2.2 High-Level Architecture

flowchart TB
    Client["Client Application"]
    LB["Load Balancer"]

    Client -->|"put(k,v) / get(k)"| LB

    subgraph Cluster["KV Store Cluster (Consistent Hash Ring)"]
        direction TB
        Coord["Coordinator Node<br/>(any node can be coordinator)"]

        subgraph Ring["Consistent Hash Ring"]
            N1["Node A<br/>Token: 0-85"]
            N2["Node B<br/>Token: 86-170"]
            N3["Node C<br/>Token: 171-255"]
            N4["Node D<br/>Token: 256-340"]
            N5["Node E<br/>Token: 341-425"]
            N6["Node F<br/>Token: 426-511"]
        end

        Coord -->|"Route by hash(key)"| N1
        Coord -->|"Replicate"| N2
        Coord -->|"Replicate"| N3
    end

    LB --> Coord

    subgraph NodeInternal["Inside Each Node"]
        CL["Commit Log<br/>(append-only, WAL)"]
        MT["Memtable<br/>(in-memory sorted)"]
        BF["Bloom Filter"]
        SS["SSTables on disk<br/>(sorted, immutable)"]
    end

    N1 --- NodeInternal

    style Coord fill:#ff9800,color:#000
    style N1 fill:#4caf50,color:#fff
    style N2 fill:#4caf50,color:#fff
    style N3 fill:#4caf50,color:#fff
    style MT fill:#ffeb3b,color:#000
    style SS fill:#90a4ae,color:#000

Giải thích luồng:

  1. Client gọi put("user:1001", value) hoặc get("user:1001")
  2. Load Balancer route request tới bất kỳ node nào trong cluster
  3. Node nhận request trở thành Coordinator cho request đó
  4. Coordinator dùng consistent hashing để xác định key thuộc node nào → route tới node chủ (primary) và N-1 replicas
  5. Coordinator chờ W (write) hoặc R (read) acknowledgements rồi trả response cho client

Key insight: Không có “master node” — bất kỳ node nào cũng có thể là coordinator. Đây là kiến trúc peer-to-peer (masterless), khác với master-slave.

2.3 Data Partitioning — Consistent Hashing

Chi tiết thuật toán: Tuan-10-Consistent-Hashing

flowchart LR
    subgraph "Consistent Hash Ring"
        direction TB
        R["Hash Ring<br/>0 ────── 2^128"]

        A["Node A<br/>pos: 30, 120, 250"]
        B["Node B<br/>pos: 70, 180, 310"]
        C["Node C<br/>pos: 100, 220, 380"]
    end

    K1["key1 → hash: 45<br/>→ Node B (70)"]
    K2["key2 → hash: 130<br/>→ Node C (220... no, 180)<br/>→ Node B (180)"]
    K3["key3 → hash: 260<br/>→ Node B (310)"]

    style A fill:#e53935,color:#fff
    style B fill:#1e88e5,color:#fff
    style C fill:#43a047,color:#fff

Tại sao dùng Virtual Nodes (Vnodes)?

Mỗi physical node sở hữu nhiều vị trí trên hash ring (virtual nodes). Lợi ích:

  • Phân bổ đều tải: Tránh tình trạng 1 node nhận quá nhiều key
  • Thêm/bớt node mượt mà: Chỉ cần di chuyển một phần nhỏ data
  • Heterogeneous hardware: Node mạnh → nhiều vnodes hơn

Cassandra mặc định: 256 vnodes/node. Với 100 nodes → 25,600 vnodes trên ring.

2.4 Data Replication

Mỗi key được replicate lên N node liên tiếp trên ring (theo chiều kim đồng hồ).

flowchart LR
    subgraph "Replication with N=3"
        direction LR
        NA["Node A<br/>(Primary)"]
        NB["Node B<br/>(Replica 1)"]
        NC["Node C<br/>(Replica 2)"]
        ND["Node D"]
        NE["Node E"]
    end

    K["key: user:1001<br/>hash → Node A"]

    K --> NA
    NA -->|"Replicate"| NB
    NA -->|"Replicate"| NC

    style NA fill:#e53935,color:#fff,stroke-width:3px
    style NB fill:#ff7043,color:#fff
    style NC fill:#ff7043,color:#fff
    style ND fill:#bdbdbd,color:#000
    style NE fill:#bdbdbd,color:#000

Rack-aware replication: Đảm bảo N replicas nằm trên khác rack (hoặc khác data center) để survive rack/DC failure.

Với N=3, mỗi node có availability 99.9%:

Với replication factor N=3, khả năng mất data là 1 trong 1 tỷ. Đó là lý do N=3 là chuẩn industry.


Step 3 — Design Deep Dive

3.1 Consistency Models (Mô hình nhất quán)

ModelTiếng ViệtMô tảVí dụ
Strong ConsistencyNhất quán mạnhSau khi write thành công, mọi read đều thấy giá trị mới nhấtBanking balance
Eventual ConsistencyNhất quán cuối cùngSau khi write, read có thể thấy giá trị cũ trong một khoảng thời gian. Cuối cùng tất cả replicas sẽ convergeSocial media likes count
Causal ConsistencyNhất quán nhân quảNếu operation A xảy ra trước B (A causes B), thì mọi node đều thấy A trước B. Các operations không liên quan có thể thấy thứ tự khácChat messages trong cùng thread

3.2 Quorum Consensus — Tunable Consistency

Đây là cơ chế cho phép điều chỉnh mức consistency dựa trên 3 tham số:

Tham sốÝ nghĩa
NReplication factor — số replicas cho mỗi key
WWrite quorum — số replicas phải confirm write trước khi trả success
RRead quorum — số replicas phải respond trước khi trả data

Quy tắc vàng:

Vì nếu , tập hợp nodes đã write và tập hợp nodes đang read chắc chắn có giao (overlap). Ít nhất 1 node trong read quorum có data mới nhất.

Các cấu hình phổ biến

ConfigNWRW+RConsistencyĐặc điểm
Strong3224 > 3StrongBalanced read/write latency
Strong (write-heavy)3134 > 3StrongFast write, slow read
Strong (read-heavy)3314 > 3StrongSlow write, fast read
Eventual3112 < 3EventualFastest, lowest consistency
ONE quorum3112 < 3EventualDynamoDB default cho non-critical
flowchart TD
    subgraph "Write with W=2, N=3"
        Client1["Client: put(k, v)"]
        Coord1["Coordinator"]
        R1["Replica 1 ✅"]
        R2["Replica 2 ✅"]
        R3["Replica 3 ⏳ (slow)"]

        Client1 --> Coord1
        Coord1 --> R1
        Coord1 --> R2
        Coord1 --> R3
        R1 -->|"ACK"| Coord1
        R2 -->|"ACK"| Coord1
        Coord1 -->|"Success<br/>(W=2 reached)"| Client1
    end

    subgraph "Read with R=2, N=3"
        Client2["Client: get(k)"]
        Coord2["Coordinator"]
        R4["Replica 1: v2 ✅"]
        R5["Replica 2: v2 ✅"]
        R6["Replica 3: v1 (stale) ⏳"]

        Client2 --> Coord2
        Coord2 --> R4
        Coord2 --> R5
        Coord2 --> R6
        R4 -->|"v2"| Coord2
        R5 -->|"v2"| Coord2
        Coord2 -->|"Return v2<br/>(latest by timestamp)"| Client2
    end

    style R1 fill:#4caf50,color:#fff
    style R2 fill:#4caf50,color:#fff
    style R3 fill:#ff9800,color:#000
    style R4 fill:#4caf50,color:#fff
    style R5 fill:#4caf50,color:#fff
    style R6 fill:#ff9800,color:#000

Latency trade-off:

Khi tăng W → write chậm hơn nhưng read nhanh hơn (vì R có thể nhỏ). Và ngược lại.

3.3 Inconsistency Resolution — Xử lý xung đột

Vấn đề: Concurrent Writes

Khi 2 client cùng write vào 1 key trên 2 node khác nhau (do network partition hoặc concurrent access):

Timeline:
  Client A: put("cart", [iPhone])     → Node 1 lúc t=1
  Client B: put("cart", [MacBook])    → Node 2 lúc t=2

Node 1 có [iPhone], Node 2 có [MacBook]. Đâu là giá trị đúng?

Giải pháp 1: Last-Write-Wins (LWW)

Mỗi write đi kèm timestamp. Khi conflict, giá trị có timestamp lớn hơn thắng.

Ưu điểm: Đơn giản, dễ implement. Nhược điểm: Mất data! Trong ví dụ trên, hoặc iPhone hoặc MacBook sẽ bị mất. Cassandra dùng LWW làm default.

Giải pháp 2: Vector Clocks (Đồng hồ vector)

Vector clock là một danh sách các cặp (node, counter) gắn với mỗi value. Nó cho phép phát hiện:

  • Causally related writes: Nếu → B xảy ra sau A → B thắng
  • Concurrent writes: Nếu → conflict → cần client resolve

Ví dụ chi tiết:

Bước 1: Client A write lên Node Sx
  cart = [iPhone]
  VC = {Sx: 1}

Bước 2: Client A write tiếp lên Node Sx
  cart = [iPhone, AirPods]
  VC = {Sx: 2}

Bước 3: Client B read từ Sx, rồi write lên Node Sy
  cart = [iPhone, AirPods, MacBook]
  VC = {Sx: 2, Sy: 1}

Bước 4: Client C read từ Sx (chưa thấy update của B), write lên Node Sz
  cart = [iPhone, AirPods, iPad]
  VC = {Sx: 2, Sz: 1}

Bước 5: Client D read cả 2 versions:
  Version 1: [iPhone, AirPods, MacBook]  VC = {Sx: 2, Sy: 1}
  Version 2: [iPhone, AirPods, iPad]     VC = {Sx: 2, Sz: 1}

  → VC1 và VC2 là CONCURRENT (không ai "sau" ai)
  → Client D phải merge: [iPhone, AirPods, MacBook, iPad]
  → Write merged value: VC = {Sx: 2, Sy: 1, Sz: 1}

So sánh VC:

Nếu vs : Sy=1 vs Sy=0 (B thua), Sz=0 vs Sz=1 (A thua) → Concurrent → conflict.

Nhược điểm Vector Clock: Vector clock phình to theo số nodes đã tham gia write. Giải pháp: truncate entries cũ nhất khi vector vượt quá threshold (ví dụ: giữ tối đa 10 entries).

3.4 Handling Failures — Xử lý lỗi

3.4.1 Failure Detection — Gossip Protocol

Trong cluster 100 nodes, làm sao biết node nào chết?

Naive approach: Mỗi node ping tất cả node khác → messages → không scale.

Gossip Protocol (Epidemic Protocol):

  • Mỗi node duy trì một membership list (danh sách node + heartbeat counter)
  • Định kỳ (mỗi 1s), mỗi node chọn random một node khác và gửi membership list
  • Node nhận sẽ merge 2 list: giữ heartbeat counter cao hơn cho mỗi entry
  • Nếu heartbeat của 1 node không tăng sau T seconds → đánh dấu suspected failure
  • Nếu sau thêm T’ seconds vẫn không tăng → đánh dấu confirmed failure
sequenceDiagram
    participant A as Node A
    participant B as Node B
    participant C as Node C
    participant D as Node D

    Note over A,D: Gossip Round 1 (t=0s)

    A->>B: Membership: {A:10, B:8, C:9, D:7}
    B->>B: Merge: {A:10, B:9, C:9, D:7}<br/>(B tăng counter của chính mình)

    Note over A,D: Gossip Round 2 (t=1s)

    B->>C: Membership: {A:10, B:9, C:9, D:7}
    C->>C: Merge: {A:10, B:9, C:10, D:7}

    Note over A,D: Node D dies at t=2s

    Note over A,D: Gossip Round 3-5 (t=2-4s)

    C->>A: {A:10, B:9, C:10, D:7}
    A->>A: D's heartbeat still 7...<br/>T_fail = 5s, not reached yet

    Note over A,D: Gossip Round 8 (t=7s)

    A->>A: D's heartbeat = 7 for 5s<br/>→ SUSPECT D is down!

    A->>B: {A:15, B:12, C:14, D:7💀}
    B->>C: Propagate D's failure

Phi Accrual Failure Detector (Cassandra dùng cái này):

Thay vì binary “alive/dead”, dùng phi value (giá trị phi) — xác suất node đã chết dựa trên lịch sử heartbeat:

Trong đó là xác suất heartbeat đến muộn hơn khoảng thời gian hiện tại, dựa trên distribution lịch sử.

  • : Rất có thể node còn sống
  • : Gần như chắc chắn node đã chết (Cassandra default threshold)

Ưu điểm so với fixed timeout: Tự adapt theo network conditions. Mạng chậm → threshold tự nới rộng.

3.4.2 Temporary Failures — Sloppy Quorum & Hinted Handoff

Khi một node trong quorum tạm thời không thể trả lời (chưa chết hẳn, chỉ chậm hoặc đang restart):

Strict Quorum: Reject write → giảm availability.

Sloppy Quorum: Thay vì chờ node chết, gửi write cho node kế tiếp trên ring (node “thay thế”).

flowchart LR
    subgraph "Normal Operation (N=3)"
        W1["Write key X"]
        NA1["Node A ✅"]
        NB1["Node B ✅"]
        NC1["Node C ✅"]
        W1 --> NA1
        W1 --> NB1
        W1 --> NC1
    end

    subgraph "Node B is down → Sloppy Quorum"
        W2["Write key X"]
        NA2["Node A ✅"]
        NB2["Node B ❌ (down)"]
        NC2["Node C ✅"]
        ND2["Node D ✅<br/>(temporary holder)"]
        W2 --> NA2
        W2 -.->|"Failed"| NB2
        W2 --> NC2
        W2 -->|"Hinted Handoff"| ND2
    end

    subgraph "Node B recovers"
        ND3["Node D"]
        NB3["Node B ✅ (back!)"]
        ND3 -->|"Transfer hinted data<br/>back to B"| NB3
    end

    style NB2 fill:#e53935,color:#fff
    style ND2 fill:#ff9800,color:#000
    style NB3 fill:#4caf50,color:#fff

Hinted Handoff:

  1. Node D nhận data tạm và lưu kèm hint (metadata: “data này thuộc Node B”)
  2. Node D định kỳ check xem Node B đã recover chưa
  3. Khi Node B recover → Node D gửi data trả lại → xoá hint

Giới hạn: Nếu Node B chết quá lâu, hints trên Node D tích tụ → disk đầy. Cassandra giới hạn hint storage (default: 10GB hoặc 3 giờ).

3.4.3 Permanent Failures — Anti-Entropy with Merkle Trees

Khi một node chết vĩnh viễn và được thay thế bằng node mới, hoặc replicas bị drift (data khác nhau do hinted handoff không đầy đủ):

Vấn đề: Làm sao so sánh data giữa 2 replicas một cách hiệu quả? So sánh từng key → quá chậm.

Giải pháp: Merkle Tree (Hash Tree)

Merkle tree là cây mà:

  • Leaf nodes: hash của từng data block (hoặc range of keys)
  • Internal nodes: hash của con trái + con phải
  • Root: hash đại diện cho toàn bộ data
flowchart TD
    subgraph "Replica 1 Merkle Tree"
        R1["Root: abc123"]
        L1["Left: def456"]
        R1R["Right: ghi789"]
        LL1["Keys 1-25<br/>hash: aaa"]
        LR1["Keys 26-50<br/>hash: bbb"]
        RL1["Keys 51-75<br/>hash: ccc"]
        RR1["Keys 76-100<br/>hash: ddd"]

        R1 --> L1
        R1 --> R1R
        L1 --> LL1
        L1 --> LR1
        R1R --> RL1
        R1R --> RR1
    end

    subgraph "Replica 2 Merkle Tree"
        R2["Root: abc123... wait, xyz999!"]
        L2["Left: def456"]
        R2R["Right: DIFFERENT!"]
        LL2["Keys 1-25<br/>hash: aaa ✅"]
        LR2["Keys 26-50<br/>hash: bbb ✅"]
        RL2["Keys 51-75<br/>hash: DIFFERS ❌"]
        RR2["Keys 76-100<br/>hash: ddd ✅"]

        R2 --> L2
        R2 --> R2R
        L2 --> LL2
        L2 --> LR2
        R2R --> RL2
        R2R --> RR2
    end

    style R1 fill:#4caf50,color:#fff
    style R2 fill:#e53935,color:#fff
    style RL2 fill:#e53935,color:#fff
    style R2R fill:#e53935,color:#fff

Quy trình Anti-Entropy:

  1. So sánh root hash → Nếu giống → 2 replicas đồng bộ, xong!
  2. Nếu root khác → so sánh children: Left giống, Right khác
  3. Drill down Right subtree: Keys 51-75 khác, Keys 76-100 giống
  4. Chỉ cần sync Keys 51-75 → giảm lượng data transfer cực lớn

Với 1 triệu keys, brute force so sánh ~10 GB. Merkle tree so sánh ~20 hashes (mỗi cái 32 bytes) = 640 bytes. Hiệu quả gấp hàng triệu lần.

3.4.4 Data Center Outage — Cross-DC Replication

Khi cả một data center sập (thiên tai, mất điện toàn bộ):

flowchart TB
    Client["Client"]

    subgraph DC1["Data Center 1 (US-East)"]
        N1A["Node A1"]
        N1B["Node B1"]
        N1C["Node C1"]
    end

    subgraph DC2["Data Center 2 (US-West)"]
        N2A["Node A2"]
        N2B["Node B2"]
        N2C["Node C2"]
    end

    subgraph DC3["Data Center 3 (EU-West)"]
        N3A["Node A3"]
        N3B["Node B3"]
        N3C["Node C3"]
    end

    Client --> DC1
    Client -.->|"Failover"| DC2

    N1A <-->|"Async Replication<br/>~100-150ms"| N2A
    N1A <-->|"Async Replication<br/>~80-120ms"| N3A
    N2A <-->|"Async Replication"| N3A

    style DC1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style DC2 fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px
    style DC3 fill:#fce4ec,stroke:#e53935,stroke-width:2px

Cấu hình Cassandra multi-DC (ví dụ):

  • NetworkTopologyStrategy với replication factor: {DC1: 3, DC2: 3, DC3: 3}
  • Tổng 9 replicas cho mỗi key → data cực kỳ durable
  • Write consistency: LOCAL_QUORUM (chỉ cần quorum trong local DC) → low latency
  • Read consistency: LOCAL_QUORUM (đọc từ local DC)
  • Cross-DC replication là async → không ảnh hưởng write latency

3.5 Write Path — Luồng ghi chi tiết

flowchart TD
    C["Client: put(key, value)"]
    Coord["Coordinator Node"]
    CL["1. Commit Log<br/>(append-only on disk)<br/>→ Durability guarantee"]
    MT["2. Memtable<br/>(in-memory sorted structure)<br/>→ Red-Black tree / Skip list"]
    FL["Memtable full?"]
    SS["3. Flush to SSTable<br/>(Sorted String Table)<br/>→ Immutable file on disk"]
    CP["4. Compaction<br/>(merge multiple SSTables)"]

    C --> Coord
    Coord -->|"Route to responsible nodes"| CL
    CL --> MT
    MT --> FL
    FL -->|"Yes (size > threshold)"| SS
    FL -->|"No"| DONE["ACK to coordinator"]
    SS --> CP
    CP --> DONE2["Background process"]

    style CL fill:#ff9800,color:#000
    style MT fill:#ffeb3b,color:#000
    style SS fill:#78909c,color:#fff
    style CP fill:#5c6bc0,color:#fff

Chi tiết từng bước:

Step 1 — Commit Log (WAL — Write-Ahead Log):

  • Ghi vào file append-only trên disk (sequential write → cực nhanh)
  • Mục đích: Nếu node crash trước khi memtable flush xuống disk → recover từ commit log
  • Tương tự WAL trong PostgreSQL → Tuan-07-Database-Sharding-Replication

Step 2 — Memtable:

  • Cấu trúc dữ liệu in-memory, sorted by key (Red-Black tree, Skip list, hoặc B-tree)
  • Write vào memtable = write vào memory → cực nhanh (microseconds)
  • Đây chính là lý do write latency của KV store rất thấp

Step 3 — Flush to SSTable:

  • Khi memtable đạt threshold (ví dụ: 64MB) → flush xuống disk thành SSTable
  • SSTable (Sorted String Table): file immutable, data sorted by key
  • Mỗi SSTable đi kèm: index file + bloom filter

Step 4 — Compaction:

  • Theo thời gian, nhiều SSTables tích tụ → cần merge (compaction)
  • Compaction: merge nhiều SSTables thành 1, loại bỏ deleted keys (tombstones) và old versions

Toàn bộ kiến trúc này gọi là LSM Tree (Log-Structured Merge Tree). Đây là nền tảng storage engine của Cassandra, RocksDB, LevelDB, HBase.

3.6 Read Path — Luồng đọc chi tiết

flowchart TD
    C["Client: get(key)"]
    Coord["Coordinator Node"]
    MT2["1. Check Memtable<br/>(in-memory)"]
    Found1{"Found?"}
    BF["2. Check Bloom Filter<br/>for each SSTable"]
    BFR{"Bloom Filter<br/>says 'maybe'?"}
    IDX["3. Check SSTable Index<br/>(sparse index)"]
    SS2["4. Read from SSTable<br/>(disk read)"]
    Merge["5. Merge results<br/>(latest timestamp wins)"]
    Return["Return to client"]

    C --> Coord
    Coord --> MT2
    MT2 --> Found1
    Found1 -->|"Yes"| Return
    Found1 -->|"No"| BF
    BF --> BFR
    BFR -->|"No (definitely not here)"| NextSS["Try next SSTable"]
    BFR -->|"Yes (might be here)"| IDX
    IDX --> SS2
    SS2 --> Merge
    NextSS --> BF
    Merge --> Return

    style MT2 fill:#ffeb3b,color:#000
    style BF fill:#ab47bc,color:#fff
    style SS2 fill:#78909c,color:#fff

Bloom Filter — Tại sao cần?

Bloom filter là cấu trúc dữ liệu xác suất:

  • “Key không có”100% chắc chắn không có (no false negatives)
  • “Key có thể có” → có thể sai (false positives, tỷ lệ ~1%)
  • Kích thước: vài KB cho hàng triệu keys

Trong đó: = số hash functions, = số elements, = số bits.

Không có bloom filter: phải check mọi SSTable trên disk cho mỗi read (có thể hàng chục file). Với bloom filter: skip ngay các SSTables chắc chắn không chứa key → giảm disk I/O dramatically.

Read Repair: Khi coordinator nhận response từ R replicas và phát hiện data không đồng nhất → gửi bản mới nhất cho replicas có data cũ → self-healing.

3.7 Compaction Strategies

Compaction là quá trình merge SSTables, loại bỏ tombstones và duplicate keys. Có 2 chiến lược chính:

Size-Tiered Compaction (STCS)

Level 0: [SST1 4MB] [SST2 4MB] [SST3 4MB] [SST4 4MB]
         ↓ merge khi có đủ 4 SSTables cùng size
Level 1: [SST5 16MB] [SST6 16MB] [SST7 16MB] [SST8 16MB]
         ↓ merge
Level 2: [SST9 64MB] ...
ƯuNhược
Write throughput caoSpace amplification cao (cần 2x disk tạm thời khi compact)
Tốt cho write-heavy workloadRead chậm hơn (nhiều SSTables chồng chéo)

Leveled Compaction (LCS)

Level 0: [SST1] [SST2] (memtable flushes, có thể overlap)
Level 1: [a-d] [e-h] [i-m] [n-r] [s-z] (non-overlapping, mỗi SSTable ~160MB)
Level 2: [a-b] [c-d] [e-f] ... (10x larger, non-overlapping)
ƯuNhược
Read hiệu quả (mỗi key chỉ ở 1 SSTable per level)Write amplification cao (mỗi write có thể trigger cascade compaction)
Space amplification thấp (~10%)Write throughput thấp hơn STCS

Cassandra default: STCS cho write-heavy, LCS cho read-heavy tables.

3.8 Gossip Protocol for Cluster Membership

flowchart TD
    subgraph "Gossip Protocol — Cluster State"
        N1["Node 1<br/>State: {<br/>  N1: gen5/hb100,<br/>  N2: gen3/hb98,<br/>  N3: gen2/hb99,<br/>  N4: gen4/hb95<br/>}"]

        N2["Node 2<br/>State: {<br/>  N1: gen5/hb97,<br/>  N2: gen3/hb101,<br/>  N3: gen2/hb99,<br/>  N4: gen4/hb90<br/>}"]

        N3["Node 3"]
        N4["Node 4"]

        N1 -->|"1. Random pick:<br/>gossip to N2"| N2
        N2 -->|"2. Merge states:<br/>keep max heartbeat"| N2
        N2 -->|"3. Send merged<br/>state back"| N1
        N1 -->|"4. Merge again"| N1

        N2 -.->|"Next round:<br/>random pick N3"| N3
        N3 -.->|"Next round:<br/>random pick N4"| N4
    end

    style N1 fill:#42a5f5,color:#fff
    style N2 fill:#66bb6a,color:#fff
    style N3 fill:#ffa726,color:#000
    style N4 fill:#ef5350,color:#fff

Convergence time — bao lâu để toàn cluster biết 1 node chết?

Với 100 nodes, gossip interval = 1s:

Mất khoảng 7 giây để 100% cluster biết node X chết. Nhưng detector threshold (phi accrual) có thể cần thêm 5-10s nữa → tổng ~15-20s.

Thông tin gossip truyền tải:

  • Membership list: node nào đang sống/chết
  • Token ownership: node nào sở hữu range nào trên hash ring
  • Schema version: cluster schema có đồng nhất không
  • Load information: giúp routing thông minh hơn

3.9 Tổng hợp — Complete Architecture

flowchart TB
    Client["Client App"]
    LB["Load Balancer / Client-side routing"]

    Client --> LB

    subgraph Cluster["Distributed KV Store Cluster"]
        direction TB

        subgraph GossipLayer["Gossip Layer (Cluster Membership)"]
            G1["Gossip Protocol"]
            G2["Phi Accrual Failure Detector"]
            G3["Token Ring Manager"]
        end

        subgraph CoordLayer["Coordinator Layer"]
            C1["Request Router<br/>(Consistent Hash)"]
            C2["Quorum Manager<br/>(W/R/N config)"]
            C3["Conflict Resolver<br/>(Vector Clock / LWW)"]
        end

        subgraph StorageLayer["Storage Engine (per node)"]
            CL["Commit Log<br/>(WAL)"]
            MT["Memtable<br/>(in-memory)"]
            BF["Bloom Filters"]
            SST["SSTables<br/>(on disk)"]
            COMP["Compaction<br/>Engine"]
            MK["Merkle Tree<br/>(anti-entropy)"]
        end

        subgraph RepairLayer["Repair & Sync"]
            HH["Hinted Handoff<br/>Queue"]
            RR["Read Repair"]
            AE["Anti-Entropy<br/>Repair"]
        end
    end

    LB --> C1
    C1 --> C2
    C2 --> CL
    CL --> MT
    MT --> SST
    BF --> SST
    COMP --> SST
    G1 --> G2
    G2 --> C1
    G3 --> C1
    C3 --> C2
    HH --> StorageLayer
    RR --> StorageLayer
    AE --> MK

    style GossipLayer fill:#e8eaf6,stroke:#3f51b5
    style CoordLayer fill:#fff3e0,stroke:#ff9800
    style StorageLayer fill:#e8f5e9,stroke:#4caf50
    style RepairLayer fill:#fce4ec,stroke:#e53935

Step 4 — Wrap Up

Tổng kết các trade-offs

Design DecisionOption AOption BKV Store thường chọn
Consistency vs AvailabilityCP (reject when partition)AP (accept, resolve later)AP (tunable)
Conflict resolutionLWW (simple, data loss)Vector Clock (complex, no loss)LWW (Cassandra) hoặc VC (Riak, DynamoDB)
CompactionSize-tiered (write-optimized)Leveled (read-optimized)Depends on workload
Failure detectionFixed timeoutPhi accrualPhi accrual (adaptive)
QuorumStrict (may block)Sloppy (always available)Sloppy cho availability
ReplicationSync (strong, slow)Async (fast, eventual)Async + tunable quorum

Tóm tắt components và vai trò

ComponentGiải quyết vấn đề gì
Consistent hashingData partitioning, automatic rebalancing
Replication (N replicas)Durability, availability
Quorum (W + R > N)Tunable consistency
Vector clocksConflict detection for concurrent writes
Gossip protocolDecentralized failure detection, cluster membership
Merkle treeEfficient data synchronization between replicas
Sloppy quorum + hinted handoffAvailability during temporary failures
LSM tree (commit log + memtable + SSTable)High write throughput
Bloom filterFast read path (avoid unnecessary disk reads)
CompactionReclaim space, remove tombstones, merge data

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

FeatureCassandraDynamoDBetcdRedis Cluster
CAPAPAPCPAP/CP
ConsistencyTunableTunableStrong (Raft)Eventual
PartitioningConsistent hash (Murmur3)Consistent hashRaft groupsHash slots (16384)
ReplicationConfigurable N3 (fixed)Raft logAsync master-slave
Conflict resolutionLWWVector clocksRaft consensusLWW
Storage engineLSM treeB-tree (internal)BoltDB (B+tree)In-memory + RDB/AOF
Use caseTime-series, IoT, messagingShopping cart, session, gamingConfig, service discoveryCache, session, leaderboard

5. Security — Bảo mật cho Distributed KV Store

5.1 Encryption at Rest (Mã hoá dữ liệu trên disk)

Mỗi node lưu data trên disk (SSTables, commit log) → cần mã hoá.

LayerPhương phápƯu/Nhược
Disk-level (dm-crypt, LUKS)Encrypt toàn bộ partitionTransparent, nhưng ai có access vào OS đều đọc được
File-level (SSTable encryption)Mỗi SSTable encrypted riêngGranular control, nhưng thêm CPU overhead
Field-levelEncrypt từng value trước khi writeClient-side, KV store không thấy plaintext, nhưng không thể search/filter

So với write latency ~1ms (disk flush), encryption overhead chỉ thêm 0.5%. Negligible.

Key Management cho N replicas:

Cần KMS (AWS KMS, HashiCorp Vault) để quản lý. Xem Tuan-15-Data-Security-Encryption.

5.2 Inter-Node TLS (Mã hoá giao tiếp giữa các node)

  • Gossip traffic: mang membership info, token ranges → nếu bị intercept, attacker biết topology
  • Replication traffic: mang actual data → nếu bị intercept, data leak
  • Mandatory: mTLS (mutual TLS) giữa mọi node trong cluster
# cassandra.yaml — inter-node encryption
server_encryption_options:
  internode_encryption: all          # encrypt tất cả traffic giữa nodes
  keystore: /etc/cassandra/keystore.jks
  keystore_password: ${KEYSTORE_PASS}
  truststore: /etc/cassandra/truststore.jks
  truststore_password: ${TRUSTSTORE_PASS}
  protocol: TLSv1.3
  cipher_suites:
    - TLS_AES_256_GCM_SHA384
    - TLS_CHACHA20_POLY1305_SHA256
  require_client_auth: true          # mTLS: node phải chứng minh identity

5.3 Client Authentication & Access Control

Cơ chếMô tả
Username/PasswordBasic auth, Cassandra hỗ trợ sẵn
Certificate-based (mTLS)Client cũng phải có certificate → mạnh hơn
RBAC (Role-Based Access Control)Phân quyền: read-only, read-write, admin
Keyspace-level ACLGiới hạn quyền theo keyspace (namespace)
-- Cassandra CQL: Tạo role và phân quyền
CREATE ROLE app_reader WITH PASSWORD = 'secure_pass' AND LOGIN = true;
CREATE ROLE app_writer WITH PASSWORD = 'secure_pass' AND LOGIN = true;
 
GRANT SELECT ON KEYSPACE user_data TO app_reader;
GRANT SELECT, MODIFY ON KEYSPACE user_data TO app_writer;
 
-- Audit: xem ai đang có quyền gì
LIST ALL PERMISSIONS OF app_writer;

5.4 Data Protection with Multiple Replicas

Rủi ro: Data được replicate trên N nodes → attack surface tăng N lần. Nếu 1 node bị compromise, attacker có full data.

Mitigation:

  • Mỗi node encrypt at rest với unique key (không share key giữa nodes)
  • Rack-aware placement: replicas trên khác rack → compromise 1 rack chưa đủ
  • Network segmentation: KV store cluster trong private VLAN, chỉ application layer truy cập được

6. DevOps — Vận hành Cassandra Cluster

6.1 Monitoring với nodetool

# Kiểm tra trạng thái cluster
nodetool status
# Output: UN (Up Normal), DN (Down Normal), UJ (Up Joining), etc.
 
# Xem token ownership của mỗi node
nodetool ring
 
# Kiểm tra compaction đang chạy
nodetool compactionstats
 
# Xem thông tin chi tiết 1 table
nodetool tablestats keyspace_name.table_name
 
# Xem gossip info
nodetool gossipinfo
 
# Kiểm tra thread pool usage
nodetool tpstats
 
# Trigger repair thủ công (anti-entropy)
nodetool repair keyspace_name --full

6.2 Repair Operations

Tại sao cần repair?

  • Hinted handoff có thể miss data (hint expired, node down quá lâu)
  • Read repair chỉ fix data được đọc, data không ai đọc vẫn inconsistent
  • Anti-entropy repair là cách duy nhất đảm bảo 100% data consistency
# Full repair: so sánh toàn bộ data giữa replicas
# WARNING: resource-intensive, chạy trong maintenance window
nodetool repair -full my_keyspace
 
# Incremental repair: chỉ repair data mới từ lần repair trước
# Nhanh hơn, chạy thường xuyên hơn (daily)
nodetool repair my_keyspace
 
# Sub-range repair: repair 1 token range cụ thể
nodetool repair -st 0 -et 1000000 my_keyspace

Rule of thumb: Chạy repair ít nhất 1 lần mỗi gc_grace_seconds (default 10 ngày). Nếu không → tombstones bị xoá trước khi propagate → zombie data (data đã xoá sống lại).

6.3 Compaction Monitoring

# prometheus-alerts.yml — Cassandra compaction alerts
groups:
  - name: cassandra_compaction
    rules:
      - alert: CompactionPending
        expr: cassandra_table_pending_compactions > 50
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.instance }}: {{ $value }} pending compactions"
          description: "Compaction falling behind. May cause read latency increase."
 
      - alert: CompactionThroughputLow
        expr: rate(cassandra_table_bytes_compacted_total[5m]) < 10485760  # < 10MB/s
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Compaction throughput below 10MB/s on {{ $labels.instance }}"
 
      - alert: TombstoneAccumulation
        expr: cassandra_table_tombstone_scanned_histogram{quantile="0.99"} > 1000
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "Too many tombstones scanned per read on {{ $labels.instance }}"

6.4 Prometheus JMX Metrics

Cassandra expose metrics qua JMX → dùng jmx_exporter để Prometheus scrape.

# jmx_exporter_config.yml
---
rules:
  # Read/Write latency per table
  - pattern: org.apache.cassandra.metrics<type=Table, keyspace=(\w+), scope=(\w+), name=(Read|Write)Latency><>(Mean|99thPercentile)
    name: cassandra_table_${3}_latency_${4}
    labels:
      keyspace: "$1"
      table: "$2"
 
  # Pending compactions
  - pattern: org.apache.cassandra.metrics<type=Table, keyspace=(\w+), scope=(\w+), name=PendingCompactions><>Value
    name: cassandra_table_pending_compactions
    labels:
      keyspace: "$1"
      table: "$2"
 
  # SSTable count per table
  - pattern: org.apache.cassandra.metrics<type=Table, keyspace=(\w+), scope=(\w+), name=LiveSSTableCount><>Value
    name: cassandra_table_live_sstable_count
    labels:
      keyspace: "$1"
      table: "$2"
 
  # Bloom filter false positive rate
  - pattern: org.apache.cassandra.metrics<type=Table, keyspace=(\w+), scope=(\w+), name=BloomFilterFalseRatio><>Value
    name: cassandra_table_bloom_filter_fp_ratio
    labels:
      keyspace: "$1"
      table: "$2"
 
  # Gossip active/pending
  - pattern: org.apache.cassandra.metrics<type=ThreadPools, path=internal, scope=Gossip.*
    name: cassandra_threadpool_gossip_$1

6.5 Grafana Dashboard Essentials

PanelPromQL QueryThreshold
Cluster Statuscount(cassandra_node_status == 1)Alert if < expected nodes
Read Latency p99cassandra_table_Read_latency_99thPercentile< 10ms
Write Latency p99cassandra_table_Write_latency_99thPercentile< 5ms
Pending Compactionscassandra_table_pending_compactionsWarning > 20, Critical > 100
SSTable Countcassandra_table_live_sstable_countWarning > 30 per table
Bloom Filter FP Ratecassandra_table_bloom_filter_fp_ratioWarning > 0.01 (1%)
Disk Usage %node_filesystem_avail_bytes / node_filesystem_size_bytesWarning < 40%, Critical < 20%
Gossip Healthcassandra_threadpool_gossip_pendingWarning > 0 for > 5m

6.6 Capacity Planning

"""
Capacity Planning Calculator for Distributed KV Store
Dùng để dự đoán khi nào cần thêm nodes
"""
 
def plan_capacity(
    current_data_tb: float,
    growth_rate_pct_monthly: float,
    replication_factor: int,
    disk_per_node_tb: float,
    max_disk_usage_pct: float = 0.50,  # Cassandra best practice
    current_nodes: int = 100,
    months_ahead: int = 12,
) -> dict:
    """Dự đoán capacity needs trong N tháng tới."""
    results = []
    data = current_data_tb
 
    for month in range(1, months_ahead + 1):
        data *= (1 + growth_rate_pct_monthly / 100)
        total_with_replication = data * replication_factor
        # Overhead: index, bloom filter, compaction temp space
        total_with_overhead = total_with_replication * 2.5
        usable_per_node = disk_per_node_tb * max_disk_usage_pct
        nodes_needed = int(total_with_overhead / usable_per_node) + 1
 
        results.append({
            "month": month,
            "raw_data_tb": round(data, 2),
            "total_storage_tb": round(total_with_overhead, 2),
            "nodes_needed": nodes_needed,
            "nodes_to_add": max(0, nodes_needed - current_nodes),
        })
 
    return results
 
# Ví dụ: data 10TB, tăng 10%/tháng, N=3, disk 1.5TB/node
plan = plan_capacity(
    current_data_tb=10,
    growth_rate_pct_monthly=10,
    replication_factor=3,
    disk_per_node_tb=1.5,
    current_nodes=100,
    months_ahead=12,
)
for p in plan:
    print(f"Month {p['month']:2d}: {p['raw_data_tb']:8.1f} TB raw, "
          f"{p['nodes_needed']:4d} nodes needed (+{p['nodes_to_add']})")

7. Code Examples

7.1 Python: Consistent Hashing Implementation

"""
Consistent Hashing with Virtual Nodes
Tham chiếu: [[Tuan-10-Consistent-Hashing]]
"""
 
import hashlib
from bisect import bisect_right, insort
from typing import Optional
 
 
class ConsistentHash:
    """
    Consistent hashing ring with virtual nodes.
 
    Mỗi physical node được map thành `num_vnodes` virtual nodes
    trên hash ring để phân bổ đều tải.
    """
 
    def __init__(self, num_vnodes: int = 150):
        self.num_vnodes = num_vnodes
        self.ring: list[int] = []             # sorted list of hash positions
        self.ring_map: dict[int, str] = {}    # hash position → physical node
        self.nodes: set[str] = set()
 
    def _hash(self, key: str) -> int:
        """MD5 hash → integer position trên ring."""
        return int(hashlib.md5(key.encode()).hexdigest(), 16)
 
    def add_node(self, node: str) -> list[int]:
        """
        Thêm physical node vào ring.
        Returns: list of vnode positions.
        """
        if node in self.nodes:
            return []
 
        self.nodes.add(node)
        positions = []
        for i in range(self.num_vnodes):
            vnode_key = f"{node}:vnode{i}"
            pos = self._hash(vnode_key)
            insort(self.ring, pos)       # insert giữ sorted order
            self.ring_map[pos] = node
            positions.append(pos)
        return positions
 
    def remove_node(self, node: str) -> None:
        """Xoá physical node khỏi ring (graceful decommission)."""
        if node not in self.nodes:
            return
 
        self.nodes.discard(node)
        self.ring = [pos for pos in self.ring if self.ring_map.get(pos) != node]
        self.ring_map = {pos: n for pos, n in self.ring_map.items() if n != node}
 
    def get_node(self, key: str) -> Optional[str]:
        """Tìm node chịu trách nhiệm cho key."""
        if not self.ring:
            return None
 
        h = self._hash(key)
        idx = bisect_right(self.ring, h)
        # Wrap around ring nếu hash > max position
        if idx == len(self.ring):
            idx = 0
        return self.ring_map[self.ring[idx]]
 
    def get_replica_nodes(self, key: str, n: int = 3) -> list[str]:
        """
        Tìm N node liên tiếp trên ring (cho replication).
        Đảm bảo N nodes là DISTINCT physical nodes.
        """
        if not self.ring or n > len(self.nodes):
            return list(self.nodes)
 
        h = self._hash(key)
        idx = bisect_right(self.ring, h)
 
        replicas = []
        seen_nodes = set()
        ring_len = len(self.ring)
 
        for i in range(ring_len):
            pos = self.ring[(idx + i) % ring_len]
            node = self.ring_map[pos]
            if node not in seen_nodes:
                replicas.append(node)
                seen_nodes.add(node)
                if len(replicas) == n:
                    break
 
        return replicas
 
 
# === Demo ===
if __name__ == "__main__":
    ch = ConsistentHash(num_vnodes=150)
 
    for node in ["node-A", "node-B", "node-C", "node-D", "node-E"]:
        ch.add_node(node)
 
    # Test key distribution
    distribution: dict[str, int] = {n: 0 for n in ch.nodes}
    for i in range(100_000):
        node = ch.get_node(f"key:{i}")
        distribution[node] += 1
 
    print("Key distribution across 5 nodes (100K keys):")
    for node, count in sorted(distribution.items()):
        bar = "#" * (count // 500)
        print(f"  {node}: {count:6d} ({count/1000:.1f}%) {bar}")
 
    # Test replication
    replicas = ch.get_replica_nodes("user:1001", n=3)
    print(f"\nReplicas for 'user:1001': {replicas}")
 
    # Test node removal (simulate decommission)
    print(f"\nRemoving node-C...")
    ch.remove_node("node-C")
    new_replicas = ch.get_replica_nodes("user:1001", n=3)
    print(f"New replicas for 'user:1001': {new_replicas}")

7.2 Python: Vector Clock Implementation

"""
Vector Clock — Conflict detection for concurrent writes.
Dùng để xác định: 2 versions là causally related hay concurrent?
"""
 
from __future__ import annotations
from dataclasses import dataclass, field
from copy import deepcopy
from enum import Enum
 
 
class Relation(Enum):
    BEFORE = "BEFORE"         # A happened before B
    AFTER = "AFTER"           # A happened after B
    CONCURRENT = "CONCURRENT" # Neither — conflict!
    EQUAL = "EQUAL"           # Same version
 
 
@dataclass
class VectorClock:
    """
    Vector clock: {node_id: counter} dictionary.
 
    Rules:
    - Khi node X thực hiện write: increment clock[X]
    - Khi node X nhận message từ Y: merge(clock_X, clock_Y), rồi increment clock[X]
    """
 
    clock: dict[str, int] = field(default_factory=dict)
 
    def increment(self, node_id: str) -> VectorClock:
        """Node thực hiện local event → tăng counter."""
        new_clock = deepcopy(self)
        new_clock.clock[node_id] = new_clock.clock.get(node_id, 0) + 1
        return new_clock
 
    def merge(self, other: VectorClock) -> VectorClock:
        """Merge 2 vector clocks: giữ max counter cho mỗi node."""
        merged = VectorClock()
        all_nodes = set(self.clock.keys()) | set(other.clock.keys())
        for node in all_nodes:
            merged.clock[node] = max(
                self.clock.get(node, 0),
                other.clock.get(node, 0),
            )
        return merged
 
    def compare(self, other: VectorClock) -> Relation:
        """
        So sánh 2 vector clocks:
        - BEFORE: self < other (self happened before other)
        - AFTER: self > other (self happened after other)
        - CONCURRENT: neither ≤ nor ≥ → CONFLICT
        - EQUAL: same
        """
        all_nodes = set(self.clock.keys()) | set(other.clock.keys())
 
        self_less = False
        other_less = False
 
        for node in all_nodes:
            s = self.clock.get(node, 0)
            o = other.clock.get(node, 0)
 
            if s < o:
                self_less = True
            elif s > o:
                other_less = True
 
        if self_less and other_less:
            return Relation.CONCURRENT
        elif self_less:
            return Relation.BEFORE
        elif other_less:
            return Relation.AFTER
        else:
            return Relation.EQUAL
 
    def __repr__(self) -> str:
        items = ", ".join(f"{k}:{v}" for k, v in sorted(self.clock.items()))
        return f"VC({items})"
 
 
@dataclass
class VersionedValue:
    """Key-value pair with vector clock for conflict detection."""
 
    key: str
    value: str
    vector_clock: VectorClock
 
    def __repr__(self) -> str:
        return f"({self.key}={self.value}, {self.vector_clock})"
 
 
# === Demo: Shopping Cart conflict scenario ===
if __name__ == "__main__":
    print("=== Shopping Cart Conflict Detection ===\n")
 
    # Step 1: Client A writes on node Sx
    vc1 = VectorClock().increment("Sx")
    v1 = VersionedValue("cart", "[iPhone]", vc1)
    print(f"Step 1 - Client A writes: {v1}")
 
    # Step 2: Client A writes again on node Sx
    vc2 = vc1.increment("Sx")
    v2 = VersionedValue("cart", "[iPhone, AirPods]", vc2)
    print(f"Step 2 - Client A writes: {v2}")
    print(f"  v1 vs v2: {vc1.compare(vc2)} → v2 supersedes v1")
 
    # Step 3: Client B reads v2, writes on node Sy
    vc3 = vc2.merge(VectorClock()).increment("Sy")
    v3 = VersionedValue("cart", "[iPhone, AirPods, MacBook]", vc3)
    print(f"\nStep 3 - Client B writes: {v3}")
 
    # Step 4: Client C reads v2 (hasn't seen v3!), writes on node Sz
    vc4 = vc2.merge(VectorClock()).increment("Sz")
    v4 = VersionedValue("cart", "[iPhone, AirPods, iPad]", vc4)
    print(f"Step 4 - Client C writes: {v4}")
 
    # Step 5: Detect conflict
    relation = vc3.compare(vc4)
    print(f"\nStep 5 - v3 vs v4: {relation}")
    print(f"  v3: {v3}")
    print(f"  v4: {v4}")
 
    if relation == Relation.CONCURRENT:
        # Client-side merge
        merged_vc = vc3.merge(vc4).increment("Sx")
        merged = VersionedValue("cart", "[iPhone, AirPods, MacBook, iPad]", merged_vc)
        print(f"\n  → Client resolves conflict by merging:")
        print(f"  → {merged}")

7.3 Python: Simple In-Memory KV Store with Replication

"""
Simple distributed KV Store simulator.
Demonstrates: consistent hashing, replication, quorum reads/writes.
"""
 
import time
import threading
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
 
 
@dataclass
class Entry:
    value: str
    timestamp: float
    is_tombstone: bool = False
 
 
class KVNode:
    """Simulates a single KV store node."""
 
    def __init__(self, node_id: str, latency_ms: float = 1.0):
        self.node_id = node_id
        self.latency_ms = latency_ms
        self.store: dict[str, Entry] = {}
        self.is_alive = True
        self._lock = threading.Lock()
 
    def put(self, key: str, value: str) -> bool:
        """Write key-value pair. Returns True if successful."""
        if not self.is_alive:
            raise ConnectionError(f"Node {self.node_id} is down")
 
        time.sleep(self.latency_ms / 1000)  # simulate latency
 
        with self._lock:
            self.store[key] = Entry(
                value=value,
                timestamp=time.time(),
                is_tombstone=False,
            )
        return True
 
    def get(self, key: str) -> Optional[Entry]:
        """Read value for key. Returns None if not found."""
        if not self.is_alive:
            raise ConnectionError(f"Node {self.node_id} is down")
 
        time.sleep(self.latency_ms / 1000)  # simulate latency
 
        with self._lock:
            entry = self.store.get(key)
            if entry and entry.is_tombstone:
                return None
            return entry
 
    def delete(self, key: str) -> bool:
        """Soft delete: write tombstone."""
        if not self.is_alive:
            raise ConnectionError(f"Node {self.node_id} is down")
 
        with self._lock:
            self.store[key] = Entry(
                value="",
                timestamp=time.time(),
                is_tombstone=True,
            )
        return True
 
 
class DistributedKVStore:
    """
    Coordinator that routes requests to nodes using consistent hashing.
    Supports tunable quorum (N, W, R).
    """
 
    def __init__(self, n: int = 3, w: int = 2, r: int = 2):
        self.N = n  # replication factor
        self.W = w  # write quorum
        self.R = r  # read quorum
        self.nodes: dict[str, KVNode] = {}
        self.hash_ring: Optional[object] = None  # ConsistentHash from 7.1
 
    def add_node(self, node: KVNode) -> None:
        self.nodes[node.node_id] = node
 
    def _get_replicas(self, key: str) -> list[KVNode]:
        """Get N replica nodes for a key (simplified: round-robin)."""
        node_ids = sorted(self.nodes.keys())
        # Simple hash to pick starting node
        start = hash(key) % len(node_ids)
        replicas = []
        for i in range(self.N):
            idx = (start + i) % len(node_ids)
            replicas.append(self.nodes[node_ids[idx]])
        return replicas
 
    def put(self, key: str, value: str) -> bool:
        """
        Quorum write: send to N replicas, wait for W ACKs.
        """
        replicas = self._get_replicas(key)
        acks = 0
        errors = []
 
        with ThreadPoolExecutor(max_workers=self.N) as executor:
            futures = {
                executor.submit(node.put, key, value): node
                for node in replicas
            }
 
            for future in as_completed(futures):
                node = futures[future]
                try:
                    if future.result():
                        acks += 1
                        if acks >= self.W:
                            return True  # Quorum reached!
                except ConnectionError as e:
                    errors.append(str(e))
 
        if acks >= self.W:
            return True
 
        raise Exception(
            f"Write quorum not met: {acks}/{self.W} ACKs. "
            f"Errors: {errors}"
        )
 
    def get(self, key: str) -> Optional[str]:
        """
        Quorum read: read from N replicas, wait for R responses,
        return value with latest timestamp.
        """
        replicas = self._get_replicas(key)
        responses: list[tuple[Entry, KVNode]] = []
        errors = []
 
        with ThreadPoolExecutor(max_workers=self.N) as executor:
            futures = {
                executor.submit(node.get, key): node
                for node in replicas
            }
 
            for future in as_completed(futures):
                node = futures[future]
                try:
                    entry = future.result()
                    if entry:
                        responses.append((entry, node))
                    if len(responses) >= self.R:
                        break
                except ConnectionError as e:
                    errors.append(str(e))
 
        if not responses:
            return None
 
        # Return value with latest timestamp (LWW)
        latest = max(responses, key=lambda x: x[0].timestamp)
        return latest[0].value
 
 
# === Demo ===
if __name__ == "__main__":
    # Create cluster with 5 nodes
    store = DistributedKVStore(n=3, w=2, r=2)
    for i in range(5):
        store.add_node(KVNode(f"node-{i}", latency_ms=0.5))
 
    # Write
    store.put("user:1001", '{"name": "Hieu", "role": "student"}')
    print("Written: user:1001")
 
    # Read
    value = store.get("user:1001")
    print(f"Read: user:1001 = {value}")
 
    # Simulate node failure
    store.nodes["node-0"].is_alive = False
    print("\nnode-0 is DOWN")
 
    # Should still work (sloppy quorum)
    value = store.get("user:1001")
    print(f"Read after failure: user:1001 = {value}")
 
    # Write still works (W=2, still have 4 alive nodes)
    store.put("user:1001", '{"name": "Hieu", "role": "engineer"}')
    print("Written updated value despite node-0 being down")

7.4 Python: Merkle Tree Comparison

"""
Merkle Tree — Efficient data comparison between replicas.
Used in anti-entropy repair to find which key ranges are out of sync.
"""
 
import hashlib
from dataclasses import dataclass
from typing import Optional
 
 
@dataclass
class MerkleNode:
    hash_value: str
    left: Optional["MerkleNode"] = None
    right: Optional["MerkleNode"] = None
    key_range: tuple[int, int] = (0, 0)  # (start, end) token range
    is_leaf: bool = False
 
 
class MerkleTree:
    """
    Merkle tree for a range of keys.
    Leaf nodes contain hash of actual data in that key range.
    Internal nodes contain hash(left.hash + right.hash).
    """
 
    def __init__(self, data: dict[int, str], token_range: tuple[int, int] = (0, 1024)):
        """
        Args:
            data: {token_position: value_hash} — sorted by token
            token_range: (min_token, max_token) cho node này
        """
        self.data = data
        self.token_range = token_range
        self.root = self._build(token_range[0], token_range[1])
 
    def _hash(self, content: str) -> str:
        return hashlib.sha256(content.encode()).hexdigest()[:16]
 
    def _build(self, start: int, end: int, depth: int = 0, max_depth: int = 4) -> MerkleNode:
        """Recursively build Merkle tree."""
        # Leaf node: hash all data in this range
        if depth >= max_depth or end - start <= 1:
            keys_in_range = {k: v for k, v in self.data.items() if start <= k < end}
            content = "".join(f"{k}:{v}" for k, v in sorted(keys_in_range.items()))
            return MerkleNode(
                hash_value=self._hash(content) if content else self._hash("empty"),
                key_range=(start, end),
                is_leaf=True,
            )
 
        mid = (start + end) // 2
        left = self._build(start, mid, depth + 1, max_depth)
        right = self._build(mid, end, depth + 1, max_depth)
 
        return MerkleNode(
            hash_value=self._hash(left.hash_value + right.hash_value),
            left=left,
            right=right,
            key_range=(start, end),
        )
 
    @staticmethod
    def find_differences(
        node1: Optional[MerkleNode],
        node2: Optional[MerkleNode],
    ) -> list[tuple[int, int]]:
        """
        Compare 2 Merkle trees and return list of key ranges that differ.
        This is the magic: O(log N) comparison instead of O(N).
        """
        if node1 is None or node2 is None:
            return [(0, 0)]
 
        # Hashes match → subtrees are identical, no need to go deeper!
        if node1.hash_value == node2.hash_value:
            return []
 
        # Leaf node with different hash → this range needs sync
        if node1.is_leaf or node2.is_leaf:
            return [node1.key_range]
 
        # Internal node: recurse into children
        diffs = []
        diffs.extend(MerkleTree.find_differences(node1.left, node2.left))
        diffs.extend(MerkleTree.find_differences(node1.right, node2.right))
        return diffs
 
 
# === Demo: Compare 2 replicas ===
if __name__ == "__main__":
    # Replica 1 data: {token_position: value_hash}
    replica1_data = {
        10: "hash_a", 50: "hash_b", 100: "hash_c", 200: "hash_d",
        300: "hash_e", 400: "hash_f", 500: "hash_g", 600: "hash_h",
        700: "hash_i", 800: "hash_j", 900: "hash_k", 1000: "hash_l",
    }
 
    # Replica 2: identical except tokens 200 and 700 have different values
    replica2_data = replica1_data.copy()
    replica2_data[200] = "hash_d_MODIFIED"    # Changed!
    replica2_data[700] = "hash_i_MODIFIED"    # Changed!
 
    tree1 = MerkleTree(replica1_data, (0, 1024))
    tree2 = MerkleTree(replica2_data, (0, 1024))
 
    print(f"Replica 1 root hash: {tree1.root.hash_value}")
    print(f"Replica 2 root hash: {tree2.root.hash_value}")
    print(f"Roots match: {tree1.root.hash_value == tree2.root.hash_value}")
 
    diffs = MerkleTree.find_differences(tree1.root, tree2.root)
    print(f"\nKey ranges that differ: {diffs}")
    print(f"Only need to sync {len(diffs)} ranges instead of checking all {len(replica1_data)} keys!")
 
    # In real system: chỉ transfer data trong các ranges khác biệt
    for start, end in diffs:
        keys_to_sync = [k for k in replica1_data if start <= k < end]
        print(f"  Range [{start}, {end}): sync keys {keys_to_sync}")

8. Mermaid Diagrams — Tổng hợp

8.1 Consistent Hashing Ring with Replication

flowchart TD
    subgraph "Consistent Hash Ring (N=3, 6 physical nodes)"
        direction LR

        K["key: 'user:1001'<br/>hash = 42"]

        Ring["
        0 ──── Node A (pos 30) ←── Primary
        │
        50 ─── Node B (pos 70) ←── Replica 1
        │
        100 ── Node C (pos 100) ←── Replica 2
        │
        150 ── Node D (pos 180)
        │
        200 ── Node E (pos 220)
        │
        250 ── Node F (pos 310)
        │
        ↩ wrap to 0
        "]

        K -->|"hash(key)=42 → next node clockwise"| Ring
    end

    Note["key hash=42 → gần Node B nhất (pos 70)<br/>Replicas: B (primary), C, D<br/>(3 distinct physical nodes clockwise)"]

    style K fill:#ff9800,color:#000
    style Note fill:#e8f5e9,color:#000

8.2 Write Path Detail

sequenceDiagram
    participant C as Client
    participant Co as Coordinator
    participant R1 as Replica 1 (Primary)
    participant R2 as Replica 2
    participant R3 as Replica 3

    C->>Co: put("user:1001", value)
    Co->>Co: hash("user:1001") → determine replicas

    par Send to all N=3 replicas
        Co->>R1: Write request
        Co->>R2: Write request
        Co->>R3: Write request
    end

    Note over R1: 1. Append to Commit Log
    Note over R1: 2. Write to Memtable
    R1-->>Co: ACK ✅

    Note over R2: 1. Append to Commit Log
    Note over R2: 2. Write to Memtable
    R2-->>Co: ACK ✅

    Co->>C: Success (W=2 quorum met)

    Note over R3: 1. Append to Commit Log
    Note over R3: 2. Write to Memtable
    R3-->>Co: ACK (late, but data saved)

    Note over R1,R3: Later: Memtable flush → SSTable → Compaction

8.3 Read Path Detail

sequenceDiagram
    participant C as Client
    participant Co as Coordinator
    participant R1 as Replica 1
    participant R2 as Replica 2
    participant R3 as Replica 3

    C->>Co: get("user:1001")
    Co->>Co: hash → determine replicas

    par Read from all N=3 replicas
        Co->>R1: Read request
        Co->>R2: Read request
        Co->>R3: Read request
    end

    Note over R1: Check Memtable → Found!
    R1-->>Co: value_v3 (ts=1003)

    Note over R2: Memtable miss → Bloom Filter<br/>→ SSTable → Found
    R2-->>Co: value_v3 (ts=1003)

    Co->>C: Return value_v3 (R=2 quorum met, latest ts)

    Note over R3: Slow response...
    R3-->>Co: value_v2 (ts=1002) — STALE!

    Note over Co: Read Repair: send v3 to R3
    Co->>R3: Write value_v3 (repair)

8.4 Gossip Protocol Visualization

flowchart TD
    subgraph "T=0s: Initial State"
        A0["A: {A:1, B:1, C:1, D:1, E:1}"]
        B0["B: {A:1, B:1, C:1, D:1, E:1}"]
        C0["C: {A:1, B:1, C:1, D:1, E:1}"]
        D0["D: {A:1, B:1, C:1, D:0, E:1}"]
        E0["E: DEAD 💀"]
    end

    subgraph "T=1s: Round 1"
        A1["A gossips to C"]
        B1["B gossips to D"]
        A1 -->|"A increments own counter"| A1r["A: {A:2, B:1, C:1, D:1, E:1}"]
    end

    subgraph "T=2s: Round 2"
        C1["C gossips to B<br/>B merges: E still at 1"]
        D1["D gossips to A<br/>A sees E still at 1"]
    end

    subgraph "T=5s: E heartbeat stale > 5s"
        AF["A: E suspected! phi > 8"]
        BF["B: E suspected!"]
        CF["C: E suspected!"]
        DF["D: E suspected!"]
    end

    A0 --> A1
    B0 --> B1
    A1r --> C1
    C1 --> AF

    style E0 fill:#e53935,color:#fff
    style AF fill:#ff9800,color:#000
    style BF fill:#ff9800,color:#000
    style CF fill:#ff9800,color:#000
    style DF fill:#ff9800,color:#000

9. Aha Moments — Khoảnh khắc “À ha!”

#1 — LSM Tree trade-off: Write nhanh vì chỉ cần append vào commit log + write vào memory. Cái giá phải trả: read có thể chậm (check memtable → bloom filter → nhiều SSTables) và write amplification do compaction. Không có storage engine nào tối ưu cả read lẫn write — đây là trade-off cốt lõi.

#2 — W + R > N không phải “silver bullet”: Ngay cả khi W + R > N, vẫn có thể đọc stale data nếu: (a) write chưa complete trên W nodes khi read xảy ra, (b) sloppy quorum gửi write cho node ngoài preference list. Strong consistency chỉ đúng khi mọi thứ hoạt động bình thường.

#3 — Gossip protocol = eventual consistency cho metadata: Cluster membership cũng là dạng distributed state. Gossip đảm bảo eventually consistent — tức là có khoảng thời gian 1 node nghĩ node X còn sống trong khi node Y đã biết X chết. Đây là nguồn gốc của nhiều edge case.

#4 — Tombstones là “technical debt” của KV store: Khi delete key, thực tế chỉ ghi tombstone (đánh dấu đã xoá). Tombstone phải tồn tại đủ lâu (gc_grace_seconds) để propagate tới mọi replica. Nếu compaction chạy trước khi repair → tombstone bị xoá → data “sống lại” (zombie resurrection). Đây là bug khó debug nhất trong Cassandra.

#5 — Vector clock giải quyết vấn đề mà timestamp không thể: Trong distributed system, clock skew giữa các node có thể lên tới hàng chục milliseconds. 2 writes cách nhau 5ms trên 2 node khác nhau — timestamp không thể xác định ai trước ai. Vector clock không phụ thuộc vào wall clock → chính xác hơn. Nhưng cái giá: phức tạp hơn, client phải handle conflict.

#6 — Merkle tree biến O(N) thành O(log N): So sánh 1 tỷ keys giữa 2 replicas bằng brute force = transfer 10TB. Bằng Merkle tree = compare vài chục hashes (~1KB). Đây là ví dụ tuyệt vời của “right data structure changes everything”.

#7 — Tất cả đều là trade-off: Distributed KV store là bài tập về trade-off engineering: consistency vs availability, write speed vs read speed, simplicity vs correctness, storage efficiency vs write throughput. Không có “best” design — chỉ có design phù hợp nhất với use case.


10. Common Pitfalls — Sai lầm thường gặp

Pitfall 1: Write Amplification bị đánh giá thấp

Sai: “12K writes/s, Cassandra dư sức.” Đúng: Mỗi client write → N replica writes → compaction rewrite → thực tế 30x–50x write amplification. Với LCS, mỗi byte data có thể bị viết lại 10–30 lần qua các levels compaction.

Phải tính write amplification khi chọn SSD (SSD có giới hạn write endurance - TBW).

Pitfall 2: Tombstone Accumulation — “Zombie Data”

Sai: Delete hàng triệu key rồi nghĩ data đã mất. Đúng: Tombstones tích tụ, gây:

  • Read chậm (phải scan qua tombstones)
  • Disk usage tăng
  • Nếu repair không chạy trong gc_grace_secondszombie resurrection: data đã xoá sống lại!

Giải pháp: Tránh mass delete. Dùng TTL thay vì explicit delete. Monitor tombstone ratio per read.

Pitfall 3: Gossip Protocol Convergence Time bị quên

Sai: “Node chết là cluster biết ngay.” Đúng: Gossip cần rounds để propagate. Với 100 nodes, 1s interval → 7-20 giây trước khi toàn cluster nhận ra. Trong khoảng thời gian đó, một số requests có thể bị route tới node chết → timeout → tăng latency.

Pitfall 4: Vector Clock Bloat

Sai: Dùng vector clock cho mọi key, mặc kệ size. Đúng: Vector clock có 1 entry per node that ever wrote to the key. Với 100 nodes, mỗi VC entry ~20 bytes → 2KB overhead per key. Với 1 tỷ keys → 2TB chỉ cho vector clocks!

Giải pháp:

  • Truncate VC khi quá dài (Dynamo giữ max 10 entries, xoá entry cũ nhất)
  • Chấp nhận risk: truncation có thể gây false concurrent detection

Pitfall 5: Sloppy Quorum tạo ảo giác consistency

Sai: “W=2, R=2, N=3 → W+R > N → strong consistency.” Đúng: Với sloppy quorum, W=2 writes có thể đi tới node D (thay thế node B chết). R=2 reads vẫn đọc từ A, B, C → không overlap → stale read!

Chỉ strict quorum mới đảm bảo overlap. Trade-off: strict quorum → giảm availability khi node down.

Pitfall 6: Hot Key / Hot Partition

Sai: “Consistent hashing phân bổ đều, không cần lo.” Đúng: Nếu 1 key cực hot (celebrity tweet, viral product), tất cả reads/writes tập trung vào N nodes chịu trách nhiệm key đó → overload N nodes, 97 nodes khác rảnh.

Giải pháp:

  • Client-side caching cho hot reads
  • Key splitting: hot_keyhot_key:shard_1, hot_key:shard_2, … (scatter reads)
  • Separate hot key handling layer

Pitfall 7: Compaction làm gián đoạn production traffic

Sai: “Compaction chạy background, không ảnh hưởng gì.” Đúng: Compaction tiêu tốn disk I/O, CPU, và temporary disk space. Khi compaction chạy nặng:

  • Read latency tăng vọt (disk I/O contention)
  • Disk usage đột ngột tăng 2x (old + new SSTables cùng tồn tại)
  • Nếu disk full → node crash → cascade failure

Giải pháp:

  • Giữ disk usage < 50% (headroom cho compaction)
  • Throttle compaction throughput trong peak hours
  • Monitor pending_compactions metric

11. Bài tập tự luyện

Bài 1: Tunable Consistency Scenarios

Cho cluster N=3. Xác định mức consistency cho mỗi config:

ConfigWRW+RStrong hay Eventual?Tại sao?
A11???
B22???
C31???
D13???
E21???

Bài 2: Vector Clock Comparison

Cho 3 vector clocks:

  • VC1 = {A:3, B:2, C:1}
  • VC2 = {A:3, B:3, C:1}
  • VC3 = {A:4, B:1, C:2}

Xác định:

  • VC1 vs VC2: BEFORE, AFTER, hay CONCURRENT?
  • VC1 vs VC3: BEFORE, AFTER, hay CONCURRENT?
  • VC2 vs VC3: BEFORE, AFTER, hay CONCURRENT?

Bài 3: Capacity Planning

Cluster hiện tại: 50 nodes, mỗi node 2TB disk, N=3, data 5TB raw. Data growth: 20%/tháng.

Tính:

  • Khi nào cluster hết disk? (giả sử max 50% usage cho compaction headroom)
  • Cần thêm bao nhiêu node sau 6 tháng?
  • Inter-node bandwidth nếu write QPS = 20K, avg value = 5KB?

Bài 4: Design Decision

Em đang thiết kế KV store cho 2 use case khác nhau. Chọn config phù hợp:

Use case A: Session store cho e-commerce (availability quan trọng hơn consistency)

  • CP hay AP?
  • N, W, R = ?
  • LWW hay Vector Clock?
  • STCS hay LCS?

Use case B: Distributed lock manager (consistency quan trọng hơn availability)

  • CP hay AP?
  • N, W, R = ?
  • Giải thích tại sao KV store có thể không phải lựa chọn tốt nhất cho case này

12. Quick Reference — Các con số cần nhớ

Cassandra Performance Benchmarks (single node, SSD)

OperationThroughputLatency (p99)
Write (1KB value)20,000–50,000 ops/s< 5ms
Read (1KB value, memtable hit)50,000–100,000 ops/s< 2ms
Read (1KB value, SSTable)10,000–30,000 ops/s< 10ms
Scan (100 rows)5,000–10,000 ops/s< 50ms

Key Formulas

FormulaÝ nghĩa
Strong consistency condition
Probability all N replicas fail
Gossip convergence time
Bloom filter false positive rate
Total write amplification

Typical Configurations

Use CaseNWRConsistency
General purpose322Strong
Write-heavy analytics311Eventual
Read-heavy cache331Strong (slow write, fast read)
Maximum availability311Eventual
Cross-DC3 per DCLOCAL_QUORUMLOCAL_QUORUMStrong within DC

Tham khảo


Capstone complete. Tuần này tổng hợp toàn bộ 19 tuần trước. Nếu em hiểu bài này, em đã có nền tảng vững chắc cho mọi System Design Interview.