await db.$transaction(async (tx) => {
const inserted = await tx.$executeRaw`
INSERT INTO processed_events (event_id) VALUES (${evt.id})
ON CONFLICT DO NOTHING`;
if (inserted === 0) return; // duplicate delivery
await tx.invoice.create({ data: toInvoice(evt) });
});
Messaging & Kafka
draftedPartitions, consumer groups, ordering, delivery semantics, idempotency, outbox/CDC, DLQ, consumer lag, schema evolution, Kafka vs RabbitMQ/SQS, Node consumer pitfalls, Socket.IO scaling.
0/64 rated · mastery 0%
Bài học
12 bài · ~148 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~14 phút· 4 câuverify
Kafka là commit log: topic, partition, offset, retention, compaction
Vì sao Kafka không xoá message khi đọc: topic chia thành partition, mỗi partition là log append-only trên đĩa, offset là vị trí do consumer tự giữ; retention theo thời gian/dung lượng vs log compaction theo key, tombstone, và khác biệt với queue truyền thống. Có output thật từ cụm 3 broker Kafka 4.2.
- 02~13 phút· 5 câuverify
Key, partitioner và ordering
Key quyết định partition (murmur2 % N), nên chọn key là chọn đơn vị thứ tự và đơn vị song song. Bài này đo thật: cùng key qua ba client cho cùng partition, 75% key đổi partition khi tăng 12 → 16, tenant lớn làm một partition gánh 62% tải; giải thích vì sao producer có thể đảo thứ tự khi tắt idempotence, cách chọn số partition và cách chữa hot partition.
- 03~12 phút· 5 câuverify
Replication, ISR, acks và độ bền
Mỗi partition có leader và follower; ISR là tập replica bắt kịp leader; high watermark quyết định consumer đọc tới đâu. acks=all chỉ bền khi đi cùng min.insync.replicas: đo thật trên cụm 3 broker việc RF=3/minISR=2 vẫn ghi khi mất một broker còn RF=2/minISR=2 thì từ chối ghi. Thêm KRaft, unclean leader election, và vì sao Kafka không fsync mỗi message.
- 04~13 phút· 7 câuverify
Consumer group, offset commit và consumer lag
Group chia partition cho consumer ra sao, commit offset nghĩa là gì và auto-commit/manual commit hỏng thế nào, đo lag theo partition (kể cả cái bẫy partition chưa từng commit không hiện trong describe), điều tra lag tăng trong production, replay bằng reset offsets hoặc group mới, và SLO/alert cho pipeline streaming. Output thật từ kafka-consumer-groups trên Kafka 4.2.
- 05~13 phút· 4 câuverify
Rebalance: eager, cooperative, KIP-848 và static membership
Khi nào group rebalance, ba timeout (session, heartbeat, max.poll.interval) bảo vệ cái gì, vì sao xử lý chậm gây vòng lặp rebalance và xử lý trùng (đo thật với KafkaJS và librdkafka), so sánh eager / cooperative-sticky / KIP-848 bằng log assignment thật, và static membership cho rolling deploy trên Kubernetes.
- 06~12 phút· 5 câuverify
Delivery semantics và idempotent consumer
At-most-once, at-least-once và exactly-once thực ra là câu hỏi 'commit trước hay sau side effect'. Bài này tái hiện thật cả hai lỗi (mất order khi commit trước, tạo hoá đơn trùng khi crash sau DB commit), rồi sửa bằng bảng processed_events trong cùng transaction, upsert có version guard, và external versioning của Elasticsearch; cộng với cách xử lý email trùng sau deploy khi provider không có idempotency key.
- 07~11 phút· 4 câuverify
Idempotent producer, transactions và exactly-once
Producer id + sequence number chống trùng do retry; transactional.id + epoch fence producer zombie; transaction ghi nhiều partition và commit offset atomic; read_committed đọc tới LSO nên một transaction treo chặn cả record không thuộc transaction. Đo thật: client nào bật idempotence mặc định, dump-log thấy control marker COMMIT/ABORT, zombie bị INVALID_PRODUCER_EPOCH, và vì sao EOS không phủ được ghi Postgres.
- 08~11 phút· 5 câuverify
Poison pill, retry topic và DLQ
Một message luôn lỗi có thể chặn cả partition. Bài này phân loại lỗi transient vs permanent, so sánh blocking retry với retry topic (và cái giá về ordering), thiết kế DLQ có đủ metadata để replay, 'park' cả key khi một event của nó vào DLQ, và áp dụng cho pipeline đồng bộ dữ liệu từ provider bên ngoài. Có demo thật: retry topic + DLQ với header gốc trên Kafka 4.2.
- 09~12 phút· 6 câuverify
Dual write, transactional outbox, CDC và saga
Ghi DB rồi publish Kafka không atomic, nên mất hoặc thừa event. Transactional outbox ghi event trong cùng transaction DB, rồi relay (polling hoặc CDC/Debezium) publish at-least-once. Bài này chạy thật: relay crash sau publish tạo bản trùng, relay theo cursor id bỏ sót một row do transaction commit muộn, replication slot giữ 122 MB WAL khi connector dừng; rồi ghép thành pipeline order/payment/inventory bằng saga và bàn khi nào nên async.
- 10~13 phút· 6 câuverify
Schema evolution, thiết kế event và governance
Event là API công khai giữa các team. Bài này chạy thật Schema Registry 8.1: BACKWARD cho thêm field có default nhưng chặn field không default và đổi tên, FORWARD thì ngược lại, BACKWARD_TRANSITIVE bắt được lỗi mà BACKWARD bỏ qua; tolerant reader bằng zod; event vs command, thin event vs event-carried state transfer; header cho tracing; topic per tenant hay shared; và quy tắc ownership, đặt tên, deprecate topic.
- 11~13 phút· 8 câuverify
Kafka trong Node.js và realtime với Socket.IO
Consumer Node đúng cách: eachMessage vs eachBatch và heartbeat trong KafkaJS, bug handler fire-and-forget làm mất đơn (tái hiện thật: lag 0, 0 đơn được xử lý), graceful shutdown với SIGTERM (và vì sao consumer.on('SIGTERM') ném lỗi), back-pressure bằng pause/resume khi gặp 429, chiến lược rời KafkaJS sang @confluentinc/kafka-javascript; rồi đẩy event tới browser qua Socket.IO nhiều instance: sticky session, Redis adapter, và không mất cập nhật sau reconnect.
- 12~13 phút· 6 câuverify
Kafka, RabbitMQ, SQS và share groups: chọn thế nào
Log và queue giải hai bài toán khác nhau. So sánh mô hình của Kafka, RabbitMQ (exchange, ack, DLX, quorum queue, stream) và SQS/SNS (visibility timeout, FIFO, redrive), cái bẫy visibility timeout ngắn hơn thời gian xử lý, share groups của Kafka 4.2 (đo thật: hai consumer chia 10.000 record từ một partition, trong khi classic group để một consumer ngồi chơi), và một khung quyết định cho team sáu người.
Tóm tắt & cheat sheet của track
TL;DR
- Kafka là distributed, partitioned, replicated commit log: message không bị xoá khi đọc; consumer tự quản offset. Khác hẳn queue truyền thống (RabbitMQ/SQS) nơi broker xoá message sau khi ack.
- Thứ tự chỉ được đảm bảo trong một partition; key quyết định partition → chọn key = chọn đơn vị ordering và đơn vị song song.
- Delivery thực tế là at-least-once + idempotent consumer. "Exactly-once" của Kafka (idempotent producer + transactions) chỉ áp dụng trong phạm vi Kafka → Kafka; side effect ra ngoài (DB, email, HTTP) cần idempotency riêng.
- Bài toán senior hay hỏi: dual write (DB + publish) → transactional outbox/CDC; poison pill → retry topic + DLQ; consumer lag; rebalance storm; schema evolution.
- Kafka 4.0 (2025) bỏ hẳn ZooKeeper — cluster chạy KRaft; consumer rebalance protocol mới (KIP-848) GA ở 4.0; share groups (queue semantics, KIP-932) production-ready ở 4.2 — kiểm tra lại theo version bạn chạy.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Topic / partition | Topic chia thành partition; mỗi partition là log append-only có thứ tự | orders có 12 partition |
| Offset | Vị trí record trong partition; consumer group commit offset đã xử lý | Group billing ở offset 1042 của p3 |
| Key → partition | Default partitioner: murmur2(key) % numPartitions; key null → sticky partitioner | key = orderId giữ thứ tự của 1 đơn |
| Consumer group | Mỗi partition gán cho đúng 1 consumer trong group | 12 partition, 4 consumer → mỗi con 3 |
| Replication / ISR | Mỗi partition có leader + follower; ISR = replica bắt kịp leader | RF=3, ISR={1,2,3} |
acks | Producer chờ bao nhiêu replica xác nhận: 0, 1, all | acks=all + min.insync.replicas=2 |
| Idempotent producer | Broker dedupe retry bằng producer id + sequence number | Mặc định bật từ Kafka 3.0 (Java client); KafkaJS/confluent-js mặc định tắt → bật tường minh |
| Transactions | Ghi nhiều partition + commit offset atomic | read-process-write exactly-once |
| Consumer lag | Log-end offset − committed offset | Lag 2M trên p7 → consumer chậm/kẹt |
| Log compaction | Giữ bản ghi mới nhất theo key; value null = tombstone | Topic customer-state |
| Outbox | Ghi event vào bảng outbox cùng transaction với business data | orders + outbox trong 1 tx |
| DLQ | Nơi đậu message xử lý thất bại sau N lần | orders.dlq |
Cơ chế hoạt động
Produce → replicate → consume
Transactional outbox + CDC
Rebalance (classic protocol)
- Eager: mọi consumer revoke hết partition → stop-the-world → assign lại.
- Cooperative (incremental): chỉ revoke partition cần chuyển; consumer khác tiếp tục xử lý.
- KIP-848 (
group.protocol=consumer): broker (group coordinator) tính assignment, rebalance incremental, không còn barrier đồng bộ toàn group.
Trade-offs
| Kafka | RabbitMQ | SQS (+SNS) | |
|---|---|---|---|
| Mô hình | Log bền, consumer kéo (pull) theo offset | Broker định tuyến (exchange → queue), push + ack | Queue managed, pull, visibility timeout |
| Replay | Có (theo retention/compaction) | Không với classic queue (Streams thì có) | Không (trừ tính năng redrive DLQ) |
| Ordering | Theo partition | Theo queue (1 consumer) | Standard: best-effort; FIFO: theo MessageGroupId |
| Throughput | Rất cao, scale theo partition | Cao vừa | Gần như không giới hạn (standard) |
| Routing | Đơn giản (topic/key) | Rất linh hoạt (topic/headers/fanout exchange) | SNS filter policy + fanout |
| Vận hành | Nặng (hoặc managed: MSK/Confluent) | Vừa | Không có server |
| Hợp | Event streaming, nhiều consumer độc lập, replay, CDC | Task queue, routing phức tạp, RPC-ish | Task queue đơn giản trên AWS, fanout với SNS |
| Delivery | Cách đạt | Rủi ro |
|---|---|---|
| At-most-once | Commit offset trước khi xử lý | Mất message khi crash |
| At-least-once | Commit sau khi xử lý | Trùng lặp → cần idempotent consumer |
| Exactly-once (Kafka→Kafka) | Idempotent producer + transactions + read_committed | Không bao phủ side effect ngoài Kafka |
Pitfalls
- Thêm partition cho topic có key → mapping key→partition đổi → mất thứ tự giữa bản cũ và mới của cùng key.
- Xử lý lâu hơn
max.poll.interval.ms→ consumer bị đá khỏi group → rebalance → xử lý lại → vòng lặp. - Handler async fire-and-forget (KafkaJS) → offset commit trước khi xử lý xong → mất message khi crash.
- Retry vô hạn trên poison pill → partition kẹt, lag tăng mãi.
- Gửi message sang DLQ/retry topic cho một key nhưng vẫn xử lý message sau của cùng key → vỡ ordering.
acks=allnhưngmin.insync.replicas=1→ vẫn có thể mất dữ liệu khi chỉ leader xác nhận.- Dual write (commit DB rồi
producer.send) không atomic → mất hoặc thừa event. - Coi Redis Pub/Sub hay Socket.IO adapter là "message bus bền" — chúng at-most-once.
Cheat sheet
- Producer bền:
acks=all,enable.idempotence=true(default từ 3.0 ở Java client; Node: bật tường minh), topic RF=3,min.insync.replicas=2,unclean.leader.election.enable=false(default). - Consumer: commit sau xử lý; handler idempotent (unique
event_id, upsert, version check);max.poll.recordsnhỏ khi xử lý chậm. - Ordering: 1 key = 1 partition; idempotent producer giữ thứ tự với
max.in.flight.requests.per.connection ≤ 5. - Rebalance: cooperative-sticky hoặc KIP-848; static membership
group.instance.idcho rolling deploy. - Error: phân loại lỗi transient (retry với backoff) vs permanent (DLQ ngay); DLQ có header lý do + offset gốc; có quy trình replay.
- Schema Registry:
BACKWARD(default) → nâng cấp consumer trước;FORWARD→ producer trước;FULL→ thứ tự nào cũng được. - Lag: đo theo partition (Burrow/kafka-consumer-groups/exporter), alert theo thời gian trễ chứ không chỉ số message.
- Socket.IO nhiều node: sticky session (khi có HTTP long-polling) + adapter (Redis/Redis Streams) để broadcast giữa node.
References
- Apache Kafka documentation — Design ↗
- Apache Kafka — Producer configs ↗
- Apache Kafka — Consumer configs ↗
- Apache Kafka 4.2.0 release announcement (share groups production-ready) ↗
- KIP-848 — The Next Generation of the Consumer Rebalance Protocol ↗
- KIP-932 — Queues for Kafka ↗
- Confluent — Schema evolution and compatibility ↗
- Confluent — Exactly-once semantics in Kafka ↗
- microservices.io — Transactional outbox ↗
- Debezium — Outbox event router ↗
- Confluent JavaScript client (migration from KafkaJS) ↗
- Socket.IO — Using multiple nodes ↗
- Socket.IO — Redis adapter ↗
- AWS — Amazon SQS FIFO queues ↗
- RabbitMQ — Consumer acknowledgements and publisher confirms ↗
Interview Questions
Showing 64 / 64
await db.$transaction([
db.order.create({ data: order }),
db.outbox.create({
data: { id: randomUUID(), aggregateId: order.id, type: "OrderPlaced", payload: order },
}),
]);
await consumer.run({
eachMessage: async ({ message }) => {
const order = JSON.parse(message.value!.toString());
processOrder(order).catch((err) => logger.error(err)); // don't block the consumer
},
});
acks=1
enable.idempotence=false
retries=2147483647
max.in.flight.requests.per.connection=5
# key = null, account id is only in the payload
max.poll.records=500
max.poll.interval.ms=300000
session.timeout.ms=45000
enable.auto.commit=true
const io = new Server(httpServer, { cors: { origin: APP_URL } });
io.on("connection", (socket) => {
socket.join(`account:${socket.data.accountId}`);
});
// elsewhere, on instance that received the Kafka event:
io.to(`account:${evt.accountId}`).emit("balance", evt.balance);
// LB: round-robin, no session affinity
consumer.on("SIGTERM", async () => {
await consumer.disconnect();
});
await consumer.run({ eachMessage: handle });