Case Study: Design a Distributed Message Queue

“Message Queue là xương sống của mọi distributed system. Không hiểu cách nó hoạt động bên trong thì chỉ là người dùng — hiểu cách thiết kế nó thì mới là kiến trúc sư.”

Tags: system-design message-queue kafka distributed-log replication alex-xu-vol2 case-study Student: Hieu Source: Alex Xu — System Design Interview Volume 2, Chapter 4 Prerequisite: Tuan-01-Scale-From-Zero-To-Millions · Tuan-02-Back-of-the-envelope · Tuan-08-Message-Queue Liên quan: Tuan-07-Database-Sharding-Replication · Tuan-10-Consistent-Hashing · Tuan-13-Monitoring-Observability · Case-Design-Payment-System


1. Context & Why — Tại sao cần thiết kế Message Queue từ đầu?

1.1 Analogy: Hệ thống bưu điện quốc gia

tưởng tượng bạn được giao nhiệm vụ thiết kế hệ thống bưu điện quốc gia từ con số không. Không phải chỉ đặt một cái hộp thư ở góc phố — mà là toàn bộ hệ thống: từ lúc người gửi bỏ thư vào hộp, đến lúc người nhận cầm thư trên tay.

Hệ thống này phải giải quyết những vấn đề sau:

  1. Nhận thư (Ingestion): Hàng triệu người gửi thư mỗi ngày. Mỗi bưu cục phải nhận thư nhanh chóng, không được để người gửi phải xếp hàng quá lâu. Tương tự producer gửi message vào broker.

  2. Phân loại (Routing): Thư gửi đến Hà Nội phải đi Hà Nội, thư gửi Sài Gòn phải đi Sài Gòn. Không được nhầm. Tương tự việc partition message theo key — tất cả message của cùng một khách hàng phải vào cùng một partition để đảm bảo thứ tự.

  3. Lưu trữ (Storage): Trước khi giao, thư phải được lưu an toàn trong kho. Nếu kho bị cháy, thư phải có bản sao ở kho khác. Tương tự replication trong message queue — mỗi partition có nhiều replica để đảm bảo không mất dữ liệu.

  4. Giao hàng (Delivery): Người đưa thư phải giao đúng người, đúng địa chỉ. Nếu người nhận không có nhà, phải quay lại giao lần sau (retry). Nếu giao nhiều lần mà không được, chuyển vào kho thư chết (Dead Letter Queue). Tương tự consumer đọc message và xử lý.

  5. Đảm bảo không mất (Durability): Một lá thư mất = mất lòng tin của cả hệ thống. Tương tự delivery guarantee — at-least-once, exactly-once.

  6. Đảm bảo không trùng (Deduplication): Không được giao cùng một lá thư hai lần cho người nhận. Tương tự exactly-once semantics với idempotent producer.

  7. Theo dõi (Tracking): Người gửi muốn biết thư đã đến chưa. Tương tự offset tracking — consumer biết mình đã đọc đến đâu.

1.2 Bài toán thiết kế từ scratch

Trong Tuan-08-Message-Queue, bạn đã học cách sử dụng message queue (Kafka, RabbitMQ) trong system design. Bây giờ, chúng ta sẽ thiết kế một distributed message queue từ đầu — giống như Kafka, Pulsar, hay Redpanda.

Đây là bài toán khó vì nó kết hợp nhiều khái niệm:

Thách thứcGiải thích
High throughput1 triệu messages/giây — mỗi message phải được nhận, lưu, và giao
DurabilityKhông được mất message — dù broker bị crash
OrderingMessages trong cùng partition phải giữ đúng thứ tự
ScalabilityThêm broker, thêm partition khi traffic tăng
Fault toleranceBroker chết → hệ thống vẫn hoạt động bình thường
Low latencyProducer gửi → consumer nhận trong vài millisecond
Replay capabilityConsumer có thể đọc lại message từ bất kỳ thời điểm nào
Exactly-once deliveryTrong nhiều trường hợp, không được xử lý trùng

1.3 Tại sao không dùng có sẵn?

Câu hỏi hay: “Tại sao không cứ dùng Kafka?”

  • Interview: Đây là bài thiết kế — interviewer muốn thấy bạn hiểu tại sao Kafka thiết kế như vậy, không phải chỉ biết cách dùng.
  • Thực tế: Nhiều công ty lớn (LinkedIn, Uber, Confluent) đã xây dựng hoặc custom message queue riêng vì yêu cầu đặc thù.
  • Hiểu sâu: Khi bạn biết cách thiết kế message queue, bạn sẽ dùng nó tốt hơn — biết parameter nào quan trọng, biết khi nào dùng at-least-once vs exactly-once, biết cách tune performance.

Aha Moment: Bài này bổ sung cho Tuan-08-Message-Queue. Tuần 08 dạy bạn dùng MQ như một component. Bài này dạy bạn xây MQ từ scratch. Hai góc nhìn bổ sung cho nhau — người dùng vs kiến trúc sư.


2. Deep Dive — Alex Xu 4-Step Framework

Step 1: Requirements — Hiểu và giới hạn bài toán

2.1.1 Functional Requirements

Chức năngMô tả chi tiết
Producer APIProducer gửi message vào một topic cụ thể. Hỗ trợ batch gửi nhiều message cùng lúc
Consumer APIConsumer đọc message từ topic. Hỗ trợ pull-based model (consumer chủ động kéo data)
Topic managementTạo, xóa, liệt kê topics. Mỗi topic có nhiều partitions
Message retentionGiữ message trong một khoảng thời gian (7 ngày default) hoặc theo kích thước
Message replayConsumer có thể đọc lại message từ bất kỳ offset nào trong retention period
Consumer groupNhóm consumer cùng đọc một topic — mỗi partition chỉ gán cho 1 consumer trong group
Delivery semanticsHỗ trợ at-most-once, at-least-once (default), và exactly-once
Message orderingĐảm bảo ordering trong cùng một partition

2.1.2 Non-Functional Requirements

Yêu cầuMục tiêuLý do
Throughput1,000,000 messages/sec (write)Xử lý event streaming cho hệ thống lớn (e-commerce, fintech, IoT)
Latency< 10ms P99 (end-to-end)Producer gửi → consumer nhận phải nhanh
DurabilityKhông mất message khi có 2 broker failures đồng thờiDữ liệu là tài sản — mất message = mất tiền
Availability99.99% uptimeMessage queue là backbone — nó chết thì toàn hệ thống chết
ScalabilityScale horizontally bằng cách thêm brokerTraffic tăng → thêm máy, không cần thiết kế lại
Storage efficiencyLưu trữ hiệu quả trên diskMessage được giữ nhiều ngày — disk là bottleneck
Ordering guaranteeMessages trong cùng partition giữ đúng thứ tựNhiều use case yêu cầu strict ordering (payment events, order state changes)

2.1.3 Out of Scope

Để giữ bài tập trung, chúng ta không thiết kế:

  • Schema registry (Avro, Protobuf schema management)
  • Stream processing engine (Kafka Streams, Flink)
  • Multi-datacenter replication (MirrorMaker)
  • Tiered storage (hot/cold storage)

2.1.4 API Design

Producer API:

APIParametersMô tả
produce(topic, message, key?, partition?)topic: string, message: bytes, key: bytes (optional), partition: int (optional)Gửi message vào topic. Nếu có key → hash(key) chọn partition. Nếu có partition → gửi trực tiếp
produce_batch(topic, messages[])topic: string, messages: list of (key, value)Gửi nhiều message cùng lúc — giảm network overhead

Consumer API:

APIParametersMô tả
subscribe(topics[], group_id)topics: list of string, group_id: stringConsumer tham gia group và subscribe vào topics
poll(timeout)timeout: msKéo batch messages mới. Trả về list messages
commit(offsets)offsets: map<partition, offset>Commit offset — đánh dấu đã xử lý đến đâu
seek(partition, offset)partition: int, offset: longNhảy đến vị trí bất kỳ — dùng cho replay

Step 2: High-Level Design — Kiến trúc tổng quan

2.2.1 Các thành phần chính

