"Kafka giữ thứ tự tin nhắn" là câu đúng một nửa, và nửa sai là nửa gây sự cố. Bài này đo chính xác cái gì được bảo đảm.
Thứ tự toàn cục không tồn tại
Gửi 12 tin đánh số theo đúng thứ tự vào topic 4 partition, rồi đọc lại bằng một consumer:
gửi: 1 2 3 4 5 6 7 8 9 10 11 12
nhận: 1 5 9 2 3 4 6 7 8 10 11 12
Tin 5 và 9 nhảy lên trước tin 2.
Đọc riêng từng partition thì mọi thứ ngăn nắp:
p0: 1 5 9
p1: (rỗng)
p2: (rỗng)
p3: 2 3 4 6 7 8 10 11 12
Trong mỗi partition, thứ tự đúng tuyệt đối. Consumer đọc bốn partition song song và trả về theo lô — nên thứ tự tổng hợp là thứ tự nào cũng có thể.
Đây là điều Kafka thật sự hứa: thứ tự trong một partition, không phải thứ tự trong một topic.
Hệ quả thực dụng: nếu nghiệp vụ của bạn cần "đơn hàng phải được xử lý theo đúng thứ tự phát sinh", câu đó chỉ đúng khi bạn nói rõ theo từng cái gì. Theo từng đơn hàng thì dùng mã đơn làm khoá. Theo toàn hệ thống thì bạn cần một partition duy nhất, và mất hết khả năng đọc song song.
Tăng partition làm 45% khoá đổi chỗ
20 khoá, gửi lần một khi topic có 4 partition, rồi --alter --partitions 8 và gửi lại đúng 20 khoá đó:
| Khoá | 4 part | 8 part | Khoá | 4 part | 8 part | |
|---|---|---|---|---|---|---|
| KH1 | p3 | p3 | KH11 | p1 | p5 | |
| KH5 | p3 | p7 | KH14 | p0 | p4 | |
| KH7 | p3 | p7 | KH17 | p3 | p7 | |
| KH8 | p1 | p5 | KH18 | p1 | p5 | |
| KH9 | p2 | p6 | KH20 | p3 | p7 |
9 trong 20 khoá đổi partition.
Hậu quả không dừng ở việc chia lại tải. Với những khoá đó, tin cũ nằm ở partition này và tin mới nằm ở partition khác. Hai partition được đọc bởi hai consumer khác nhau, chạy độc lập, không có gì đồng bộ giữa chúng. Thứ tự giữa tin cũ và tin mới của cùng một khoá không còn được bảo đảm — vĩnh viễn.
Ví dụ cụ thể: KH8 là mã một tài khoản. Lệnh "nạp 100.000" nằm ở p1, lệnh "rút 50.000" gửi sau khi tăng partition nằm ở p5. Consumer đọc p5 có thể xử lý lệnh rút trước lệnh nạp.
Không có cách sửa. Tăng partition trên topic có khoá là thao tác một chiều và phá vỡ ngữ nghĩa. Nếu buộc phải tăng, cách an toàn là:
- Dừng producer.
- Đợi consumer đọc hết (lag về 0).
- Tăng partition.
- Bật producer lại.
Lúc đó dữ liệu cũ đã xử lý xong, và không còn tin cũ nào chờ để bị vượt mặt.
Phân bố vẫn lệch ở cả hai cấu hình
20 khoá vào 4 partition: p0=2 p1=5 p2=4 p3=9
20 khoá vào 8 partition: p0=1 p1=2 p2=3 p3=5 p4=1 p5=3 p6=1 p7=4
Partition nặng nhất gấp 4,5 lần partition nhẹ nhất ở cấu hình 4, và gấp 5 lần ở cấu hình 8. Tăng partition không làm phân bố đều hơn.
Đây là cùng bài học ở phần 3, nhắc lại vì nó hay bị bỏ qua: hàm băm chỉ đều khi có nhiều giá trị để băm. Với 20 khoá thì luật số lớn chưa kịp có tác dụng. Cần vài nghìn khoá phân biệt trở lên mới nói đến chuyện "chia đều".
Bốn điều được bảo đảm
- Thứ tự trong một partition. Chắc chắn, không điều kiện.
- Cùng khoá vào cùng partition — nhưng chỉ khi số partition không đổi.
- Offset tăng đơn điệu và không bao giờ dùng lại, kể cả sau khi broker khởi động lại hay đổi leader.
- Không mất tin đã ghi, với
acks=allvàmin.insync.replicashợp lý (phần 4).
Bốn điều không được bảo đảm
- Thứ tự giữa các partition. Đã đo ở trên.
- Thứ tự sau khi tăng số partition. Đã đo ở trên.
- Thứ tự khi consumer xử lý bất đồng bộ. Đây là chỗ hay bị mất nhất trong hệ thống thật: consumer đọc theo thứ tự rồi đẩy vào một thread pool. Kafka làm đúng phần của nó, ứng dụng phá phần còn lại. Nếu cần thứ tự, luồng xử lý phải nối tiếp theo từng partition.
- Thứ tự trong một partition nếu tắt idempotence mà bật thử lại. Với
max.in.flight.requests.per.connection=5vàenable.idempotence=false, lô số 2 có thể được ghi trước lô số 1 khi lô 1 phải gửi lại. Từ Kafka 3.0 idempotence bật mặc định nên chuyện này gần như không còn gặp — nhưng cấu hình cũ chép lại từ các bài viết cũ vẫn tắt nó.
Chọn khoá thế nào
Khoá quyết định ba thứ cùng lúc: thứ tự, phân bố tải, và khả năng đọc song song. Chúng kéo nhau ngược chiều.
- Khoá quá thô (
region,tenant,event_type— vài chục giá trị): partition rỗng, tải lệch, và một consumer gánh hết. Đã đo ở phần 3. - Khoá quá mịn (mã sự kiện duy nhất): phân bố hoàn hảo nhưng không giữ được thứ tự gì cả, vì mỗi tin một khoá thì không có hai tin nào cùng partition.
- Khoá vừa đúng: đơn vị nghiệp vụ nhỏ nhất mà bạn cần giữ thứ tự — mã đơn hàng, mã tài khoản, mã phiên. Nhiều giá trị (nên chia đều) và có ý nghĩa (nên giữ đúng thứ tự cần giữ).
Quy tắc: khoá là thứ bạn cần giữ thứ tự theo nó. Không phải thứ bạn muốn nhóm lại để tiện đọc, không phải thứ nghe có vẻ hợp lý.
Nếu một khoá cụ thể quá nóng — ví dụ một khách hàng lớn chiếm 40% lưu lượng — cách chữa là thêm thành phần vào khoá: KH123-01, KH123-02, ... Nó chia khách hàng đó ra nhiều partition, và bạn chấp nhận mất thứ tự giữa các nhánh trong khi vẫn giữ thứ tự trong mỗi nhánh. Đó là đánh đổi có ý thức, chứ không phải tai nạn.
Thử ba mươi giây
Xem khoá của bạn phân bố ra sao, trước khi phải sửa:
kafka-console-consumer.sh --bootstrap-server kf:9092 --topic topic-cua-ban \
--from-beginning --max-messages 10000 --timeout-ms 30000 \
--property print.partition=true | cut -f1 | sort | uniq -c | sort -rn
Nếu partition nặng nhất gấp hơn hai lần partition nhẹ nhất, khoá của bạn quá thô — và consumer đọc partition đó sẽ luôn là cái tụt lại sau.
Phần sau đo bản sao: ISR, leader, và chuyện gì thật sự xảy ra khi một broker chết.