Đây là một trong những hiểu nhầm phổ biến nhất về Kafka: "Kafka giữ thứ tự message". Đúng một nửa — và nửa sai có thể gây bug cực khó lần. Kafka chỉ đảm bảo thứ tự trong một partition, không đảm bảo thứ tự giữa các partition. Khi hệ còn nhỏ (một partition) mọi thứ có vẻ ổn; nhưng ngay khi bạn scale ra nhiều partition để tăng throughput (như bài 3), thứ tự toàn topic vỡ — và nếu nghiệp vụ của bạn phụ thuộc thứ tự (sự kiện "nạp tiền" phải xử lý trước "rút tiền"), hậu quả là dữ liệu sai một cách tinh vi. Bài này (phần 8 loạt Message Queue) chạy thật để thấy chính xác thứ tự vỡ khi nào và cách dùng key để cứu.

Partition là đơn vị đảm bảo thứ tự

Mỗi partition là một log được sắp xếp — message trong đó có offset tăng dần và consumer đọc đúng thứ tự đó. Nhưng một topic có nhiều partition, và:

  • Message không key được rải round-robin (hoặc theo batch) qua các partition. Thứ tự giữa các partition không được đảm bảo — consumer đọc toàn topic sẽ thấy message xen kẽ lộn xộn.
  • Message có key được định tuyến bằng partition = hash(key) % số_partition. Nghĩa là mọi message cùng key luôn vào cùng một partition → giữ nguyên thứ tự theo key, dù topic có bao nhiêu partition.

Đây là chìa khoá: thứ tự trong Kafka là thứ tự theo key, không phải theo topic. Chọn key đúng (thực thể cần giữ thứ tự: user_id, order_id, account_id) là cách bạn có được đảm bảo thứ tự mà vẫn scale song song.

# (a) 1 partition: thứ tự toàn cục được giữ
kafka-topics.sh --create --topic one --partitions 1
# (b) 3 partition không key: đọc toàn topic → lộn
kafka-topics.sh --create --topic three --partitions 3
# (c) 3 partition CÓ key: giữ thứ tự mỗi key
kafka-console-producer.sh --property parse.key=true \
  --property key.separator=:        # gửi "userA:ev1"
kafka-console-consumer.sh --partition N --from-beginning   # đọc riêng 1 partition

Ảnh chụp code nền tối thứ tự message chỉ đảm bảo trong một partition, quy tắc thứ tự được giữ trong một partition theo offset tăng không có thứ tự giữa các partition partition bằng hash key chia số partition cùng key cùng partition giữ thứ tự theo key không key rải round-robin mất thứ tự toàn cục, demo thật Kafka CLI một partition thứ tự toàn cục được giữ ba partition không key đọc toàn topic lộn ba partition có key giữ thứ tự mỗi key kafka-console-producer parse key true key separator gửi userA ev1 kafka-console-consumer partition N đọc riêng một partition

Hình 1: Kafka giữ thứ tự trong một partition (offset tăng dần), không giữa các partition. Message không key rải round-robin (mất thứ tự toàn cục); message có key đi theo hash(key) nên cùng key luôn cùng partition (giữ thứ tự theo key). Bên dưới là ba kịch bản demo.

Đo thật: ba kịch bản, ba kết quả

Mình chạy ba kịch bản trên kafka-lab, đọc theo partition để thấy rõ:

Ảnh chụp output thật nền tối partition giữ thứ tự toàn topic thì không Kafka CLI đọc theo partition, a 1 partition produce ev-1 tới 10 consume ev-1 ev-2 ev-3 tới ev-10 đúng thứ tự toàn cục, b 3 partition không key produce ev-1 tới 12 đọc toàn topic ev-3 ev-8 ev-7 ev-12 ev-1 ev-2 ev-4 lộn nhưng riêng từng partition vẫn tăng đều P0 1 2 4 5 6 9 10 11 P1 3 8 P2 7 12 mỗi P tăng, c 3 partition có key user A và B xen kẽ P0 userB B-ev1 tới B-ev6 P1 userA A-ev1 tới A-ev6 cùng user cùng partition thứ tự mỗi user giữ nguyên, cần thứ tự đặt key theo thực thể user_id order_id thứ tự toàn cục chỉ có khi 1 partition đánh đổi song song

Hình 2: Kết quả thật. (a) 1 partition: consume ra đúng ev-1..ev-10. (b) 3 partition không key: đọc toàn topic lộn (ev-3, ev-8, ev-7, ev-12, ev-1...) nhưng riêng từng partition vẫn tăng đều (P0: 1,2,4,5,6,9,10,11; P1: 3,8; P2: 7,12). (c) 3 partition có key user: toàn bộ event userA vào P1 theo thứ tự, userB vào P0 theo thứ tự — thứ tự mỗi user giữ nguyên.