flowchart TB
    subgraph Producers
        P1["Producer 1<br/>(Order Service)"]
        P2["Producer 2<br/>(Payment Service)"]
        P3["Producer 3<br/>(IoT Devices)"]
    end

    subgraph "Broker Cluster"
        B1["Broker 1<br/>Partitions: 0, 3, 6"]
        B2["Broker 2<br/>Partitions: 1, 4, 7"]
        B3["Broker 3<br/>Partitions: 2, 5, 8"]
    end

    subgraph "Metadata Store"
        MS["ZooKeeper / etcd<br/>- Broker registry<br/>- Topic config<br/>- Partition assignment<br/>- Consumer group state"]
    end

    subgraph "Coordinator"
        CO["Coordinator<br/>- Leader election<br/>- Partition assignment<br/>- Consumer rebalancing"]
    end

    subgraph Consumers
        subgraph "Consumer Group A (Order Processing)"
            C1["Consumer A1"]
            C2["Consumer A2"]
            C3["Consumer A3"]
        end
        subgraph "Consumer Group B (Analytics)"
            C4["Consumer B1"]
            C5["Consumer B2"]
        end
    end

    P1 & P2 & P3 --> B1 & B2 & B3
    B1 & B2 & B3 <--> MS
    CO <--> MS
    CO --> B1 & B2 & B3
    B1 & B2 & B3 --> C1 & C2 & C3
    B1 & B2 & B3 --> C4 & C5

    style B1 fill:#1e88e5,color:#fff
    style B2 fill:#1e88e5,color:#fff
    style B3 fill:#1e88e5,color:#fff
    style MS fill:#f57c00,color:#fff
    style CO fill:#7b1fa2,color:#fff

2.2.2 Vai trò từng thành phần

Thành phầnVai tròChi tiết
ProducerGửi message vào brokerChọn partition (round-robin, key-based hash, custom). Batch và compress trước khi gửi
BrokerLưu trữ và phục vụ messageMỗi broker quản lý một số partitions. Ghi message vào disk (WAL). Phục vụ read cho consumer
Metadata StoreLưu metadata của clusterDanh sách broker, topic config, partition assignment, consumer group state. Dùng ZooKeeper hoặc etcd
CoordinatorĐiều phối clusterLeader election cho partition, consumer group rebalancing, broker membership management
ConsumerĐọc message từ brokerPull-based — consumer chủ động kéo data. Track offset để biết đọc đến đâu
Consumer GroupNhóm consumer chia nhau đọcMỗi partition chỉ gán cho 1 consumer trong group. Cho phép parallel processing

2.2.3 Message Flow tổng quan

sequenceDiagram
    participant P as Producer
    participant LB as Broker (Leader)
    participant F1 as Broker (Follower 1)
    participant F2 as Broker (Follower 2)
    participant C as Consumer

    P->>LB: 1. Send message batch
    LB->>LB: 2. Write to WAL (disk)
    LB->>F1: 3. Replicate to follower 1
    LB->>F2: 3. Replicate to follower 2
    F1-->>LB: 4. ACK replication
    F2-->>LB: 4. ACK replication
    LB-->>P: 5. ACK to producer (acks=all)

    C->>LB: 6. Poll(offset=100)
    LB->>LB: 7. Read from WAL/page cache
    LB-->>C: 8. Return messages [100-150]
    C->>C: 9. Process messages
    C->>LB: 10. Commit offset=150

Step 3: Deep Dive — Chi tiết từng thành phần

2.3.1 Data Model — Mô hình dữ liệu

Topic

Topic là logical channel để tổ chức messages. Tương tự như chuyên mục trong bưu điện — thư kinh doanh, thư cá nhân, thư quảng cáo.

Thuộc tínhMô tảVí dụ
NameTên topic — định danh duy nhấtorders, payments, user-events
Partition countSố partitions — quyết định parallelism12 partitions
Replication factorSố bản sao mỗi partition3 (1 leader + 2 followers)
RetentionThời gian giữ message7 ngày (default)
Cleanup policyCách dọn dẹp message cũdelete (xóa theo thời gian) hoặc compact (giữ latest per key)
Partition

Partition là đơn vị cơ bản của parallelism và ordering. Tương tự như nhiều ô bưu điện song song — mỗi ô xử lý độc lập.

Mỗi partition là một ordered, immutable sequence of messages. Messages được append vào cuối — không bao giờ sửa hay xóa (chỉ xóa khi retention hết hạn).

Đặc điểm quan trọng:

  • Messages trong cùng một partition có thứ tự đảm bảo (ordering guarantee)
  • Messages ở khác partition không có thứ tự với nhau
  • Mỗi partition có một leader và nhiều followers (replicas)
  • Chỉ leader xử lý read/write — followers chỉ replicate
Offset

Offset là số thứ tự của message trong partition. Bắt đầu từ 0, tăng dần. Tương tự số thứ tự của thư trong hộp thư — thư số 0, thư số 1, thư số 2…

Thông tinChi tiết
Kiểu dữ liệu64-bit integer
Phạm vi0 đến 2^63 - 1 (đủ cho 292 năm với 1M msg/sec)
Tính chấtMonotonically increasing trong partition
Mục đíchConsumer dùng offset để biết đọc đến đâu, có thể seek đến bất kỳ offset nào
Message (Record)

Mỗi message gồm các thành phần:

TrườngKiểuMô tả
Keybytes (nullable)Dùng để quyết định partition. Ví dụ: user_id, order_id. Nullable — nếu null thì round-robin
ValuebytesNội dung message. Có thể là JSON, Avro, Protobuf. Broker không care format — chỉ là bytes
Timestamplong (epoch ms)Thời điểm message được tạo (CreateTime) hoặc thời điểm broker nhận (LogAppendTime)
Headerslist of key-valueMetadata bổ sung. Ví dụ: trace-id, source-service, content-type
OffsetlongĐược broker gán khi message được ghi vào partition. Producer không set
PartitionintPartition mà message thuộc về
CRCintChecksum để phát hiện data corruption

Aha Moment: Broker không parse hay hiểu nội dung message. Với broker, message chỉ là một cục bytes. Điều này giúp broker cực kỳ nhanh — nó chỉ cần ghi bytes vào disk và đọc bytes từ disk. Không có serialization/deserialization overhead.


2.3.2 Broker Storage — Cách lưu trữ trên disk

Đây là phần quan trọng nhất của bài thiết kế. Storage design quyết định throughput và durability của toàn hệ thống.

Write-Ahead Log (WAL) per Partition

Mỗi partition được lưu trên disk như một append-only log. Tương tự như cuốn sổ ghi chép của bưu điện — chỉ ghi thêm, không bao giờ xóa hay sửa.

Tại sao append-only?

  • Disk sequential write cực nhanh: HDD sequential write đạt 200-300 MB/s (nhanh hơn random write 1000x). SSD sequential write đạt 500-3000 MB/s.
  • Không cần lock phức tạp: Chỉ ghi vào cuối → không có conflict
  • Tự nhiên hỗ trợ ordering: Message được ghi theo thứ tự → đọc ra cũng đúng thứ tự
Segment Files

Log của mỗi partition được chia thành nhiều segment files. Mỗi segment là một file trên disk.

Topic: orders, Partition: 0

/data/orders-0/
    00000000000000000000.log     ← Segment 0: offset 0 - 999,999
    00000000000000000000.index   ← Offset index cho segment 0
    00000000000000000000.timeindex ← Time index cho segment 0
    00000000000001000000.log     ← Segment 1: offset 1,000,000 - 1,999,999
    00000000000001000000.index
    00000000000001000000.timeindex
    00000000000002000000.log     ← Segment 2 (active segment - đang ghi)
    00000000000002000000.index
    00000000000002000000.timeindex

Tại sao chia segment?

Lý doGiải thích
Retention cleanupXóa segment cũ để giải phóng disk — xóa cả file, không cần scan từng message
CompactionNén segment cũ — giữ lại latest value per key
File system limitsMột file quá lớn → OS xử lý chậm. Segment nhỏ hơn dễ quản lý
RecoveryKhi broker restart, chỉ cần recover active segment — các segment cũ đã được flush

Cấu hình segment:

ConfigDefaultMô tả
segment.bytes1 GBKích thước tối đa mỗi segment
segment.ms7 ngàyThời gian tối đa trước khi roll segment mới
segment.index.bytes10 MBKích thước index file
Index Files — Tìm message nhanh

Khi consumer muốn đọc từ offset 1,500,000 — làm sao broker tìm đúng vị trí trên disk mà không cần scan từ đầu?

Offset Index: Map từ offset → vị trí (position) trong segment file.

Offset Index (sparse — không lưu mỗi offset):
Offset 1,000,000 → Position 0
Offset 1,000,100 → Position 15,200
Offset 1,000,200 → Position 30,800
Offset 1,000,300 → Position 46,100
...

Khi cần tìm offset 1,000,150:

  1. Binary search trong index → tìm entry gần nhất nhỏ hơn: offset 1,000,100 tại position 15,200
  2. Scan sequential từ position 15,200 đến khi gặp offset 1,000,150

Timestamp Index: Map từ timestamp → offset. Dùng khi consumer muốn đọc message từ một thời điểm cụ thể (ví dụ: “đọc lại tất cả message từ 2 ngày trước”).

Timestamp Index:
Timestamp 1710700000000 → Offset 1,000,000
Timestamp 1710700060000 → Offset 1,000,500
Timestamp 1710700120000 → Offset 1,001,200
...

