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
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”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.
Quy tắc cần nhớ
Phần tiêu đề “Quy tắc cần nhớ”| Đ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.
Key đi đến partition thế nào
Phần tiêu đề “Key đi đến partition thế nào”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.
createdProducer 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.
Tăng số partition làm vỡ mapping
Phần tiêu đề “Tăng số partition làm vỡ mapping”Đang tải diagram…
- Record cũ của
order-42vẫn nằm ở partition 1; record mới sang partition 2. - Hai consumer khác nhau xử lý hai partition, nên
shippedcó thể chạy trước khipaidđượ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.
Ba lớp thứ tự
Phần tiêu đề “Ba lớp thứ tự”| 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.
Chọn key
Phần tiêu đề “Chọn key”| 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.
Khi nào dùng / không dùng
Phần tiêu đề “Khi nào dùng / không dùng”- 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.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”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.
docker compose -f examples/docker-compose.yml up -d zookeeper kafkanpx --prefix examples ts-node examples/src/kafka-message-key/key-order-demo.tsdocker compose -f examples/docker-compose.yml stop kafka zookeeperKỳ 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.
Trade-offs
Phần tiêu đề “Trade-offs”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.
Checklist production
Phần tiêu đề “Checklist production”- Chọn key là id entity cần thứ tự; ghi lại quyết định này.
- Dự phòng số partition cho tăng trưởng, vì tăng sau này làm đổi mapping.
- Mọi producer (kể cả khác ngôn ngữ) dùng cùng thuật toán partition.
- Consumer
awaitside effect trước khi commit; state machine từ chối transition sai. - Đo lag và throughput theo từng partition để phát hiện hot key.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”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?