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:
-
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.
-
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ự.
-
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.
-
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ý.
-
Đả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.
-
Đả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.
-
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ức | Giải thích |
|---|---|
| High throughput | 1 triệu messages/giây — mỗi message phải được nhận, lưu, và giao |
| Durability | Không được mất message — dù broker bị crash |
| Ordering | Messages trong cùng partition phải giữ đúng thứ tự |
| Scalability | Thêm broker, thêm partition khi traffic tăng |
| Fault tolerance | Broker chết → hệ thống vẫn hoạt động bình thường |
| Low latency | Producer gửi → consumer nhận trong vài millisecond |
| Replay capability | Consumer có thể đọc lại message từ bất kỳ thời điểm nào |
| Exactly-once delivery | Trong 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ăng | Mô tả chi tiết |
|---|---|
| Producer API | Producer gửi message vào một topic cụ thể. Hỗ trợ batch gửi nhiều message cùng lúc |
| Consumer API | Consumer đọc message từ topic. Hỗ trợ pull-based model (consumer chủ động kéo data) |
| Topic management | Tạo, xóa, liệt kê topics. Mỗi topic có nhiều partitions |
| Message retention | Giữ message trong một khoảng thời gian (7 ngày default) hoặc theo kích thước |
| Message replay | Consumer có thể đọc lại message từ bất kỳ offset nào trong retention period |
| Consumer group | Nhóm consumer cùng đọc một topic — mỗi partition chỉ gán cho 1 consumer trong group |
| Delivery semantics | Hỗ 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ầu | Mục tiêu | Lý do |
|---|---|---|
| Throughput | 1,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 |
| Durability | Không mất message khi có ⇐ 2 broker failures đồng thời | Dữ liệu là tài sản — mất message = mất tiền |
| Availability | 99.99% uptime | Message queue là backbone — nó chết thì toàn hệ thống chết |
| Scalability | Scale horizontally bằng cách thêm broker | Traffic tăng → thêm máy, không cần thiết kế lại |
| Storage efficiency | Lưu trữ hiệu quả trên disk | Message được giữ nhiều ngày — disk là bottleneck |
| Ordering guarantee | Messages 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:
| API | Parameters | Mô 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:
| API | Parameters | Mô tả |
|---|---|---|
subscribe(topics[], group_id) | topics: list of string, group_id: string | Consumer tham gia group và subscribe vào topics |
poll(timeout) | timeout: ms | Ké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: long | Nhả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ần | Vai trò | Chi tiết |
|---|---|---|
| Producer | Gửi message vào broker | Chọn partition (round-robin, key-based hash, custom). Batch và compress trước khi gửi |
| Broker | Lưu trữ và phục vụ message | Mỗi broker quản lý một số partitions. Ghi message vào disk (WAL). Phục vụ read cho consumer |
| Metadata Store | Lưu metadata của cluster | Danh sách broker, topic config, partition assignment, consumer group state. Dùng ZooKeeper hoặc etcd |
| Coordinator | Điều phối cluster | Leader election cho partition, consumer group rebalancing, broker membership management |
| Consumer | Đọc message từ broker | Pull-based — consumer chủ động kéo data. Track offset để biết đọc đến đâu |
| Consumer Group | Nhóm consumer chia nhau đọc | Mỗ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ính | Mô tả | Ví dụ |
|---|---|---|
| Name | Tên topic — định danh duy nhất | orders, payments, user-events |
| Partition count | Số partitions — quyết định parallelism | 12 partitions |
| Replication factor | Số bản sao mỗi partition | 3 (1 leader + 2 followers) |
| Retention | Thời gian giữ message | 7 ngày (default) |
| Cleanup policy | Cá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 tin | Chi tiết |
|---|---|
| Kiểu dữ liệu | 64-bit integer |
| Phạm vi | 0 đến 2^63 - 1 (đủ cho 292 năm với 1M msg/sec) |
| Tính chất | Monotonically increasing trong partition |
| Mục đích | Consumer 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ường | Kiểu | Mô tả |
|---|---|---|
| Key | bytes (nullable) | Dùng để quyết định partition. Ví dụ: user_id, order_id. Nullable — nếu null thì round-robin |
| Value | bytes | Nội dung message. Có thể là JSON, Avro, Protobuf. Broker không care format — chỉ là bytes |
| Timestamp | long (epoch ms) | Thời điểm message được tạo (CreateTime) hoặc thời điểm broker nhận (LogAppendTime) |
| Headers | list of key-value | Metadata bổ sung. Ví dụ: trace-id, source-service, content-type |
| Offset | long | Được broker gán khi message được ghi vào partition. Producer không set |
| Partition | int | Partition mà message thuộc về |
| CRC | int | Checksum để 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ý do | Giải thích |
|---|---|
| Retention cleanup | Xóa segment cũ để giải phóng disk — xóa cả file, không cần scan từng message |
| Compaction | Nén segment cũ — giữ lại latest value per key |
| File system limits | Một file quá lớn → OS xử lý chậm. Segment nhỏ hơn dễ quản lý |
| Recovery | Khi broker restart, chỉ cần recover active segment — các segment cũ đã được flush |
Cấu hình segment:
| Config | Default | Mô tả |
|---|---|---|
segment.bytes | 1 GB | Kích thước tối đa mỗi segment |
segment.ms | 7 ngày | Thời gian tối đa trước khi roll segment mới |
segment.index.bytes | 10 MB | Kí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:
- Binary search trong index → tìm entry gần nhất nhỏ hơn: offset 1,000,100 tại position 15,200
- 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:
- Producer gửi message → broker ghi vào page cache (RAM)
- OS tự động flush page cache xuống disk (async)
- 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ào | Lợi ích |
|---|---|---|
| Page cache | Segment files (.log) | Write nhanh (async flush), read nhanh (từ RAM) |
| mmap | Index files (.index, .timeindex) | Truy cập index nhanh như RAM |
| sendfile() / zero-copy | Transfer data từ disk → network | Giả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.
| Config | Default | Mô tả |
|---|---|---|
batch.size | 16 KB | Kích thước tối đa của batch |
linger.ms | 0 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 msbatch.sizelớ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.
| Algorithm | Compression ratio | CPU usage | Tốc độ | Khi nào dùng |
|---|---|---|---|---|
| gzip | Cao nhất (~70-80%) | Cao | Chậm | Bandwidth limited, batch lớn |
| snappy | Trung bình (~50-60%) | Thấp | Nhanh | Cần balance giữa compression và CPU |
| lz4 | Trung bình (~55-65%) | Rất thấp | Rất nhanh | Default choice — tốt nhất cho đa số trường hợp |
| zstd | Cao (~65-75%) | Trung bình | Nhanh | Muố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.msvàbatch.sizegiú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:
| acks | Hành vi | Durability | Latency | Khi nào dùng |
|---|---|---|---|---|
| 0 | Producer không chờ ACK. “Fire and forget” | Thấp nhất — có thể mất message | Thấp nhất | Metrics, logs — chấp nhận mất |
| 1 | Chờ ACK từ leader. Leader ghi xong → ACK | Trung bình — mất nếu leader crash trước khi replicate | Trung bình | Đa số trường hợp |
| all (-1) | Chờ ACK từ tất cả ISR (In-Sync Replicas). Leader + followers ghi xong → ACK | Cao nhất — chỉ mất khi tất cả ISR crash cùng lúc | Cao nhất | Payment events, critical data — không được mất |
Pitfall:
acks=allkhô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=alltương đươngacks=1. Đây là lý do cần setmin.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 |
|---|---|
| ISR | Tậ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ình | Hành vi |
|---|---|
replication.factor = 3 | Mỗi partition có 3 replicas |
min.insync.replicas = 2 | Cần ít nhất 2 replicas trong ISR để ghi được |
acks = all | Producer đợ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.replicaslà “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ọn | Config | Hậu quả |
|---|---|---|
| Chờ đợi ISR follower online | unclean.leader.election.enable = false | Partition không hoạt động cho đến khi ISR follower quay lại. Durability > Availability |
| Chọn follower ngoài ISR làm leader | unclean.leader.election.enable = true | Partition 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ất và hay 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):
-
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
-
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ì
-
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
| Config | Default | Mô tả |
|---|---|---|
retention.ms | 604,800,000 (7 ngày) | Message cũ hơn 7 ngày bị xóa |
retention.minutes | - | Tính theo phút |
retention.hours | 168 | Tính theo giờ |
Khi segment file cũ nhất có timestamp > retention → xóa cả segment file.
Size-Based Retention
| Config | Default | Mô 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ăng | Chi tiết |
|---|---|
| Broker registry | Mỗi broker đăng ký vào ZooKeeper khi start. ZK biết broker nào đang sống |
| Controller election | Chọn 1 broker làm “Controller” — phụ trách leader election cho partitions |
| Topic/partition metadata | Lư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ạnh | ZooKeeper | KRaft |
|---|---|---|
| Architecture | External ZK cluster | Built-in — metadata stored trong Kafka brokers |
| Metadata storage | ZK znodes | Internal __cluster_metadata topic (Raft log) |
| Controller | 1 broker được ZK bầu làm controller | Quorum of controllers (3-5 brokers có role controller) |
| Failover time | 10-30 giây (ZK session timeout) | 1-3 giây (Raft election) |
| Scaling | ZK là bottleneck | Metadata replicate qua Raft — scale tốt hơn |
| Operational | 2 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ạnh | Pull (Kafka model) | Push (RabbitMQ model) |
|---|---|---|
| Ai điều khiển tốc độ? | Consumer — kéo data theo tốc độ của mình | Broker — đẩy data xuống consumer |
| Consumer chậm? | Không ảnh hưởng broker — data vẫn nằm trên disk | Broker phải buffer hoặc drop messages |
| Consumer nhanh? | Kéo data liên tục, không phải đợi | Broker có thể không đẩy đủ nhanh |
| Batching | Consumer tự chọn batch size tối ưu | Broker quyết định batch — có thể không phù hợp |
| Long polling | Consumer poll với timeout — nếu không có data mới, đợi một lúc rồi poll lại | Không cần — broker đẩy ngay khi có data |
| Back-pressure | Tự nhiên — consumer chỉ kéo khi sẵn sàng | Cần cơ chế riêng (prefetch count, flow control) |
| Replay | Dễ dàng — consumer chỉ cần seek offset | Khó — message đã bị xóa sau khi ACK |
| Complexity | Consumer 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?
- Consumer tự quyết định tốc độ — không bị overwhelm
- Batching tối ưu — consumer kéo đúng lượng data phù hợp
- Replay dễ dàng — seek đến bất kỳ offset nào
- Back-pressure tự nhiên — không cần cơ chế đặc biệt
- 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
| Config | Mô tả |
|---|---|
| Max retries | Số lần retry trước khi chuyển vào DLQ |
| Retry delay | Thời gian đợi giữa các lần retry (exponential backoff) |
| DLQ topic name | Convention: {original-topic}-dlq |
| DLQ retention | Thườ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):
- Start broker mới, join cluster
- Broker mới chưa có partition nào — nó là “empty”
- Admin chạy partition reassignment — di chuyển một số partitions từ brokers cũ sang broker mới
- Trong quá trình di chuyển, data được replicate từ broker cũ sang broker mới
- 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):
- Admin tăng partition count của topic (ví dụ: từ 6 lên 12)
- Broker tạo partitions mới trên các brokers
- Consumer group rebalance — partitions được gán lại cho consumers
- 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) % 6khác vớihash(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 throughput | 1,000,000 messages/sec | Yêu cầu từ requirements |
| Average message size | 1 KB | JSON event trung bình |
| Peak/Average ratio | 3x | Flash sales, events |
| Replication factor | 3 | 1 leader + 2 followers |
3.2 Storage Estimation
| Thông số | Giá trị |
|---|---|
| Messages/day | 1,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 factor | 3 |
| Data/day (with replication) | 43.2 TB x 3 = 129.6 TB |
| Retention | 7 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 processing | Time | Messages/sec per consumer |
|---|---|---|
| Simple log/forward | 0.1 ms/msg | 10,000 msg/sec |
| Database write | 1 ms/msg | 1,000 msg/sec |
| Complex processing | 5 ms/msg | 200 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
| Metric | Giá trị |
|---|---|
| Write throughput | 1 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 needed | 100 (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ối | TLS | Mô tả |
|---|---|---|
| Producer → Broker | Có | TLS 1.2/1.3. Ngăn man-in-the-middle đọc messages |
| Broker → Broker (replication) | Có | Inter-broker replication cũng phải encrypt |
| Broker → Consumer | Có | Consumer đọc messages qua TLS |
| Broker → ZooKeeper/Controller | Có | Metadata 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áp | Mô tả | Khi nào dùng |
|---|---|---|
| Disk encryption (dm-crypt, LUKS) | Encrypt toàn bộ disk — transparent cho Kafka | Standard cho mọi production deployment |
| File system encryption | Encrypt ở file system level (eCryptfs, fscrypt) | Khi không có full disk encryption |
| Message-level encryption | Producer encrypt message body trước khi gửi | Khi 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áp | Mô tả | Khi nào dùng |
|---|---|---|
| SASL/PLAIN | Username/password | Dev/test environments. Không an toàn cho production (password đi plain text, cần dùng kèm TLS) |
| SASL/SCRAM | Challenge-response (không gửi password) | Production — an toàn hơn PLAIN |
| SASL/GSSAPI (Kerberos) | Enterprise SSO | Enterprise environments có Kerberos infrastructure |
| SASL/OAUTHBEARER | OAuth 2.0 tokens | Cloud-native environments, integration với identity providers |
| mTLS | Mutual TLS — client và server đều present certificate | Zero-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:
| Principal | Resource | Operation | Permission |
|---|---|---|---|
order-service | Topic: orders | WRITE | ALLOW |
order-service | Topic: payments | WRITE | DENY |
analytics-service | Topic: orders | READ | ALLOW |
analytics-service | Topic: orders | WRITE | DENY |
admin | Topic: * | ALL | ALLOW |
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
| Event | Thông tin log |
|---|---|
| Topic created/deleted | Who, when, topic name, config |
| ACL changed | Who, when, old ACL, new ACL |
| Authentication failure | Who (attempted), when, IP address, reason |
| Admin operation | Who, 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
| Metric | Threshold | Ý nghĩa |
|---|---|---|
| Under-replicated partitions | > 0 | Có partition chưa được replicate đủ. Alert ngay — risk của data loss |
| ISR shrink rate | > 0 | Followers đang bị loại khỏi ISR. Broker overloaded hoặc network issue |
| Active controller count | != 1 | Phải luôn có đúng 1 controller. 0 = không ai điều phối. >1 = split brain |
| Offline partitions | > 0 | Partition 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
| Metric | Threshold | Ý nghĩa |
|---|---|---|
| Produce request rate | Bao nhiêu requests/sec | Throughput của producers |
| Produce latency P99 | > 100ms | Ghi chậm — có thể do broker overloaded, replication lag |
| Record error rate | > 0 | Producer gửi thất bại — broker reject hoặc network error |
| Batch size average | < 1KB | Batch quá nhỏ — tăng linger.ms để gom nhiều hơn |
| Compression ratio | > 0.8 | Compression không hiệu quả — check data pattern |
5.1.3 Consumer Metrics — Quan trọng nhất
| Metric | Threshold | Ý nghĩa |
|---|---|---|
| Consumer lag | > 10,000 messages | Consumer không theo kịp producer. Đây là metric #1 |
| Consumer lag trend | Tăng liên tục | Consumer chậm hơn producer — sẽ tràn memory/disk nếu không fix |
| Commit rate | Giảm đột ngột | Consumer có thể bị stuck hoặc crash |
| Rebalance rate | > 1/hour | Rebalance quá thường xuyên — consumer unstable |
| Poll interval | > max.poll.interval.ms | Consumer 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
| Metric | Threshold | Ý 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% capacity | Network là bottleneck — cần nâng cấp NIC |
| CPU usage | > 70% | Thường do compression/decompression |
| JVM GC pause | > 200ms | GC pause làm broker không respond — client timeout |
| File descriptor count | > 80% ulimit | Mỗi partition + segment + connection tốn 1 fd. Hết fd = broker crash |
5.2 Alerting Strategy
| Severity | Metric | Action |
|---|---|---|
| P1 — Critical | Offline partitions > 0 | Ngay lập tức: page on-call. Data không truy cập được |
| P1 — Critical | Active controller count != 1 | Page on-call. Cluster không có coordinator |
| P2 — High | Under-replicated partitions > 0 (> 5 min) | Investigate broker health, disk, network |
| P2 — High | Consumer lag tăng liên tục (> 30 min) | Scale consumers hoặc investigate bottleneck |
| P3 — Medium | Disk usage > 80% | Plan capacity — thêm disk hoặc giảm retention |
| P3 — Medium | ISR shrink rate > 0 | Check follower health, replication bandwidth |
| P4 — Low | Produce latency P99 > 100ms | Investigate — có thể là transient |
| P4 — Low | Rebalance rate > 1/hour | Check consumer stability, session timeout config |
5.3 Operational Runbook
5.3.1 Broker Failure
- Alert: Under-replicated partitions tăng
- Check: Broker nào bị mất? (
broker.idkhông còn trong cluster) - Automatic: Controller tự động elect leader mới từ ISR cho các partitions của broker chết
- Action: Restart broker hoặc thay hardware. Sau khi broker online, tự động rejoin và sync data
- Monitor: ISR count trở về replication factor → recovery hoàn tất
5.3.2 Consumer Lag Spike
- Alert: Consumer lag > threshold
- Check: Consumer có đang chạy không? Processing time trên mỗi message?
- Action short-term: Scale consumers (thêm instances). Lưu ý: cần đủ partitions
- Action long-term: Optimize processing logic, tăng partition count (nếu cần)
- Monitor: Lag giảm dần về 0
5.3.3 Disk Full
- Alert: Disk usage > 80%
- Action ngay: Giảm retention (ví dụ: từ 7 ngày xuống 3 ngày) để free disk
- Action trung hạn: Thêm disk, thêm broker
- Action dài hạn: Implement tiered storage (hot/cold) hoặc tăng compression
5.4 Capacity Planning
| Thời điểm | Action |
|---|---|
| Weekly | Review consumer lag trends, disk growth rate |
| Monthly | Review throughput growth, plan broker additions |
| Quarterly | Review partition counts, rebalance strategy, retention policy |
| Yearly | Major 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
| # | Insight | Giải thích |
|---|---|---|
| 1 | WAL là nền tảng | Toà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 |
| 2 | ISR cân bằng durability vs availability | ISR 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 |
| 3 | Consumer lag là metric #1 | Broker 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 |
| 4 | Partition count khó thay đổi | Tă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 |
| 5 | Pull model ưu việt cho streaming | Consumer 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) |
| 6 | Zero-copy là “bí quyết” performance | Khô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 |
| 7 | Broker không hiểu message | Broker 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 |
| 8 | Exactly-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
| # | Pitfall | Hậu quả | Cách tránh |
|---|---|---|---|
| 1 | Không set message key | Round-robin → không ordering. Messages của cùng entity đi vào partition khác nhau | Luôn set key = entity ID (user_id, order_id) khi cần ordering |
| 2 | Partition count quá ít | Không scale được consumers. Throughput bị giới hạn | Chọn partition count = 2-3x số consumers dự kiến |
| 3 | acks=0 cho critical data | Mất message khi broker crash | Dùng acks=all + min.insync.replicas=2 cho mọi data quan trọng |
| 4 | Auto commit với complex processing | Mất message hoặc xử lý trùng | Dùng manual commit — chỉ commit sau khi xử lý xong |
| 5 | Không có DLQ | Poison message block consumer vĩnh viễn | Luôn design DLQ với max retries + exponential backoff |
| 6 | Tăng partition count tùy tiện | Key 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 |
| 7 | Không monitor consumer lag | Data mất do retention expire trước khi consumer đọc | Alert consumer lag > threshold. Đây là P2 alert |
| 8 | unclean.leader.election = true cho financial data | Mất messages khi leader crash | Tắt unclean leader election cho mọi topic quan trọng |
| 9 | Rebalance storm | Consumer group liên tục rebalance → không ai xử lý messages | Tăng session.timeout.ms, tăng max.poll.interval.ms, dùng sticky assignment |
| 10 | Không encrypt inter-broker traffic | Attacker trong internal network đọc được tất cả messages | TLS 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ế
| Component | Thiết kế của chúng ta | Apache Kafka | Ghi chú |
|---|---|---|---|
| Storage | WAL + segments + index | Giống | Kafka dùng chính xác mô hình này |
| Replication | Leader-follower + ISR | Giống | ISR là sáng tạo của Kafka team |
| Coordination | ZooKeeper/KRaft | ZK → KRaft | Kafka đang migrate sang KRaft |
| Consumer model | Pull + consumer groups | Giống | Pull là core design của Kafka |
| Delivery semantics | At-most/least/exactly once | Giống | Exactly-once từ Kafka 0.11+ |
| Retention | Time/size/compaction | Giống | 3 policies giống nhau |
| Compression | gzip, snappy, lz4, zstd | Giống | zstd từ Kafka 2.1+ |
8.2 Khi nào dùng message queue architecture này?
| Use case | Phù hợp? | Lý do |
|---|---|---|
| Event streaming (logs, metrics, clickstream) | Rất phù hợp | High throughput, retention, replay |
| Microservice communication (async) | Phù hợp | Decoupling, at-least-once, consumer groups |
| Real-time analytics pipeline | Rất phù hợp | Multiple consumer groups đọc cùng data |
| Task queue (background jobs) | Được, nhưng RabbitMQ có thể tốt hơn | Pull model không optimal cho ngắn job queue |
| Request-reply pattern | Không phù hợp | Pull model không tốt cho synchronous communication |
| Small-scale system (< 1000 msg/sec) | Overkill | Dù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ài | Liên quan |
|---|---|
| Tuan-08-Message-Queue | Bà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-Hashing | Partition assignment có thể dùng consistent hashing để giảm rebalance disruption |
| Tuan-07-Database-Sharding-Replication | Replication model (leader-follower, ISR) tương tự database replication. Sharding tương tự partitioning |
| Tuan-13-Monitoring-Observability | Consumer lag monitoring, broker health monitoring là ứng dụng trực tiếp |
| Tuan-14-AuthN-AuthZ-Security | SASL, mTLS, ACL trong message queue là ứng dụng của authn/authz patterns |
| Case-Design-Payment-System | Payment system dùng message queue với exactly-once semantics |
| Case-Design-Stock-Exchange | Stock 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.”