Bỏ qua để đến nội dung
Knowledge Hub

Kafka message key: thứ tự theo đơn hàng chỉ nằm trong một partition Trung cấp

Thời gian đọc ước tính: 5 phút

Hệ thống đơn hàng phát created, paid, shipped vào cùng một topic nhiều partition, không gán key. Các event của cùng đơn rơi vào partition khác nhau, và Kafka không hứa thứ tự giữa các partition. Kết quả: shipped có thể được xử lý trước paid.

Câu hỏi đúng là “thứ tự của đối tượng nào?”. Với đơn hàng, orderId làm message key: producer hash key để chọn partition, nên mọi event của đơn đó nằm cùng một log và được đọc theo offset.

Điều bạn muốn Cách làm Giới hạn
Thứ tự event của một entity Key = id entity (orderId) Chỉ đúng khi số partition và partitioner không đổi
Phân tải đều Key có nhiều giá trị phân bố đều Key quá nóng tạo hot partition
Không cần thứ tự, cần throughput Không key (sticky partitioner) Không có thứ tự theo entity
Thứ tự hoàn tất side effect Xử lý tuần tự trong partition Key không bảo đảm điều này

Key không phải unique ID: hàng nghìn record có thể cùng key order-42. Nó cũng không chứng minh payload hợp lệ hay record xử lý đúng một lần.

Với record có key, default partitioner của producer Java băm key rồi lấy số dư theo số partition:

  • Docs Kafka: “If no partition is specified but a key is present, choose a partition based on a hash of the key”.
  • Docs KafkaJS nêu thuật toán là murmur2 và DefaultPartitioner của KafkaJS tương thích với Java client.

Record không key được gán partition “sticky” và đổi partition sau mỗi batch.size byte.

Cùng key đi vào một partition, consumer đọc offset theo thứ tự
Cùng key đi vào một partition, consumer đọc offset theo thứ tựBước 1/4: Producer gán key order-42 cho event created; partitioner chọn một partition.ProducerPartitionConsumercreated
Producer → Partitioncreated
Bước 1/4

Producer gán key order-42 cho event created; partitioner chọn một partition.

Công thức: partition = hash(key) mod N. Với producer khác ngôn ngữ, kiểm tra chúng dùng cùng thuật toán băm, nếu không cùng key vẫn có thể ra partition khác nhau.

Đang tải diagram…

  • Record cũ của order-42 vẫn nằm ở partition 1; record mới sang partition 2.
  • Hai consumer khác nhau xử lý hai partition, nên shipped có thể chạy trước khi paid được áp.
  • Cách an toàn: tạo topic mới với số partition mong muốn, drain topic cũ tới mốc rõ ràng rồi chuyển producer; hoặc chọn dư partition ngay từ đầu.
Lớp Bảo đảm Không bảo đảm
Producer Gửi theo thứ tự chủ ý Nhiều producer đồng thời không có thứ tự chung
Partition Append và đọc theo offset Không có thứ tự giữa các partition
Consumer effect Áp event vào DB Không bảo đảm thứ tự hoàn tất nếu xử lý async hoặc retry

Về producer: bật idempotence (mặc định ở 4.x) và giữ max.in.flight.requests.per.connection<=5 thì retry không đảo thứ tự batch trong partition (idempotent producer).

Về consumer: nếu eachMessage chỉ void doWork() rồi trả ngay, offset tiến lên khi công việc chưa xong, nên paid có thể hoàn tất trước created. Hãy await việc bền vững, hoặc dùng state machine từ chối transition sai và đưa vào DLQ thay vì bỏ qua âm thầm.

Key Thứ tự Rủi ro
orderId Theo đơn: thường đủ Ít khi nóng
customerId Theo khách Khách lớn gom quá nhiều đơn vào một partition
eventId hoặc UUID mới mỗi lần Không có Event cùng đơn không còn cùng partition
Không key Không có Phân tải đều nhất

Khuyến nghị: mặc định chọn id của entity nhỏ nhất cần thứ tự (đơn, shipment), không phải id của tenant.

  • Dùng key khi consumer áp event theo trạng thái của entity (state machine, projection).
  • Không cần key khi event độc lập (log, metric, job không phụ thuộc nhau).
  • Đừng dựa vào key để chống trùng: đó là việc của idempotency ở consumer.

Demo (hai partition, ba record cùng key, replication factor 1). Demo dùng broker Kafka 3.6 trong compose của repo; cơ chế key-to-partition giống ở 4.x.

Terminal window
docker compose -f examples/docker-compose.yml up -d zookeeper kafka
npx --prefix examples ts-node examples/src/kafka-message-key/key-order-demo.ts
docker compose -f examples/docker-compose.yml stop kafka zookeeper

Kỳ vọng: ba dòng cùng partition, offset tăng, value theo thứ tự created, paid, shipped. Log JSON của KafkaJS, cảnh báo partitioner và TimeoutNegativeWarning là nhiễu của client.

Dùng key theo entity

  • Event của cùng entity nằm một partition, đọc theo offset.
  • Các key khác nhau vẫn chia tải giữa các consumer.
  • Chọn được phạm vi thứ tự theo nghiệp vụ.

Giới hạn đi kèm

  • Key rất nóng làm một partition thành nút nghẽn.
  • Tăng partition hoặc đổi partitioner làm đổi mapping.
  • Thứ tự offset không tạo idempotency hay thứ tự hoàn tất side effect.
  1. Chọn key là id entity cần thứ tự; ghi lại quyết định này.
  2. Dự phòng số partition cho tăng trưởng, vì tăng sau này làm đổi mapping.
  3. Mọi producer (kể cả khác ngôn ngữ) dùng cùng thuật toán partition.
  4. Consumer await side effect trước khi commit; state machine từ chối transition sai.
  5. Đo lag và throughput theo từng partition để phát hiện hot key.

Tự kiểm tra: (1) Vì sao offset 5 của partition 0 không “sớm hơn” offset 6 của partition 1? (2) Tăng partition từ 3 lên 4 làm gì với record mới của order-42? (3) Vì sao eachMessage trả ngay sau void sendEmail() phá thứ tự effect?