Aha Moment: Index là sparse (không lưu mỗi offset, chỉ lưu mỗi N offset). Điều này tiết kiệm disk và memory. Trade-off: cần scan một đoạn nhỏ để tìm chính xác — nhưng đoạn này thường nằm trong page cache nên rất nhanh.

Memory-Mapped Files (mmap) và Page Cache

Đây là “bí quyết” giúp message queue đạt throughput cực cao:

Page Cache của OS: Khi broker ghi message vào file, data không ghi thẳng vào disk. Nó đi qua page cache của OS trước:

  1. Producer gửi message → broker ghi vào page cache (RAM)
  2. OS tự động flush page cache xuống disk (async)
  3. Consumer đọc message → đọc từ page cache (nếu data còn trong cache) → zero disk I/O

Kết quả: Trong trường hợp lý tưởng (consumer đọc message vừa mới được ghi), toàn bộ read xảy ra từ RAM — không chạm disk. Đây là lý do message queue có thể đạt millions of messages/sec trên hardware thường.

Memory-mapped files (mmap): Index files được map vào memory thông qua mmap. Điều này cho phép truy cập index như truy cập mảng trong RAM — không cần read() system call.

Kỹ thuậtÁp dụng vàoLợi ích
Page cacheSegment files (.log)Write nhanh (async flush), read nhanh (từ RAM)
mmapIndex files (.index, .timeindex)Truy cập index nhanh như RAM
sendfile() / zero-copyTransfer data từ disk → networkGiảm CPU usage, giảm copy giữa kernel và user space

Zero-copy transfer: Khi consumer đọc message, broker dùng sendfile() system call để chuyển data trực tiếp từ page cache → network socket, không cần copy qua user space. Giảm 2 lần copy và 2 lần context switch.

flowchart LR
    subgraph "Truyền thống (4 copies)"
        D1["Disk"] -->|"1. read()"| K1["Kernel Buffer"]
        K1 -->|"2. copy"| U1["User Buffer"]
        U1 -->|"3. write()"| K2["Socket Buffer"]
        K2 -->|"4. send"| N1["Network"]
    end

    subgraph "Zero-copy (2 copies)"
        D2["Disk/Page Cache"] -->|"1. sendfile()"| K3["Kernel Buffer"]
        K3 -->|"2. send"| N2["Network"]
    end

    style D2 fill:#43a047,color:#fff
    style K3 fill:#43a047,color:#fff
    style N2 fill:#43a047,color:#fff

2.3.3 Producer — Gửi message hiệu quả

Partitioning Strategy

Khi producer gửi message, nó phải chọn partition nào sẽ nhận message. Có 3 strategy:

1. Round-Robin (không có key):

  • Message được gửi luân phiên đến các partition: P0, P1, P2, P0, P1, P2…
  • Ưu điểm: Phân bố đều tải — không bị hotspot
  • Nhược điểm: Không đảm bảo ordering — messages của cùng entity có thể nằm ở khác partition
  • Dùng khi: Không cần ordering (ví dụ: log messages, metrics)

2. Key-based Hash (có key):

  • partition = hash(key) % num_partitions
  • Tất cả messages cùng key → luôn vào cùng partition
  • Ưu điểm: Đảm bảo ordering cho cùng key
  • Nhược điểm: Có thể bị hotspot nếu key phân bố không đều (ví dụ: celebrity user có nhiều event)
  • Dùng khi: Cần ordering per entity (ví dụ: key = order_id → tất cả event của 1 order theo đúng thứ tự)

3. Custom Partitioner:

  • Producer tự implement logic chọn partition
  • Dùng khi: Logic đặc biệt — ví dụ: high-priority messages vào partition riêng, geo-based routing

Pitfall: Khi thay đổi số partition (ví dụ: từ 6 lên 12), hash(key) % num_partitions sẽ thay đổi. Message của cùng key có thể đi vào partition khác. Đây là lý do partition count rất khó thay đổi sau khi đã chạy production. Chọn đúng từ đầu!

Batching

Producer không gửi từng message một. Nó gom nhiều messages thành 1 batch rồi gửi cùng lúc.

ConfigDefaultMô tả
batch.size16 KBKích thước tối đa của batch
linger.ms0 ms (gửi ngay)Thời gian chờ để gom thêm messages vào batch

Trade-off:

  • linger.ms = 0: Latency thấp nhất, nhưng throughput thấp (gửi từng message)
  • linger.ms = 5-50ms: Tăng throughput đáng kể (gom nhiều messages), nhưng latency tăng thêm vài ms
  • batch.size lớn: Ít network round-trip hơn, nhưng memory usage cao hơn
Compression

Batch được compress trước khi gửi qua network. Broker lưu batch đã compress — không decompress. Consumer mới decompress.

AlgorithmCompression ratioCPU usageTốc độKhi nào dùng
gzipCao nhất (~70-80%)CaoChậmBandwidth limited, batch lớn
snappyTrung bình (~50-60%)ThấpNhanhCần balance giữa compression và CPU
lz4Trung bình (~55-65%)Rất thấpRất nhanhDefault choice — tốt nhất cho đa số trường hợp
zstdCao (~65-75%)Trung bìnhNhanhMuốn compression tốt hơn lz4, chấp nhận thêm CPU

Aha Moment: Compression xảy ra ở batch level, không phải message level. Batch càng lớn → compression ratio càng tốt (vì có nhiều data giống nhau để compress). Đây là lý do tăng linger.msbatch.size giúp tiết kiệm bandwidth đáng kể.

Acknowledgment (acks)

Sau khi gửi message, producer chờ broker xác nhận (ACK). Mức ACK quyết định durability vs latency:

acksHành viDurabilityLatencyKhi nào dùng
0Producer không chờ ACK. “Fire and forget”Thấp nhất — có thể mất messageThấp nhấtMetrics, logs — chấp nhận mất
1Chờ ACK từ leader. Leader ghi xong → ACKTrung bình — mất nếu leader crash trước khi replicateTrung bìnhĐa số trường hợp
all (-1)Chờ ACK từ tất cả ISR (In-Sync Replicas). Leader + followers ghi xong → ACKCao nhất — chỉ mất khi tất cả ISR crash cùng lúcCao nhấtPayment events, critical data — không được mất

Pitfall: acks=all không có nghĩa là tất cả replicas. Nó chỉ đợi tất cả replicas trong ISR (In-Sync Replicas). Nếu ISR chỉ còn 1 (leader) vì followers lag → acks=all tương đương acks=1. Đây là lý do cần set min.insync.replicas=2.


2.3.4 Consumer — Đọc message hiệu quả

Consumer Group

Consumer group là cơ chế chia công việc giữa nhiều consumers. Tương tự nhóm nhân viên bưu điện — mỗi người phụ trách một khu vực, không ai làm trùng với ai.

Quy tắc vàng:

  • Trong 1 consumer group: mỗi partition chỉ gán cho đúng 1 consumer
  • 1 consumer có thể đọc nhiều partitions
  • Nếu num_consumers > num_partitions → consumer thừa sẽ idle
  • Nếu num_consumers < num_partitions → một consumer đọc nhiều partitions
  • Lý tưởng: num_consumers = num_partitions

Nhiều consumer groups đọc cùng topic độc lập với nhau:

Topic: orders (6 partitions)

Consumer Group "order-processing":
  Consumer 1 → Partition 0, 1
  Consumer 2 → Partition 2, 3
  Consumer 3 → Partition 4, 5

Consumer Group "analytics":
  Consumer A → Partition 0, 1, 2
  Consumer B → Partition 3, 4, 5

Consumer Group "audit":
  Consumer X → Partition 0, 1, 2, 3, 4, 5  (1 consumer đọc tất cả)

Mỗi group có offset riêng. Group “order-processing” có thể đã đọc đến offset 10,000 trong khi group “analytics” mới đọc đến offset 5,000.

Partition Assignment Strategies

Khi consumer join/leave group, partitions phải được re-assign. Có nhiều strategy:

1. Range Assignment:

  • Sắp xếp partitions theo số thứ tự, chia đều cho consumers
  • Ví dụ: 6 partitions, 3 consumers → C1: [P0,P1], C2: [P2,P3], C3: [P4,P5]
  • Ưu điểm: Đơn giản, deterministic
  • Nhược điểm: Consumer đầu tiên có thể bị nhiều partition hơn nếu chia không đều

2. Round-Robin Assignment:

  • Phân phối partition luân phiên: C1→P0, C2→P1, C3→P2, C1→P3, C2→P4, C3→P5
  • Ưu điểm: Phân bố đều hơn range
  • Nhược điểm: Khi rebalance, nhiều partition bị di chuyển