Đọc kết quả:

  • (a) 1 partition → thứ tự toàn cục đúng: produce ev-1 tới ev-10, consume ra chính xác ev-1..ev-10. Với một partition, Kafka là một hàng đợi FIFO thuần — thứ tự tuyệt đối. Nhưng nhớ bài 3: một partition = một consumer tối đa, không scale được.
  • (b) 3 partition, không key → thứ tự toàn cục vỡ: produce ev-1 tới ev-12, nhưng đọc toàn topic ra ev-3, ev-8, ev-7, ev-12, ev-1, ev-2, ev-4... — lộn hoàn toàn. Vì message rải qua 3 partition và consumer đọc từng partition theo nhịp riêng. Tuy nhiên, đọc riêng từng partition thì vẫn tăng đều (P0: 1,2,4,5,6,9,10,11). Thứ tự không mất trong partition — nó chỉ mất giữa các partition.
  • (c) 3 partition, có key → thứ tự theo key giữ nguyên: mình gửi event của userA và userB xen kẽ, key = user. Kết quả: toàn bộ event userA (A-ev1..A-ev6) vào P1 theo đúng thứ tự, toàn bộ userB (B-ev1..B-ev6) vào P0 theo đúng thứ tự. Dù hai user xen kẽ lúc produce và topic có 3 partition, thứ tự của mỗi user được bảo toàn hoàn hảo — vì cùng key luôn rơi vào cùng partition.

Thông điệp cốt lõi: bạn không cần thứ tự toàn cục — bạn cần thứ tự theo thực thể. Ngân hàng không cần "mọi giao dịch của mọi tài khoản theo đúng thứ tự toàn cục"; nó cần "mọi giao dịch của cùng một tài khoản theo đúng thứ tự". Đặt key = account_id cho bạn chính xác điều đó, và vẫn scale được vì các tài khoản khác nhau rải qua nhiều partition.

Vì sao đây là đánh đổi với song song

Đây là một trong những đánh đổi nền tảng của Kafka, nối thẳng với bài 3 (consumer group). Thứ tự và song song kéo ngược nhau:

  • Thứ tự toàn cục tuyệt đối cần một partition → một consumer → không song song, throughput trần thấp.
  • Song song cao cần nhiều partition → mất thứ tự toàn cục.
  • Key là điểm cân bằng: thứ tự theo key + song song giữa các key. Đây gần như luôn là lựa chọn đúng, vì nghiệp vụ thực tế hiếm khi cần thứ tự toàn cục mà cần thứ tự theo thực thể.

Đánh đổi cần cân nhắc

Key lệch (hot key) phá vỡ cân bằng tải. Nếu một key chiếm phần lớn lưu lượng (ví dụ một "user VIP" tạo 50% event), mọi message của key đó dồn vào một partition → partition đó quá tải trong khi các partition khác nhàn. Key cho bạn thứ tự nhưng cũng tạo nguy cơ phân bố lệch. Chọn key có phân bố đều (user_id thường ổn; "country" thì không nếu 90% user cùng một nước). Khi hot key là vấn đề, cân nhắc key phức tạp hơn (user_id + bucket) — đánh đổi lại một phần thứ tự.

Đổi số partition làm lộn định tuyến key. partition = hash(key) % số_partition — nếu bạn tăng số partition sau khi topic đã chạy, công thức đổi, và message cùng key có thể bắt đầu vào partition khác so với message cũ → thứ tự theo key vỡ ở ranh giới thay đổi. Đây là lý do (cùng với bài 3) phải chọn số partition đủ lớn từ đầu: tăng partition không chỉ đau về rebalance mà còn phá đảm bảo thứ tự theo key.

Thứ tự chỉ được giữ tới consumer — xử lý song song trong consumer lại phá nó. Kafka giao message theo thứ tự trong partition, nhưng nếu consumer của bạn xử lý chúng bằng nhiều thread/goroutine song song để nhanh hơn, thứ tự lại vỡ ở tầng xử lý — dù Kafka đã giao đúng thứ tự. Giữ thứ tự end-to-end nghĩa là xử lý tuần tự trong một partition (hoặc theo key trong consumer), đánh đổi với tốc độ xử lý. Đảm bảo thứ tự của Kafka chỉ có giá trị nếu consumer tôn trọng nó.

Ba ý mang về

  1. Kafka giữ thứ tự trong partition, không toàn topic: đo thật 1 partition cho ev-1..10 đúng thứ tự; 3 partition không key làm đọc toàn topic lộn (ev-3,8,7,12,1...) nhưng mỗi partition riêng vẫn tăng đều — thứ tự mất giữa partition, không trong partition.
  2. Key cho thứ tự theo thực thể + song song giữa thực thể: đo thật dùng key user, toàn bộ event userA vào một partition theo thứ tự, userB vào partition khác theo thứ tự — nghiệp vụ cần thứ tự theo thực thể (account_id, order_id), không phải toàn cục.
  3. Thứ tự và song song kéo ngược nhau, và có nhiều cạm bẫy: thứ tự toàn cục = 1 partition = không song song; key cân bằng hai thứ nhưng hot key gây lệch tải, đổi số partition phá định tuyến key, và consumer xử lý song song lại vỡ thứ tự dù Kafka giao đúng.

Nguồn

Phần sau ta sang một kiến trúc dùng chính log message làm nguồn sự thật: event sourcing — lưu chuỗi sự kiện thay vì trạng thái, demo thật dựng lại trạng thái bằng cách replay toàn bộ event.