Skip to content
DevRecall
← Roadmap
17

Messaging & Kafka

drafted

Partitions, 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.
  1. 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.

  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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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. 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. 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. 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ệmMột câuVí dụ ngắn
Topic / partitionTopic chia thành partition; mỗi partition là log append-only có thứ tựorders có 12 partition
OffsetVị trí record trong partition; consumer group commit offset đã xử lýGroup billing ở offset 1042 của p3
Key → partitionDefault partitioner: murmur2(key) % numPartitions; key null → sticky partitionerkey = orderId giữ thứ tự của 1 đơn
Consumer groupMỗi partition gán cho đúng 1 consumer trong group12 partition, 4 consumer → mỗi con 3
Replication / ISRMỗi partition có leader + follower; ISR = replica bắt kịp leaderRF=3, ISR={1,2,3}
acksProducer chờ bao nhiêu replica xác nhận: 0, 1, allacks=all + min.insync.replicas=2
Idempotent producerBroker dedupe retry bằng producer id + sequence numberMặc định bật từ Kafka 3.0 (Java client); KafkaJS/confluent-js mặc định tắt → bật tường minh
TransactionsGhi nhiều partition + commit offset atomicread-process-write exactly-once
Consumer lagLog-end offset − committed offsetLag 2M trên p7 → consumer chậm/kẹt
Log compactionGiữ bản ghi mới nhất theo key; value null = tombstoneTopic customer-state
OutboxGhi event vào bảng outbox cùng transaction với business dataorders + outbox trong 1 tx
DLQNơi đậu message xử lý thất bại sau N lầnorders.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

KafkaRabbitMQSQS (+SNS)
Mô hìnhLog bền, consumer kéo (pull) theo offsetBroker định tuyến (exchange → queue), push + ackQueue managed, pull, visibility timeout
ReplayCó (theo retention/compaction)Không với classic queue (Streams thì có)Không (trừ tính năng redrive DLQ)
OrderingTheo partitionTheo queue (1 consumer)Standard: best-effort; FIFO: theo MessageGroupId
ThroughputRất cao, scale theo partitionCao vừaGầ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ànhNặng (hoặc managed: MSK/Confluent)VừaKhông có server
HợpEvent streaming, nhiều consumer độc lập, replay, CDCTask queue, routing phức tạp, RPC-ishTask queue đơn giản trên AWS, fanout với SNS
DeliveryCách đạtRủi ro
At-most-onceCommit offset trước khi xử lýMất message khi crash
At-least-onceCommit sau khi xử lýTrùng lặp → cần idempotent consumer
Exactly-once (Kafka→Kafka)Idempotent producer + transactions + read_committedKhô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=all nhưng min.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.records nhỏ 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.id cho 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.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 64 / 64

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) });
});
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 });