3. Sticky Assignment:

  • Giữ nguyên assignment cũ nhiều nhất có thể, chỉ di chuyển partition của consumer đã rời đi
  • Ưu điểm: Giảm disruption khi rebalance — consumer giữ lại state đã có
  • Nhược điểm: Phức tạp hơn để implement

Aha Moment: Sticky assignment là best practice trong production. Khi 1 consumer crash, chỉ partitions của nó bị reassign — các consumer khác giữ nguyên. Giảm thời gian “stop the world” đáng kể.

Offset Management

Consumer phải báo cho broker biết đã đọc đến offset nào. Quá trình này gọi là commit offset.

Auto Commit:

  • Consumer tự động commit offset sau mỗi auto.commit.interval.ms (default 5s)
  • Ưu điểm: Đơn giản, không cần code thêm
  • Nhược điểm: Có thể mất message hoặc xử lý trùng
    • Consumer đọc message, chưa xử lý xong → auto commit → consumer crash → message mất (at-most-once)
    • Consumer xử lý xong, chưa commit → crash → rebalance → consumer mới đọc lại (at-least-once với duplicates)

Manual Commit:

  • Consumer chỉ commit khi đã xử lý xong message
  • Synchronous commit: commitSync() — block cho đến khi commit thành công. An toàn nhưng chậm
  • Asynchronous commit: commitAsync() — không block. Nhanh nhưng có thể fail silently

Best practice: Dùng commitAsync() trong vòng lặp xử lý, và commitSync() trước khi consumer shutdown. Kết hợp cả hai để đạt balance giữa performance và safety.

Rebalancing Protocol

Khi consumer join/leave group, hệ thống phải rebalance — gán lại partitions cho consumers.

sequenceDiagram
    participant C1 as Consumer 1
    participant C2 as Consumer 2
    participant C3 as Consumer 3 (new)
    participant GC as Group Coordinator

    Note over C1,GC: Consumer 3 joins group
    C3->>GC: JoinGroup request
    GC->>GC: Trigger rebalance
    GC->>C1: Revoke partitions
    GC->>C2: Revoke partitions
    C1->>C1: Commit offsets, release partitions
    C2->>C2: Commit offsets, release partitions
    C1->>GC: JoinGroup (re-join)
    C2->>GC: JoinGroup (re-join)
    C3->>GC: JoinGroup
    GC->>C1: Assign: Consumer 1 is leader
    Note over C1: Leader computes assignment
    C1->>GC: SyncGroup (assignment plan)
    GC->>C1: SyncGroup response (P0, P1)
    GC->>C2: SyncGroup response (P2, P3)
    GC->>C3: SyncGroup response (P4, P5)
    Note over C1,C3: Resume consuming with new assignment

Eager Rebalancing (truyền thống):

  • Tất cả consumers dừng lại, trả partitions về, rồi nhận assignment mới
  • Vấn đề: “Stop the world” — trong thời gian rebalance, không consumer nào đọc được

Cooperative / Incremental Rebalancing (mới):

  • Chỉ revoke partitions cần di chuyển. Consumer giữ lại partitions không thay đổi
  • Ưu điểm: Giảm disruption đáng kể — đa số consumers vẫn hoạt động bình thường trong rebalance
  • Đây là best practice cho production systems

Pitfall: Rebalancing là thời điểm nguy hiểm nhất của consumer group. Trong khi rebalance, không ai xử lý messages → consumer lag tăng. Nếu rebalance xảy ra liên tục (ví dụ: consumer unstable, session timeout quá ngắn) → hệ thống không hoạt động được. Monitoring rebalance frequency là critical.


2.3.5 Replication — Đảm bảo dữ liệu không mất

Leader-Follower Model

Mỗi partition có 1 leader và nhiều followers. Chỉ leader nhận read/write. Followers chỉ replicate từ leader.

flowchart TB
    subgraph "Topic: orders, Partition 0"
        subgraph "Broker 1"
            L0["Partition 0<br/>LEADER<br/>Offset: 0-15,000"]
        end
        subgraph "Broker 2"
            F0a["Partition 0<br/>FOLLOWER (ISR)<br/>Offset: 0-14,998"]
        end
        subgraph "Broker 3"
            F0b["Partition 0<br/>FOLLOWER (ISR)<br/>Offset: 0-14,995"]
        end
    end

    subgraph "Topic: orders, Partition 1"
        subgraph "Broker 2 "
            L1["Partition 1<br/>LEADER<br/>Offset: 0-12,000"]
        end
        subgraph "Broker 3 "
            F1a["Partition 1<br/>FOLLOWER (ISR)<br/>Offset: 0-11,999"]
        end
        subgraph "Broker 1 "
            F1b["Partition 1<br/>FOLLOWER (ISR)<br/>Offset: 0-11,997"]
        end
    end

    L0 -->|"Replicate"| F0a
    L0 -->|"Replicate"| F0b
    L1 -->|"Replicate"| F1a
    L1 -->|"Replicate"| F1b

    style L0 fill:#e53935,color:#fff
    style L1 fill:#e53935,color:#fff
    style F0a fill:#1e88e5,color:#fff
    style F0b fill:#1e88e5,color:#fff
    style F1a fill:#1e88e5,color:#fff
    style F1b fill:#1e88e5,color:#fff

Tại sao leader phân bố trên nhiều brokers?

  • Partition 0 leader ở Broker 1, Partition 1 leader ở Broker 2…
  • Phân bố đều load — không broker nào bị overload
  • Nếu Broker 1 chết → chỉ leader của partitions trên Broker 1 cần failover
ISR (In-Sync Replicas)

ISR là tập hợp các replicas đang theo kịp leader. Một replica nằm trong ISR nếu:

  • Nó đã fetch dữ liệu từ leader và không bị lag quá nhiều
  • Cấu hình: replica.lag.time.max.ms (default 30s) — nếu follower không fetch trong 30s, bị loại khỏi ISR
Thuật ngữĐịnh nghĩa
ISRTập replicas đang sync với leader (bao gồm leader)
OSR (Out-of-Sync Replicas)Replicas bị lag — chưa theo kịp leader
LEO (Log End Offset)Offset của message cuối cùng trên mỗi replica
HW (High Watermark)Offset cao nhất mà tất cả ISR đã replicate. Consumer chỉ đọc được đến HW

High Watermark là gì?

