Kafka
Kafka là log phân tán bền vững: topic chia partition, consumer đọc theo offset và có thể đọc lại. Bài này đi qua thứ tự theo partition, consumer group, replication với ISR và acks, giới hạn của exactly-once, cách chọn số partition và so sánh với SQS, Pub/Sub, RabbitMQ.
Mẹo: So sánh Kafka với các hàng đợi khác ở So sánh Message Queue và đọc thêm RabbitMQ.
Kafka là gì và mô hình cốt lõi
Kafka là một log phân tán chỉ ghi nối thêm. Producer ghi message vào cuối log, consumer đọc từ một vị trí bất kỳ. Message không bị xóa khi được đọc. Nó ở lại tới khi hết thời gian retention. Grokking ch.17 mô tả đúng lựa chọn này: không xóa message sau khi consume, consumer tự theo dõi vị trí đã đọc và một job xóa message khi hết hạn, và nói Kafka chủ yếu dùng ý tưởng đó.1 Đây là điểm khác lớn nhất so với hàng đợi truyền thống, nơi message biến mất sau khi consumer ack (mô hình thứ hai trong cùng mục, message chỉ bị ẩn rồi consumer xóa bằng lệnh gọi API).1
Các khái niệm cần nắm:
- Topic: một luồng sự kiện có tên, ví dụ
orders,clicks. Microservices Patterns §3.3.4 ghi Kafka dùng "topic" cho cả kênh point-to-point lẫn publish-subscribe.2 - Partition: topic được chia thành nhiều partition. Mỗi partition là một log có thứ tự riêng, nằm trên một broker (kèm bản sao trên broker khác). Grokking ch.18 mô tả broker có nhiều topic, mỗi topic nhiều partition, partition lưu message trong các segment, consumer đọc theo offset.3
- Offset: số thứ tự của message trong partition, tăng dần từ 0.
- Key: producer gắn key cho message. Partition được chọn bằng
hash(key) mod số_partition. Cùng key thì cùng partition, nên cùng thứ tự; Microservices Patterns §3.3.5 mô tả đúng cơ chế này (shard key trong header, băm modulo số shard).4 - Consumer group: nhóm consumer cùng xử lý một topic. Mỗi partition được giao cho đúng một consumer trong group (sách: broker gán mỗi shard cho một receiver và gán lại khi receiver vào hoặc ra).4 Group khác nhau đọc độc lập, mỗi group có offset riêng (suy ra từ mô hình offset của Grokking ch.18).3
Hệ quả quan trọng (các điểm 1 và 2 theo Microservices Patterns §3.3.5):4
- Thứ tự chỉ được đảm bảo trong một partition. Muốn mọi sự kiện của đơn hàng 123 xử lý đúng thứ tự, dùng
orderIdlàm key. - Độ song song tối đa của một group bằng số partition. Topic 12 partition thì tối đa 12 consumer làm việc, consumer thứ 13 ngồi chờ.
- Replay. Muốn xử lý lại dữ liệu ba ngày trước (sửa bug, dựng lại index), chỉ cần đặt offset lùi về. Điều này khả thi vì message không bị xóa khi đọc;1 sách cũng lưu ý broker như Kafka chỉ giữ message theo thời hạn cấu hình, không lưu vô hạn nên khi dựng lại CQRS view phải đọc thêm event đã lưu trữ.5
Kiến trúc
Mỗi partition có một leader và vài follower trên broker khác (mô hình primary-secondary, xem Grokking ch.17 về replicate queue theo primary-secondary hoặc quorum).6 Producer và consumer nói chuyện với leader. Follower kéo dữ liệu từ leader để giữ bản sao. Metadata của cluster (partition nào ở đâu, ai là leader) do controller quản lý, tương tự cluster manager giám sát sức khoẻ broker trong Grokking ch.18.3 Phần cơ chế controller (ZooKeeper hay KRaft) là kiến thức sản phẩm của Kafka, xem ZooKeeper.7
Replication: ISR và acks
ISR (in-sync replicas) là tập replica đang theo kịp leader; follower tụt quá ngưỡng cấu hình (ví dụ replica.lag.time.max.ms) sẽ bị loại khỏi ISR. Đây là cơ chế của Kafka, chưa có trong sách của thư viện; nền tảng chung là Grokking ch.9 về replication đồng bộ và bất đồng bộ.87
Producer chọn mức xác nhận bằng acks:
acks | Ý nghĩa | Rủi ro |
|---|---|---|
0 | Không chờ gì | Mất message khi broker lỗi |
1 | Chờ leader ghi | Leader chết trước khi follower chép là mất |
all | Chờ mọi replica trong ISR ghi | Chậm hơn chút, an toàn nhất |
Về nguyên tắc: replicate đồng bộ thì mọi secondary cập nhật xong mới báo thành công nhưng chậm hơn, replicate bất đồng bộ thì nhanh nhưng ghi cuối có thể mất khi primary hỏng, đúng như bảng acks trên.8 Cấu hình bền vững điển hình của khoá học: replication.factor=3, min.insync.replicas=2, acks=all.7 Nghĩa là mỗi message nằm ít nhất ở 2 broker trước khi được xác nhận, và cluster chịu được mất 1 broker mà vẫn nhận ghi. Thêm unclean.leader.election.enable=false để không bao giờ nâng một replica tụt hậu lên làm leader.
Retention
Kafka giữ dữ liệu theo thời gian (retention.ms, ví dụ 7 ngày) hoặc kích thước (Grokking ch.18 có tham số retention time do người dùng chỉ định, sau đó message bị xóa).9 Một chế độ khác là log compaction: chỉ giữ message mới nhất cho mỗi key (tính năng riêng của Kafka, chưa có sách).7 Topic compacted phù hợp cho dữ liệu dạng "trạng thái hiện tại" như profile user, cấu hình, bảng tra cứu, vì consumer mới có thể dựng lại toàn bộ trạng thái bằng cách đọc từ đầu.
API cơ bản và cách xử lý trùng lặp
Ví dụ với thư viện kafkajs:
import { Kafka } from "kafkajs";
const kafka = new Kafka({ clientId: "order-svc", brokers: ["kafka-1:9092", "kafka-2:9092"] });
const producer = kafka.producer({ idempotent: true });
await producer.connect();
await producer.send({
topic: "orders",
acks: -1, // all
messages: [{ key: order.id, value: JSON.stringify({ type: "OrderPaid", ...order }) }],
});
const consumer = kafka.consumer({ groupId: "billing" });
await consumer.subscribe({ topic: "orders" });
await consumer.run({
autoCommit: false,
eachMessage: async ({ topic, partition, message }) => {
await handleOnce(message.key!.toString(), message); // idempotent
await consumer.commitOffsets([{ topic, partition, offset: (Number(message.offset) + 1).toString() }]);
},
});
Thứ tự "xử lý xong rồi mới commit offset" cho ngữ nghĩa at-least-once: consumer chết sau khi xử lý nhưng trước khi commit thì message được giao lại. Microservices Patterns §3.3.6 mô tả đúng kịch bản (client crash sau khi xử lý và cập nhật DB nhưng trước khi acknowledge) và nói exactly-once thường quá tốn kém nên broker chỉ hứa at-least-once.10 Vì vậy handler phải idempotent, ví dụ lưu event_id vào bảng có unique constraint trong cùng transaction với thay đổi nghiệp vụ (sách dùng bảng PROCESSED_MESSAGE).10
Exactly-once: hiểu đúng phạm vi
Kafka có idempotent producer (chống ghi trùng khi producer retry) và transactions (ghi nhiều partition và commit offset nguyên tử). Kết hợp lại cho exactly-once trong luồng đọc từ Kafka, xử lý, ghi lại vào Kafka, như Kafka Streams hoặc Flink. Các tính năng này chưa có trong sách; sách chỉ nói chung rằng nhiều broker hiện đại như Kafka không hỗ trợ distributed transaction với database.117
Khi handler gọi ra hệ thống ngoài (gửi email, gọi cổng thanh toán, ghi Postgres), Kafka không thể bảo đảm exactly-once cho tác dụng phụ đó. Lúc này bạn vẫn cần at-least-once cộng idempotency ở đích. Đây là chỗ nhiều ứng viên nói quá trong phỏng vấn.
Khi cần phát sự kiện từ một service sau khi ghi DB, dùng transactional outbox (Microservices Patterns ch.3 §3.3.7): ghi sự kiện vào bảng outbox trong cùng transaction, rồi một relay hoặc CDC (transaction log tailing, ví dụ Debezium) đẩy sang Kafka. Tránh trường hợp DB commit mà Kafka không nhận, hoặc ngược lại.11 Xem Change Data Capture.
Sai lầm hay gặp: Bật auto commit và xử lý bất đồng bộ. Offset được commit trước khi xử lý xong, consumer crash là mất message mà không ai biết.
Capacity và sizing
Các con số dưới đây là giả định của khoá học, cần load test; thư viện không có số liệu hiệu năng của Kafka.7 Phép tính dung lượng và băng thông theo quy tắc ước lượng của khoá học.12
| Chỉ số | Ước lượng |
|---|---|
| Thông lượng một partition | ~10 MB/s hoặc vài nghìn tới vài chục nghìn msg/s |
| Thông lượng một broker | 50–100 MB/s ghi bền vững (trước replication, giả định) |
| Độ trễ end-to-end | Vài ms tới vài chục ms với acks=all |
| Broker điển hình | 8 vCPU · 32–64 GB RAM, đĩa SSD vài TB |
| Partition mỗi broker | Vài nghìn là giới hạn an toàn |
Chọn số partition. Lấy giá trị lớn hơn của hai phép tính:
partitions >= throughput_peak / throughput_mỗi_partition
partitions >= số consumer song song cần có
Ví dụ tổng hợp click quảng cáo: peak 100.000 event/s, mỗi event 500 B (giả định của ví dụ).
100.000 × 500 B = 50 MB/s
50 / 10 MB/s = 5 partition cho thông lượng
Consumer xử lý ~5.000 event/s mỗi instance → 20 consumer
→ chọn 24–32 partition để còn chỗ tăng
Dung lượng: 50 MB/s × 86.400 s × 7 ngày ≈ 30 TB × RF 3 ≈ 90 TB
Thông lượng ghi 50 MB/s nhân RF 3 là 150 MB/s lưu lượng nội bộ, nên cần ít nhất 3–6 broker. Dung lượng 90 TB mới là yếu tố quyết định: với 12 TB đĩa mỗi broker cần khoảng 8 broker, hoặc giảm retention, hoặc dùng tiered storage đẩy segment cũ lên S3.
Mẹo: Thêm partition sau này sẽ đổi hash(key) mod N, key cũ chuyển partition và thứ tự theo key bị xáo trộn tạm thời. Hãy chọn dư từ đầu. (Cùng vấn đề "hash mod n" khi đổi số node mà Grokking ch.9 khuyên tránh ở phần Rebalance the partitions.)13
Hot partition. Nếu một key chiếm phần lớn lưu lượng (một quảng cáo viral), partition đó nghẽn; Grokking ch.9 gọi partition quá tải là hotspot.13 Cách xử lý: thêm hậu tố ngẫu nhiên vào key (adId#0..9) rồi gộp lại ở bước sau, chấp nhận mất thứ tự cho key đó (kỹ thuật của khoá học, cùng ý với sharded counter trong Grokking ch.24).14
Khi nào dùng và khi nào không
Bảng so sánh sau là nhận định của khoá học; phần sách xác nhận: Microservices Patterns §3.3.4 nêu các tiêu chí chọn broker (thứ tự, bảo đảm giao nhận, persistence, scalability, latency, competing consumers) và nói SQS chỉ hỗ trợ kênh point-to-point còn Kafka và RabbitMQ hỗ trợ cả hai kiểu.27
| Kafka | AWS SQS | GCP Pub/Sub | RabbitMQ | |
|---|---|---|---|---|
| Mô hình | Log, consumer kéo theo offset | Queue, xóa sau khi ack | Topic + subscription, push hoặc pull | Broker, exchange và queue, routing linh hoạt |
| Replay | Có, trong thời gian retention | Không | Có giới hạn (seek theo thời gian, snapshot) | Không (trừ Streams) |
| Thứ tự | Theo partition | FIFO queue theo message group | Có ordering key | Theo queue |
| Thông lượng | Rất cao, hàng GB/s một cụm | Cao, tự scale | Rất cao, tự scale | Trung bình, vài chục nghìn msg/s một node |
| Vận hành | Nặng nếu tự host | Không cần | Không cần | Vừa phải |
| Hợp nhất cho | Event streaming, CDC, analytics, nhiều consumer đọc cùng dữ liệu | Hàng đợi job đơn giản, ít vận hành | Fan-out trên GCP | Task queue, routing phức tạp, retry/delay theo message |
Dùng Kafka khi: nhiều hệ thống cần đọc cùng một luồng sự kiện, cần replay, cần thông lượng lớn, hoặc làm xương sống cho CDC và stream processing.
Không cần Kafka khi: chỉ có một hàng đợi job nền vài trăm việc mỗi giây. SQS, RabbitMQ, hoặc BullMQ trên Redis đơn giản hơn nhiều. Kafka cũng không hợp cho job cần delay theo từng message hoặc cần ack, retry riêng lẻ từng message, vì offset là theo partition. Một message lỗi chặn cả partition nếu không có dead-letter topic; Building Microservices kể một sự cố tương tự (request độc làm worker chết lặp lại) và kết luận cần giới hạn retry và một dead letter queue kèm công cụ xem và retry.15
Vận hành ở quy mô nhỏ và lớn
Nhỏ. Dùng managed: Confluent Cloud, Amazon MSK, Aiven, hoặc Redpanda Cloud (giá tuỳ nhà cung cấp). Nếu lưu lượng rất thấp, cân nhắc SQS hoặc Pub/Sub để khỏi trả tiền cho cụm chạy không. Microservices Patterns cũng nhắc message broker thêm một thành phần phải cài đặt, cấu hình và vận hành.2
Vừa và lớn. Theo dõi consumer lag (khoảng cách giữa offset mới nhất và offset đã commit). Đây là chỉ số quan trọng nhất, cho biết consumer có theo kịp không. Theo dõi thêm under-replicated partitions, kích thước ISR, đĩa, và thời gian rebalance.
Phần vận hành bên dưới là kinh nghiệm của khoá học.7 Rebalance là điểm đau (sách: broker gán lại shard khi receiver vào hoặc ra):4 mỗi lần consumer vào hoặc ra group, partition được chia lại, các consumer tạm dừng. Dùng cooperative rebalancing và static membership để giảm gián đoạn khi deploy.
Ở quy mô hàng trăm broker, nhiều công ty chia thành nhiều cụm theo mục đích, dùng MirrorMaker 2 hoặc Cluster Linking để sao chép giữa region, và dùng schema registry (Avro, Protobuf) để producer và consumer thống nhất định dạng.
Điểm nói trong phỏng vấn
- Chọn key để có thứ tự cần thiết:
orderId,userId,driverId. Nói rõ thứ tự chỉ trong partition.4 - Tính số partition từ thông lượng và số consumer, chọn dư.
- Độ bền: RF 3,
min.insync.replicas=2,acks=all. - Ngữ nghĩa giao nhận: at-least-once cộng consumer idempotent.10 Exactly-once chỉ trọn vẹn khi đầu ra cũng là Kafka.
- Phát sự kiện từ service bằng outbox hoặc CDC, không dual write.11
- Các bài hay dùng Kafka: ad click aggregator, news feed fan-out, notification system, metrics monitoring, web crawler frontier, live comments.
- Liên hệ quy trình nhiều bước khi Kafka làm kênh cho saga choreography, và scale ghi khi dùng Kafka làm bộ đệm hấp thụ đỉnh tải.
Trong phỏng vấn: Một câu gọn: "Topic clicks 32 partition, key là adId, RF 3, retention 7 ngày để replay khi sửa bug aggregator, consumer commit sau khi ghi kết quả idempotent vào OLAP store."
Nguồn trích dẫn
-
Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" · phần "Message deletion" (hai lựa chọn xóa message, Kafka dùng ý tưởng không xóa sau khi consume). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.4 Using a message broker (các tiêu chí chọn broker, bảng 3.2, lợi ích và nhược điểm, tr. 91–94). ↩ ↩2 ↩3
-
Grokking Modern System Design · ch.18 Pub-sub · bài "Design of a Pub-sub System" · mục "Broker" (topic, partition, segment, offset) và "Components" (cluster manager). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.5 Competing receivers and message ordering (kênh chia shard, shard key, consumer group, tr. 94–95). ↩ ↩2 ↩3 ↩4 ↩5
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.3.3 Adding and updating CQRS views · "Build CQRS views using archived events" (tr. 242). ↩
-
Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" · mục "Back-end service" (primary-secondary model, replication trong cluster). ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về Kafka; ISR, acks, controller/KRaft, log compaction, exactly-once/transactions, sizing và cấu hình vận hành chỉ nêu ở mức khái niệm cơ bản, mọi con số là giả định của khoá học cần load test. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Grokking Modern System Design · ch.9 Databases · bài "Data Replication" · mục "Synchronous versus asynchronous replication". ↩ ↩2
-
Grokking Modern System Design · ch.18 Pub-sub · bài "Requirements" · mục "Non-functional requirements" (Specify retention time) và "Components" (Retention time). ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages (tr. 95–97). ↩ ↩2 ↩3
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.7 Transactional messaging (Transactional outbox, Polling publisher, Transaction log tailing, Debezium). ↩ ↩2 ↩3
-
Quy tắc ước lượng của khoá học · /hoc/chon-server. ↩
-
Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning" · mục "Rebalance the partitions" (Avoid hash mod n) và phần mở đầu về hotspot. ↩ ↩2
-
Grokking Modern System Design · ch.24 Sharded Counters · mục "High-level solution sketch". ↩
-
Building Microservices (file .zip, tải về giải nén) · ch.4 Integration · mục "Implementing Asynchronous Event-Based Collaboration" (tr. 57). ↩