Leader:    [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
Follower1: [0] [1] [2] [3] [4] [5] [6] [7]
Follower2: [0] [1] [2] [3] [4] [5] [6]
                                        ↑
                                   High Watermark = 6

Consumer chỉ đọc được message có offset 6 (High Watermark). Messages 7, 8, 9 đã ở leader nhưng chưa được tất cả ISR replicate → chưa visible cho consumer.

Tại sao? Nếu leader crash trước khi replicate 7, 8, 9 → follower lên làm leader mới → messages 7, 8, 9 mất. Nếu consumer đã đọc chúng → data inconsistency.

min.insync.replicas

Config này quyết định số ISR tối thiểu trước khi leader chấp nhận write.

Cấu hìnhHành vi
replication.factor = 3Mỗi partition có 3 replicas
min.insync.replicas = 2Cần ít nhất 2 replicas trong ISR để ghi được
acks = allProducer đợi tất cả ISR xác nhận

Kết hợp: acks=all + min.insync.replicas=2 + replication.factor=3

  • Producer gửi message → leader ghi + ít nhất 1 follower ghi → ACK
  • Có thể chịu 1 broker failure mà không mất data và không ngừng ghi
  • Nếu 2 brokers chết → ISR < min.insync.replicas → leader từ chối write → producer nhận error

Aha Moment: min.insync.replicas là “circuit breaker” của replication. Nó ngăn leader ghi khi không đủ replicas — tránh trường hợp ghi message vào leader rồi leader chết → mất data.

Unclean Leader Election

Khi leader chết và không có follower nào trong ISR (tất cả followers đều lag) — hệ thống có 2 lựa chọn:

Lựa chọnConfigHậu quả
Chờ đợi ISR follower onlineunclean.leader.election.enable = falsePartition không hoạt động cho đến khi ISR follower quay lại. Durability > Availability
Chọn follower ngoài ISR làm leaderunclean.leader.election.enable = truePartition hoạt động ngay, nhưng mất messages chưa được replicate. Availability > Durability

Trade-off kinh điển: Durability vs Availability

  • Payment system, financial data → false (không được mất data)
  • Metrics, logs → true (chấp nhận mất vài message, miễn là hệ thống không dừng)

2.3.6 Delivery Semantics — Đảm bảo giao message

Đây là một trong những khái niệm khó nhấthay bị hiểu sai nhất trong message queue.

At-Most-Once

Định nghĩa: Message được xử lý tối đa 1 lần. Có thể mất, nhưng không bao giờ trùng.

Cách implement:

  • Producer: acks = 0 (không chờ ACK)
  • Consumer: Commit offset trước khi xử lý message

Khi nào dùng: Metrics, logs, analytics — mất vài data point không ảnh hưởng kết quả.

Rủi ro: Message mất vĩnh viễn — producer không biết, consumer không biết.

At-Least-Once (Default)

Định nghĩa: Message được xử lý ít nhất 1 lần. Không mất, nhưng có thể trùng.

Cách implement:

  • Producer: acks = all + retry khi gửi thất bại
  • Consumer: Xử lý message trước, rồi mới commit offset

Khi nào dùng: Đa số trường hợp — notification, email, order processing (với idempotent consumer).

Rủi ro: Message có thể được xử lý 2 lần. Ví dụ: consumer xử lý xong, chưa commit offset, consumer crash → message được xử lý lại.

Giải pháp: Consumer phải idempotent — xử lý cùng message 2 lần vẫn cho kết quả giống nhau. Ví dụ: dùng unique ID của message để check “đã xử lý chưa” trước khi xử lý.

Exactly-Once

Định nghĩa: Message được xử lý chính xác 1 lần. Không mất, không trùng.

Cách implement (phức tạp nhất):

  1. Idempotent Producer:

    • Broker gán mỗi producer một Producer ID (PID)
    • Mỗi message có Sequence Number tăng dần per partition
    • Nếu broker nhận message với cùng PID + sequence number → tự động bỏ qua (deduplicate)
    • Giải quyết: producer retry gửi trùng → broker chỉ ghi 1 lần
  2. Transactional API:

    • Producer bắt đầu transaction → gửi messages vào nhiều partitions → commit hoặc abort
    • Consumer chỉ đọc messages của committed transactions (isolation.level = read_committed)
    • Giải quyết: atomic write across partitions — hoặc tất cả messages được ghi, hoặc không có gì
  3. Consumer-side deduplication:

    • Dù có idempotent producer và transactions, consumer vẫn cần xử lý idempotent
    • Lý do: “exactly-once” của broker chỉ đảm bảo ghi 1 lần. Consumer xử lý là logic riêng — có thể fail giữa chừng

Aha Moment: “Exactly-once” trong distributed systems là cực kỳ khó và thường là kết hợp của nhiều cơ chế: idempotent producer (deduplicate write) + transactions (atomic write) + idempotent consumer (deduplicate processing). Không có “magic button” nào làm exactly-once tự động.


2.3.7 Message Retention — Giữ message bao lâu?

Khác với traditional message queue (RabbitMQ: message bị xóa sau khi consumed), distributed message queue giữ message theo retention policy — bất kể đã được consume hay chưa.

Time-Based Retention
ConfigDefaultMô tả
retention.ms604,800,000 (7 ngày)Message cũ hơn 7 ngày bị xóa
retention.minutes-Tính theo phút
retention.hours168Tính theo giờ

Khi segment file cũ nhất có timestamp > retention → xóa cả segment file.

Size-Based Retention
ConfigDefaultMô tả
retention.bytes-1 (unlimited)Tổng kích thước tối đa per partition

Khi tổng kích thước các segments vượt quá retention.bytes → xóa segment cũ nhất.

Log Compaction — Giữ latest per key

Đây là retention policy đặc biệt. Thay vì xóa theo thời gian, broker giữ lại message mới nhất cho mỗi key và xóa các message cũ hơn.

TRƯỚC compaction:
Key=user1, Value=Hanoi     (offset 0)
Key=user2, Value=Saigon    (offset 1)
Key=user1, Value=Danang    (offset 2)   ← user1 cập nhật địa chỉ
Key=user3, Value=Hue       (offset 3)
Key=user2, Value=null      (offset 4)   ← user2 bị xóa (tombstone)
Key=user1, Value=Dalat     (offset 5)   ← user1 cập nhật lần nữa

SAU compaction:
Key=user3, Value=Hue       (offset 3)   ← giữ nguyên
Key=user2, Value=null      (offset 4)   ← tombstone (bị xóa sau)
Key=user1, Value=Dalat     (offset 5)   ← chỉ giữ latest

Dùng khi nào?

  • Database changelog (CDC): mỗi row là 1 key, value là state mới nhất
  • Configuration store: mỗi key là config name, value là config value
  • User profile updates: mỗi key là user_id, value là profile mới nhất

Pitfall: Log compaction không xảy ra ngay lập tức. Có một “dirty ratio” threshold. Compaction thread chạy background và có thể tạo I/O load lớn. Trong production, cần monitoring disk I/O trong lúc compaction.


2.3.8 Coordination — Quản lý cluster

ZooKeeper (truyền thống)

ZooKeeper là external coordination service được Kafka dùng từ đầu để:

Chức năngChi tiết
Broker registryMỗi broker đăng ký vào ZooKeeper khi start. ZK biết broker nào đang sống
Controller electionChọn 1 broker làm “Controller” — phụ trách leader election cho partitions
Topic/partition metadataLưu danh sách topics, partitions, replica assignment
Consumer group state(cũ) Lưu consumer offsets và group membership. Từ Kafka 0.9+, consumer offsets lưu trong internal topic __consumer_offsets

Vấn đề của ZooKeeper:

  • Thêm 1 dependency: ZooKeeper là hệ thống riêng, cần deploy và vận hành riêng biệt
  • Scaling bottleneck: ZK cluster thường chỉ 3-5 nodes. Metadata updates trở thành bottleneck khi cluster lớn (hàng nghìn partitions)
  • Split brain risk: Nếu ZK và broker cluster bị network partition → có thể có hai leaders cho cùng partition
  • Operational complexity: Phải biết vận hành 2 hệ thống: Kafka + ZooKeeper
KRaft (ZooKeeper-less) — Xu hướng mới

Từ Kafka 3.3+, KRaft thay thế ZooKeeper bằng internal Raft-based consensus:

Khía cạnhZooKeeperKRaft
ArchitectureExternal ZK clusterBuilt-in — metadata stored trong Kafka brokers
Metadata storageZK znodesInternal __cluster_metadata topic (Raft log)
Controller1 broker được ZK bầu làm controllerQuorum of controllers (3-5 brokers có role controller)
Failover time10-30 giây (ZK session timeout)1-3 giây (Raft election)
ScalingZK là bottleneckMetadata replicate qua Raft — scale tốt hơn
Operational2 clusters (Kafka + ZK)1 cluster (chỉ Kafka)

Controller và metadata management trong KRaft:

  • Một nhóm brokers có role controller (thường 3 hoặc 5)
  • Các controllers dùng Raft consensus để bầu leader và replicate metadata
  • Active controller xử lý tất cả metadata changes (tạo topic, leader election, v.v.)
  • Các controller khác là follower — sẵn sàng thay thế nếu active controller chết

Aha Moment: KRaft không chỉ đơn giản là “bỏ ZooKeeper”. Nó thay đổi cơ bản cách Kafka quản lý metadata — từ external coordination sang internal log-based coordination. Đây là xu hướng của distributed systems: từ ngoài vào trong, từ complexity sang simplicity.


2.3.9 Push vs Pull — Hai mô hình tiêu thụ

Khía cạnhPull (Kafka model)Push (RabbitMQ model)
Ai điều khiển tốc độ?Consumer — kéo data theo tốc độ của mìnhBroker — đẩy data xuống consumer
Consumer chậm?Không ảnh hưởng broker — data vẫn nằm trên diskBroker phải buffer hoặc drop messages
Consumer nhanh?Kéo data liên tục, không phải đợiBroker có thể không đẩy đủ nhanh
BatchingConsumer tự chọn batch size tối ưuBroker quyết định batch — có thể không phù hợp
Long pollingConsumer poll với timeout — nếu không có data mới, đợi một lúc rồi poll lạiKhông cần — broker đẩy ngay khi có data
Back-pressureTự nhiên — consumer chỉ kéo khi sẵn sàngCần cơ chế riêng (prefetch count, flow control)
ReplayDễ dàng — consumer chỉ cần seek offsetKhó — message đã bị xóa sau khi ACK
ComplexityConsumer phức tạp hơn (phải tự quản lý polling)Consumer đơn giản hơn (chỉ nhận và xử lý)

Tại sao thiết kế của chúng ta chọn Pull?

  1. Consumer tự quyết định tốc độ — không bị overwhelm
  2. Batching tối ưu — consumer kéo đúng lượng data phù hợp
  3. Replay dễ dàng — seek đến bất kỳ offset nào
  4. Back-pressure tự nhiên — không cần cơ chế đặc biệt
  5. Scale consumer độc lập — thêm consumer không ảnh hưởng broker

Nhược điểm của Pull: Khi không có data mới, consumer vẫn phải poll → lãng phí. Giải pháp: Long polling — consumer gửi poll request, broker giữ request cho đến khi có data mới hoặc timeout.


2.3.10 Dead Letter Queue (DLQ) — Xử lý “thư chết”

Vấn đề: Consumer nhận message nhưng không xử lý được. Ví dụ:

  • Message bị corrupt (bad format)
  • Business logic fail (invalid data)
  • Dependency bị lỗi (database down → retry nhiều lần vẫn fail)

Nếu cứ retry mãi → consumer bị stuck tại message này, không xử lý được messages phía sau. Đây gọi là poison message.

Giải pháp: Dead Letter Queue

Main Topic: orders
  ↓ (consumer đọc)
  Consumer: xử lý message
    ↓ (thành công) → commit offset, tiếp tục
    ↓ (thất bại lần 1) → retry
    ↓ (thất bại lần 2) → retry
    ↓ (thất bại lần 3) → chuyển vào DLQ

DLQ Topic: orders-dlq
  ↓ (ops team review, fix, re-publish)

Cách implement DLQ:

  • Consumer có max.retries (ví dụ: 3)
  • Sau khi retry hết → producer message vào DLQ topic (ví dụ: orders-dlq)
  • Commit offset của message gốc → consumer tiếp tục xử lý message tiếp theo
  • DLQ topic được monitor bởi ops team
  • Sau khi fix root cause, message trong DLQ được re-publish vào main topic
ConfigMô tả
Max retriesSố lần retry trước khi chuyển vào DLQ
Retry delayThời gian đợi giữa các lần retry (exponential backoff)
DLQ topic nameConvention: {original-topic}-dlq
DLQ retentionThường dài hơn main topic (30-90 ngày) để có thời gian investigate

Pitfall: Không có DLQ → poison message sẽ block consumer vĩnh viễn (nếu auto-commit tắt) hoặc bị mất (nếu auto-commit bật và consumer crash trước khi xử lý). Cả hai trường hợp đều nguy hiểm. Luôn thiết kế DLQ cho production systems.


2.3.11 Scaling — Mở rộng hệ thống

Thêm Broker

Khi cluster cần thêm capacity (disk, CPU, network):

  1. Start broker mới, join cluster
  2. Broker mới chưa có partition nào — nó là “empty”
  3. Admin chạy partition reassignment — di chuyển một số partitions từ brokers cũ sang broker mới
  4. Trong quá trình di chuyển, data được replicate từ broker cũ sang broker mới
  5. Khi replicate xong, broker mới nhận role (leader hoặc follower) và broker cũ giải phóng partition

Vấn đề: Partition reassignment tốn bandwidth và disk I/O. Trong production:

  • Chạy reassignment ngoài giờ cao điểm
  • Giới hạn tốc độ reassignment (throttle) để không ảnh hưởng traffic production
  • Monitor replication lag trong quá trình reassignment
Thêm Partition

Khi topic cần thêm parallelism (nhiều consumers hơn muốn đọc đồng thời):

  1. Admin tăng partition count của topic (ví dụ: từ 6 lên 12)
  2. Broker tạo partitions mới trên các brokers
  3. Consumer group rebalance — partitions được gán lại cho consumers
  4. Messages mới được phân phối vào cả partitions cũ và mới

Vấn đề nghiêm trọng:

  • Key ordering bị phá: hash(key) % 6 khác với hash(key) % 12. Messages của cùng key trước kia vào P0, bây giờ có thể vào P6. Ordering per key bị phá trong thời gian chuyển đổi.
  • Không thể giảm partition: Kafka không hỗ trợ giảm số partition. Một khi đã tăng, không quay lại được.

Pitfall: Partition count là quyết định khó thay đổi nhất trong message queue. Chọn quá ít → không scale được. Chọn quá nhiều → lãng phí resources (mỗi partition tốn memory, file handles, replication bandwidth). Rule of thumb: bắt đầu với partition count = 2-3x số consumers dự kiến, để room cho scale.


3. Estimation — Ước lượng hệ thống

3.1 Throughput Estimation

Assumptions:

Thông sốGiá trịGiải thích
Target throughput1,000,000 messages/secYêu cầu từ requirements
Average message size1 KBJSON event trung bình
Peak/Average ratio3xFlash sales, events
Replication factor31 leader + 2 followers

3.2 Storage Estimation

Thông sốGiá trị
Messages/day1,000,000 msg/sec x 86,400 sec = 86.4 billion messages
Data/day (raw)86.4B x 1 KB = 86.4 TB
Compression ratio~50% (lz4)
Data/day (compressed)86.4 TB x 0.5 = 43.2 TB
Replication factor3
Data/day (with replication)43.2 TB x 3 = 129.6 TB
Retention7 ngày

3.3 Network Bandwidth Estimation

Inbound (producer → broker):

Replication bandwidth (leader → followers):

Outbound (broker → consumers):

Giả sử có 3 consumer groups, mỗi group đọc toàn bộ data:

Total network per cluster:

Mỗi broker cần 10 Gbps NIC để có đủ headroom.

3.4 Consumer Throughput Estimation

Consumer processingTimeMessages/sec per consumer
Simple log/forward0.1 ms/msg10,000 msg/sec
Database write1 ms/msg1,000 msg/sec
Complex processing5 ms/msg200 msg/sec

Aha Moment: Số partitions phải >= số consumers. Nếu cần 1,000 consumers → cần ít nhất 1,000 partitions. Đây là lý do chọn partition count là quyết định quan trọng.

3.5 Tóm tắt Estimation

MetricGiá trị
Write throughput1 GB/sec (3 GB/sec peak)
Total write (with replication)3 GB/sec (9 GB/sec peak)
Storage (7-day retention, RF=3, compressed)~907 TB (~0.9 PB)
Number of brokers~19+ (storage-based)
Network per broker~2.5 Gbps (cần 10 Gbps NIC)
Consumers needed100 (simple) to 1,000 (DB write)
Partitions needed>= max consumer count

4. Security — Bảo mật message queue

4.1 Encryption — Mã hóa dữ liệu

4.1.1 Encryption in Transit (TLS)

Kết nốiTLSMô tả
Producer → BrokerTLS 1.2/1.3. Ngăn man-in-the-middle đọc messages
Broker → Broker (replication)Inter-broker replication cũng phải encrypt
Broker → ConsumerConsumer đọc messages qua TLS
Broker → ZooKeeper/ControllerMetadata communication phải secure

Lưu ý: TLS giảm throughput khoảng 10-30% do encryption/decryption overhead. Trong internal network (trusted), có thể cân nhắc dùng PLAINTEXT cho inter-broker replication để tăng performance — nhưng đây là trade-off.

4.1.2 Encryption at Rest

Phương phápMô tảKhi nào dùng
Disk encryption (dm-crypt, LUKS)Encrypt toàn bộ disk — transparent cho KafkaStandard cho mọi production deployment
File system encryptionEncrypt ở file system level (eCryptfs, fscrypt)Khi không có full disk encryption
Message-level encryptionProducer encrypt message body trước khi gửiKhi broker không được phép đọc nội dung (multi-tenant, sensitive data)

Message-level encryption: Producer encrypt bằng public key của consumer. Broker chỉ thấy ciphertext — không đọc được. Consumer decrypt bằng private key. Dùng cho:

  • Healthcare data (HIPAA)
  • Financial data (PCI-DSS)
  • Multi-tenant platforms (tenant A không đọc được data của tenant B, kể cả broker admin)

Pitfall: Message-level encryption làm log compaction không hoạt động (broker không đọc được key để so sánh). Cần cân nhắc trade-off giữa security và functionality.

4.2 Authentication — Xác thực danh tính

Phương phápMô tảKhi nào dùng
SASL/PLAINUsername/passwordDev/test environments. Không an toàn cho production (password đi plain text, cần dùng kèm TLS)
SASL/SCRAMChallenge-response (không gửi password)Production — an toàn hơn PLAIN
SASL/GSSAPI (Kerberos)Enterprise SSOEnterprise environments có Kerberos infrastructure
SASL/OAUTHBEAREROAuth 2.0 tokensCloud-native environments, integration với identity providers
mTLSMutual TLS — client và server đều present certificateZero-trust environments, service-to-service authentication

Best practice: Dùng mTLS cho service-to-service (producer/consumer là services) và SASL/SCRAM hoặc OAUTHBEARER cho human users (admin tools, monitoring).

4.3 Authorization — Phân quyền truy cập

ACL (Access Control List) per topic:

PrincipalResourceOperationPermission
order-serviceTopic: ordersWRITEALLOW
order-serviceTopic: paymentsWRITEDENY
analytics-serviceTopic: ordersREADALLOW
analytics-serviceTopic: ordersWRITEDENY
adminTopic: *ALLALLOW

Principle of least privilege: Mỗi service chỉ có quyền vừa đủ để làm việc của nó. Order service chỉ ghi vào orders topic, không được đọc payments topic.

4.4 Audit Logging

EventThông tin log
Topic created/deletedWho, when, topic name, config
ACL changedWho, when, old ACL, new ACL
Authentication failureWho (attempted), when, IP address, reason
Admin operationWho, when, operation (reassignment, config change)

Audit logs phải được gửi đến external system (SIEM, Elasticsearch) — không lưu trên broker (để tránh bị xóa bởi attacker).

Aha Moment: Security trong message queue thường bị bỏ qua vì “nó là internal system”. Nhưng nếu attacker truy cập được broker → đọc được toàn bộ messages của mọi service. Message queue là central nervous system — bảo vệ nó là bảo vệ toàn bộ hệ thống.


5. DevOps — Vận hành và giám sát

5.1 Critical Metrics — Các chỉ số quan trọng nhất

5.1.1 Broker Health

MetricThresholdÝ nghĩa
Under-replicated partitions> 0Có partition chưa được replicate đủ. Alert ngay — risk của data loss
ISR shrink rate> 0Followers đang bị loại khỏi ISR. Broker overloaded hoặc network issue
Active controller count!= 1Phải luôn có đúng 1 controller. 0 = không ai điều phối. >1 = split brain
Offline partitions> 0Partition không có leader. Critical alert — data không đọc/ghi được
Request handler idle ratio< 20%Broker đang quá tải — request threads gần hết

5.1.2 Producer Metrics

MetricThresholdÝ nghĩa
Produce request rateBao nhiêu requests/secThroughput của producers
Produce latency P99> 100msGhi chậm — có thể do broker overloaded, replication lag
Record error rate> 0Producer gửi thất bại — broker reject hoặc network error
Batch size average< 1KBBatch quá nhỏ — tăng linger.ms để gom nhiều hơn
Compression ratio> 0.8Compression không hiệu quả — check data pattern

5.1.3 Consumer Metrics — Quan trọng nhất

MetricThresholdÝ nghĩa
Consumer lag> 10,000 messagesConsumer không theo kịp producer. Đây là metric #1
Consumer lag trendTăng liên tụcConsumer chậm hơn producer — sẽ tràn memory/disk nếu không fix
Commit rateGiảm đột ngộtConsumer có thể bị stuck hoặc crash
Rebalance rate> 1/hourRebalance quá thường xuyên — consumer unstable
Poll interval> max.poll.interval.msConsumer bị kick khỏi group — processing quá chậm

Aha Moment: Consumer lag là metric #1 của toàn bộ message queue system. Nếu lag tăng → messages đang bị xử lý chậm hơn tốc độ gửi → cuối cùng retention hết → messages bị xóa trước khi consumer đọc → data loss. Monitoring consumer lag là bắt buộc.

5.1.4 Infrastructure Metrics

MetricThresholdÝ nghĩa
Disk usage> 80%Gần hết disk — cần thêm disk hoặc giảm retention
Disk I/O wait> 10%Disk là bottleneck — cần SSD hoặc giảm load
Network throughput> 70% capacityNetwork là bottleneck — cần nâng cấp NIC
CPU usage> 70%Thường do compression/decompression
JVM GC pause> 200msGC pause làm broker không respond — client timeout
File descriptor count> 80% ulimitMỗi partition + segment + connection tốn 1 fd. Hết fd = broker crash

5.2 Alerting Strategy

SeverityMetricAction
P1 — CriticalOffline partitions > 0Ngay lập tức: page on-call. Data không truy cập được
P1 — CriticalActive controller count != 1Page on-call. Cluster không có coordinator
P2 — HighUnder-replicated partitions > 0 (> 5 min)Investigate broker health, disk, network
P2 — HighConsumer lag tăng liên tục (> 30 min)Scale consumers hoặc investigate bottleneck
P3 — MediumDisk usage > 80%Plan capacity — thêm disk hoặc giảm retention
P3 — MediumISR shrink rate > 0Check follower health, replication bandwidth
P4 — LowProduce latency P99 > 100msInvestigate — có thể là transient
P4 — LowRebalance rate > 1/hourCheck consumer stability, session timeout config

5.3 Operational Runbook

5.3.1 Broker Failure

  1. Alert: Under-replicated partitions tăng
  2. Check: Broker nào bị mất? (broker.id không còn trong cluster)
  3. Automatic: Controller tự động elect leader mới từ ISR cho các partitions của broker chết
  4. Action: Restart broker hoặc thay hardware. Sau khi broker online, tự động rejoin và sync data
  5. Monitor: ISR count trở về replication factor → recovery hoàn tất

5.3.2 Consumer Lag Spike

  1. Alert: Consumer lag > threshold
  2. Check: Consumer có đang chạy không? Processing time trên mỗi message?
  3. Action short-term: Scale consumers (thêm instances). Lưu ý: cần đủ partitions
  4. Action long-term: Optimize processing logic, tăng partition count (nếu cần)
  5. Monitor: Lag giảm dần về 0

5.3.3 Disk Full

  1. Alert: Disk usage > 80%
  2. Action ngay: Giảm retention (ví dụ: từ 7 ngày xuống 3 ngày) để free disk
  3. Action trung hạn: Thêm disk, thêm broker
  4. Action dài hạn: Implement tiered storage (hot/cold) hoặc tăng compression

5.4 Capacity Planning

Thời điểmAction
WeeklyReview consumer lag trends, disk growth rate
MonthlyReview throughput growth, plan broker additions
QuarterlyReview partition counts, rebalance strategy, retention policy
YearlyMajor capacity planning — hardware refresh, architecture review

Pitfall: Nhiều team chỉ monitoring throughput và latency nhưng quên consumer lag. Hệ thống trông “healthy” (broker không bị lỗi) nhưng data đang mất vì consumer không theo kịp và messages bị xóa khi retention hết. Consumer lag là “silent killer” của message queue.


6. Mermaid Diagrams — Tổng hợp kiến trúc

6.1 Broker Architecture với WAL Segments

flowchart TB
    subgraph "Broker 1"
        subgraph "Partition 0 (Leader)"
            direction TB
            SEG0["Segment 0<br/>Offset 0-999K<br/>📁 00000000.log<br/>📁 00000000.index<br/>📁 00000000.timeindex"]
            SEG1["Segment 1<br/>Offset 1M-1.99M<br/>📁 01000000.log<br/>📁 01000000.index<br/>📁 01000000.timeindex"]
            SEG2["Segment 2 (ACTIVE)<br/>Offset 2M-...<br/>📁 02000000.log<br/>📁 02000000.index<br/>📁 02000000.timeindex"]
            SEG0 --> SEG1 --> SEG2
        end

        subgraph "Write Path"
            PC["Page Cache<br/>(OS RAM)"]
            FL["Flush to Disk<br/>(async)"]
        end

        subgraph "Read Path"
            IDX["Index Lookup<br/>(mmap)"]
            ZC["Zero-Copy<br/>sendfile()"]
        end
    end

    P["Producer"] -->|"Append"| SEG2
    SEG2 -->|"Write"| PC
    PC -->|"Async"| FL

    C["Consumer"] -->|"Seek(offset)"| IDX
    IDX -->|"Position"| PC
    PC -->|"sendfile()"| ZC
    ZC -->|"Data"| C

    style SEG2 fill:#e53935,color:#fff
    style PC fill:#1e88e5,color:#fff
    style ZC fill:#43a047,color:#fff

6.2 Replication Flow — Leader + Followers + ISR

flowchart TB
    subgraph "Partition 0 Replication"
        P["Producer<br/>acks=all"] -->|"1. Send msg"| L["Broker 1<br/>LEADER<br/>LEO: 1000"]

        L -->|"2. Write WAL"| WAL["WAL<br/>(disk)"]
        L -->|"3. Replicate"| F1["Broker 2<br/>FOLLOWER<br/>LEO: 999<br/>✅ IN ISR"]
        L -->|"3. Replicate"| F2["Broker 3<br/>FOLLOWER<br/>LEO: 998<br/>✅ IN ISR"]
        L -.->|"3. Replicate"| F3["Broker 4<br/>FOLLOWER<br/>LEO: 500<br/>❌ OUT OF ISR<br/>(lag > 30s)"]

        F1 -->|"4. ACK"| L
        F2 -->|"4. ACK"| L
        L -->|"5. ACK to Producer<br/>(all ISR confirmed)"| P

        HW["High Watermark = 998<br/>(min LEO of ISR)"]
        L --- HW

        C["Consumer"] -->|"6. Read <= HW"| L
    end

    style L fill:#e53935,color:#fff
    style F1 fill:#1e88e5,color:#fff
    style F2 fill:#1e88e5,color:#fff
    style F3 fill:#757575,color:#fff
    style HW fill:#f57c00,color:#fff

6.3 Consumer Group Rebalancing (Cooperative)

flowchart TB
    subgraph "TRƯỚC — 2 consumers, 6 partitions"
        direction LR
        C1_B["Consumer 1<br/>P0, P1, P2"]
        C2_B["Consumer 2<br/>P3, P4, P5"]
    end

    subgraph "Consumer 3 joins"
        direction TB
        EV["Rebalance triggered"]
    end

    subgraph "SAU — 3 consumers, 6 partitions (Cooperative)"
        direction LR
        C1_A["Consumer 1<br/>P0, P1<br/>(giữ P0,P1 — trả P2)"]
        C2_A["Consumer 2<br/>P3, P4<br/>(giữ P3,P4 — trả P5)"]
        C3_A["Consumer 3<br/>P2, P5<br/>(nhận P2, P5)"]
    end

    C1_B --> EV
    C2_B --> EV
    EV --> C1_A
    EV --> C2_A
    EV --> C3_A

    style EV fill:#f57c00,color:#fff
    style C3_A fill:#43a047,color:#fff

6.4 End-to-End Message Flow (Chi tiết)

flowchart LR
    subgraph "Producer Side"
        APP["Application"] -->|"1. produce()"| SER["Serializer<br/>(JSON/Avro)"]
        SER -->|"2. serialize"| PART["Partitioner<br/>hash(key) % N"]
        PART -->|"3. assign partition"| BATCH["Batch Buffer<br/>(per partition)"]
        BATCH -->|"4. batch full<br/>or linger.ms"| COMP["Compressor<br/>(lz4/zstd)"]
        COMP -->|"5. compressed batch"| NET1["Network<br/>Send"]
    end

    subgraph "Broker Side"
        NET1 -->|"6. receive"| VAL["Validate<br/>CRC, auth, ACL"]
        VAL -->|"7. append"| WAL2["WAL<br/>(page cache)"]
        WAL2 -->|"8. replicate"| REP["Followers"]
        REP -->|"9. ACK"| ACK["ACK to<br/>Producer"]
    end

    subgraph "Consumer Side"
        POLL["poll()"] -->|"10. fetch"| WAL2
        WAL2 -->|"11. zero-copy"| DECOMP["Decompressor"]
        DECOMP -->|"12. decompress"| DESER["Deserializer"]
        DESER -->|"13. deserialize"| PROC["Process<br/>Message"]
        PROC -->|"14. commit"| OFFSET["Commit<br/>Offset"]
    end

    style BATCH fill:#1e88e5,color:#fff
    style WAL2 fill:#e53935,color:#fff
    style OFFSET fill:#43a047,color:#fff

7. Aha Moments & Pitfalls — Những điều cần nhớ

7.1 Aha Moments

#InsightGiải thích
1WAL là nền tảngToàn bộ message queue được xây trên một ý tưởng đơn giản: append-only log. Ghi vào cuối file là thao tác nhanh nhất trên disk. Từ đây, mọi thứ khác (replication, retention, replay) trở nên tự nhiên
2ISR cân bằng durability vs availabilityISR không phải “tất cả replicas” — nó là “các replicas đang theo kịp”. Config min.insync.replicas quyết định bạn chấp nhận mất bao nhiêu replicas mà vẫn ghi được
3Consumer lag là metric #1Broker throughput cao, latency thấp — nhưng consumer lag tăng → data đang mất dần. Đây là “silent killer”. Monitor consumer lag trước mọi thứ khác
4Partition count khó thay đổiTăng partition → key routing thay đổi → ordering bị phá. Giảm partition → không thể. Chọn partition count đúng từ đầu là quyết định architecture quan trọng nhất
5Pull model ưu việt cho streamingConsumer tự quyết định tốc độ, batch size, và khi nào đọc. Back-pressure tự nhiên. Replay dễ dàng. Đây là lý do Kafka (pull) thắng thế cho event streaming so với RabbitMQ (push)
6Zero-copy là “bí quyết” performanceKhông copy data từ kernel → user space → kernel. Trực tiếp từ page cache → network. Giảm CPU usage, giảm latency. Đây là lý do message queue đạt millions msg/sec trên hardware thường
7Broker không hiểu messageBroker chỉ thấy bytes. Không parse, không validate nội dung. Điều này giúp broker cực nhanh và generic — phù hợp với mọi loại data
8Exactly-once là kết hợp nhiều cơ chếKhông có “magic button”. Cần idempotent producer + transactions + idempotent consumer. Hiểu từng cơ chế giúp bạn biết khi nào actually cần exactly-once vs at-least-once đã đủ

7.2 Common Pitfalls

#PitfallHậu quảCách tránh
1Không set message keyRound-robin → không ordering. Messages của cùng entity đi vào partition khác nhauLuôn set key = entity ID (user_id, order_id) khi cần ordering
2Partition count quá ítKhông scale được consumers. Throughput bị giới hạnChọn partition count = 2-3x số consumers dự kiến
3acks=0 cho critical dataMất message khi broker crashDùng acks=all + min.insync.replicas=2 cho mọi data quan trọng
4Auto commit với complex processingMất message hoặc xử lý trùngDùng manual commit — chỉ commit sau khi xử lý xong
5Không có DLQPoison message block consumer vĩnh viễnLuôn design DLQ với max retries + exponential backoff
6Tăng partition count tùy tiệnKey routing thay đổi, ordering per key bị pháPlan partition count trước. Nếu phải tăng, chấp nhận downtime cho re-keying
7Không monitor consumer lagData mất do retention expire trước khi consumer đọcAlert consumer lag > threshold. Đây là P2 alert
8unclean.leader.election = true cho financial dataMất messages khi leader crashTắt unclean leader election cho mọi topic quan trọng
9Rebalance stormConsumer group liên tục rebalance → không ai xử lý messagesTăng session.timeout.ms, tăng max.poll.interval.ms, dùng sticky assignment
10Không encrypt inter-broker trafficAttacker trong internal network đọc được tất cả messagesTLS cho mọi connection, kể cả inter-broker

8. Summary — Tổng kết

8.1 So sánh thiết kế của chúng ta với Kafka thực tế

ComponentThiết kế của chúng taApache KafkaGhi chú
StorageWAL + segments + indexGiốngKafka dùng chính xác mô hình này
ReplicationLeader-follower + ISRGiốngISR là sáng tạo của Kafka team
CoordinationZooKeeper/KRaftZK → KRaftKafka đang migrate sang KRaft
Consumer modelPull + consumer groupsGiốngPull là core design của Kafka
Delivery semanticsAt-most/least/exactly onceGiốngExactly-once từ Kafka 0.11+
RetentionTime/size/compactionGiống3 policies giống nhau
Compressiongzip, snappy, lz4, zstdGiốngzstd từ Kafka 2.1+

8.2 Khi nào dùng message queue architecture này?

Use casePhù hợp?Lý do
Event streaming (logs, metrics, clickstream)Rất phù hợpHigh throughput, retention, replay
Microservice communication (async)Phù hợpDecoupling, at-least-once, consumer groups
Real-time analytics pipelineRất phù hợpMultiple consumer groups đọc cùng data
Task queue (background jobs)Được, nhưng RabbitMQ có thể tốt hơnPull model không optimal cho ngắn job queue
Request-reply patternKhông phù hợpPull model không tốt cho synchronous communication
Small-scale system (< 1000 msg/sec)OverkillDùng Redis Pub/Sub hoặc SQS đơn giản hơn

8.3 Liên kết với các bài khác

BàiLiên quan
Tuan-08-Message-QueueBài này bổ sung — Tuần 08 dạy dùng MQ, bài này dạy thiết kế MQ
Tuan-10-Consistent-HashingPartition assignment có thể dùng consistent hashing để giảm rebalance disruption
Tuan-07-Database-Sharding-ReplicationReplication model (leader-follower, ISR) tương tự database replication. Sharding tương tự partitioning
Tuan-13-Monitoring-ObservabilityConsumer lag monitoring, broker health monitoring là ứng dụng trực tiếp
Tuan-14-AuthN-AuthZ-SecuritySASL, mTLS, ACL trong message queue là ứng dụng của authn/authz patterns
Case-Design-Payment-SystemPayment system dùng message queue với exactly-once semantics
Case-Design-Stock-ExchangeStock exchange dùng message queue cho event sequencing

” sau bài này bạn đã biết cách thiết kế một distributed message queue từ scratch — không chỉ biết dùng nó. Khi interview hỏi ‘Design Kafka’, bạn có thể tự tin giải thích mọi quyết định thiết kế: tại sao append-only log, tại sao pull model, tại sao ISR, tại sao partition là đơn vị của parallelism. Đó là sự khác biệt giữa Backend Dev và System Architect.”