Bài 1
Khoá nào rơi vào partition nào
Kafka chỉ bảo đảm thứ tự bên trong một partition. Vì vậy câu hỏi “khoá của tôi rơi vào partition nào” không phải chi tiết cài đặt: nó quyết định hai bản ghi của cùng một khách hàng có được đọc theo đúng thứ tự ghi hay không. Bài này tính ra câu trả lời bằng đúng công thức producer dùng, rồi đối chiếu với 256 cặp khoá–partition đo trên Kafka 4.3.1.
Vì sao câu hỏi này quan trọng
Một topic có P partition là P hàng đợi độc lập. Consumer đọc song song từ nhiều partition, và không có gì đồng bộ giữa chúng: bản ghi ở partition 3 có thể được xử lý trước bản ghi ghi sớm hơn ở partition 0. Điều duy nhất Kafka hứa là trong một partition, thứ tự đọc bằng thứ tự ghi.
Nên nếu hai bản ghi cần giữ thứ tự với nhau — “đơn KH07 tạo”, rồi “đơn KH07 huỷ” — chúng phải nằm chung một partition. Cách duy nhất để bảo đảm điều đó là cho chúng cùng một khoá. Phần còn lại của bài là: cùng khoá thì thật sự rơi vào đâu, và khi nào lời hứa đó gãy.
Công thức thật
Khi bản ghi có khoá, producer Java tính partition bằng đúng một dòng
(BuiltInPartitioner.partitionForKey):
partition = (murmur2(khoá dạng byte) & 0x7fffffff) % số_partition
Ba chi tiết đáng nhớ, vì cả ba đều là chỗ hay hiểu sai:
- Hàm băm là murmur2 với seed
0x9747b28c, không phảihashCode()của Java. Một bản sao chép bằng ngôn ngữ khác mà dùng hàm băm khác sẽ đẩy cùng một khoá sang partition khác. & 0x7fffffffchỉ tắt bit dấu chứ không lấy trị tuyệt đối. Khác biệt lộ ra đúng ởInteger.MIN_VALUE: tắt bit dấu cho 0, cònMath.absthì tràn số và trả lại chính nó.- Số partition nằm trong công thức, nên đổi số partition là đổi kết quả. Đây là nội dung của phần sau.
Ba khoá đầu của bộ đo, tính từng bước:
| khoá | murmur2 | sau khi tắt bit dấu | % 3 | % 8 |
|---|---|---|---|---|
| KH01 | 134 561 646 | 134 561 646 | 0 | 6 |
| KH07 | −1 895 882 538 | 251 601 110 | 2 | 6 |
| KH32 | 1 810 218 049 | 1 810 218 049 | 1 | 1 |
Cách đo: ghi 32 khoá KH01…KH32 vào tám topic có 1, 2, 3, 4, 5, 6, 8
và 12 partition, rồi đọc lại và in kèm số hiệu partition.
kafka-console-producer.sh --topic t8 \
--property parse.key=true --property key.separator=: < keys.txt
kafka-console-consumer.sh --topic t8 --from-beginning \
--property print.key=true --property print.partition=true
# Partition:6 KH01
# Partition:1 KH02
js/kafka-partition.js cho đúng cả 256. Con số đó là lý do phòng thí nghiệm bên
dưới đáng tin: nó không minh hoạ, nó tính đúng thứ Kafka tính.
Đổi số partition: bao nhiêu khoá phải sang chỗ khác
Vì số partition nằm trong phép chia lấy dư, thêm partition sẽ đẩy một phần khoá sang partition
khác. Bản ghi đã ghi thì nằm nguyên chỗ cũ — Kafka không chuyển dữ liệu khi bạn
--alter --partitions. Hệ quả: với những khoá đổi chỗ, bản ghi cũ ở một partition
và bản ghi mới ở partition khác, và thứ tự giữa chúng không còn được bảo đảm nữa.
Đo trực tiếp: topic 3 partition, ghi 32 khoá, nâng lên 6 partition, ghi lại đúng 32 khoá đó.
kafka-topics.sh --alter --topic grow --partitions 6
# WARNING: If partitions are increased for a topic that has a key,
# the partition logic or ordering of the messages will be affected
Kết quả: 16 trên 32 khoá đổi partition — đúng một nửa, như phép chia lấy dư
dự đoán khi nhân đôi số partition. Ví dụ KH03 đi từ partition 0 sang 3,
KH05 từ 1 sang 4, còn KH01 và KH07 ở nguyên.
Toàn bộ ma trận, đếm trên cùng 32 khoá đó:
| từ → tới | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16 |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 23 | 23 | 27 | 30 | 29 | 31 | 32 |
| 2 | 23 | 0 | 20 | 17 | 20 | 24 | 24 | 29 |
| 3 | 23 | 20 | 0 | 23 | 16 | 28 | 23 | 31 |
| 4 | 27 | 17 | 23 | 0 | 18 | 15 | 18 | 27 |
| 6 | 30 | 20 | 16 | 18 | 0 | 24 | 16 | 30 |
| 8 | 29 | 24 | 28 | 15 | 24 | 0 | 22 | 19 |
| 12 | 31 | 24 | 23 | 18 | 16 | 22 | 0 | 27 |
Nhân đôi là trường hợp nhẹ nhất: 3→6 đổi 16 khoá, 4→8 đổi 15, 6→12 đổi 16. Những bước nhảy khác đắt hơn: 3→8 đổi 28 trên 32, và 1→2 đổi 23 vì khi có một partition thì mọi khoá đều ở partition 0, nên chỉ những khoá tình cờ được chia về 0 mới đứng yên.
Bản ghi không có khoá không đi vòng tròn
Niềm tin phổ biến là khoá null thì producer gửi lần lượt từng partition. Đo lại
thì không phải: gửi 30 bản ghi không khoá vào một topic 6 partition, trong một phiên
producer, thì cả 30 rơi vào đúng một partition.
kafka-console-producer.sh --topic nokey < 30-dong.txt
kafka-console-consumer.sh --topic nokey --from-beginning \
--property print.partition=true | sort | uniq -c
# 30 Partition:5
Chạy lần thứ hai bằng một phiên producer khác, 30 bản ghi tiếp theo cùng rơi vào một partition
— nhưng là partition 3. Đây là bộ phân vùng “dính” (sticky) mà producer dùng từ Kafka 2.4 và
đưa vào làm mặc định từ 3.3: nó chọn một partition rồi gửi vào đó cho tới khi lô đầy hoặc hết
linger.ms, vì gom thành lô lớn rẻ hơn rải đều.
Nhiều partition hơn không có nghĩa là đều hơn
Hàm băm chia đều về mặt kỳ vọng, không phải về mặt thực tế trên một tập khoá cụ thể. Với đúng 32 khoá ở trên:
| số partition | lý tưởng mỗi partition | thật | nhiều nhất | partition rỗng |
|---|---|---|---|---|
| 2 | 16,00 | 9, 23 | 23 | 0 |
| 4 | 8,00 | 5, 10, 4, 13 | 13 | 0 |
| 8 | 4,00 | 3, 5, 1, 8, 2, 5, 3, 5 | 8 | 0 |
| 12 | 2,67 | 1, 7, 1, 5, 1, 1, 1, 3, 3, 2, 2, 5 | 7 | 0 |
| 16 | 2,00 | 0, 3, 0, 2, 0, 4, 1, 3, 3, 2, 1, 6, 2, 1, 2, 2 | 6 | 3 |
| 24 | 1,33 | 1, 3, 0, 4, 0, 0, 1, 1, 1, 1, 1, 2, 0, 4, 1, 1, 1, 1, 0, 2, 2, 1, 1, 3 | 4 | 5 |
Ở 8 partition, partition đông nhất giữ 8 khoá trong khi lý tưởng là 4 — gấp đôi — và partition vắng nhất giữ 1. Ở 16 partition đã có 3 partition không nhận khoá nào, ở 24 thì 5. Thêm partition làm tăng khả năng song song tối đa, nhưng với một tập khoá hữu hạn nó cũng làm tải lệch hơn, vì mỗi partition nhận ít khoá hơn nên một khoá lẻ nặng hơn về tỉ trọng.
Đây mới chỉ là lệch theo số khoá. Trong hệ thật, số bản ghi của mỗi khoá còn khác nhau, nên một khoá nóng (một khách hàng lớn, một thiết bị gửi liên tục) sẽ dồn hết vào một partition và không có cách nào chia nhỏ nó mà vẫn giữ thứ tự theo khoá.
Phòng thí nghiệm
Gõ khoá của chính bạn, chọn số partition, rồi xem chúng rơi vào đâu. Ô thứ hai là số partition
sau khi nâng: lab sẽ đánh dấu những khoá phải đổi chỗ. Thuật toán là bản viết lại của
BuiltInPartitioner.partitionForKey và đã khớp 256/256 với Kafka 4.3.1.
Khoá
Các partition
Số partition
Lệch tải
Nhớ gì
- Cùng khoá thì cùng partition, và đó là cách duy nhất giữ thứ tự giữa hai bản ghi.
- Công thức là
murmur2(khoá) & 0x7fffffffrồi chia lấy dư cho số partition — nên số partition là một phần của ánh xạ. - Nâng 3 lên 6 partition làm 16 trên 32 khoá đổi chỗ; Kafka không chuyển dữ liệu cũ theo.
- Khoá
nullkhông đi vòng tròn: 30 bản ghi trong một phiên producer rơi hết vào một partition. - Với 32 khoá trên 8 partition, partition đông nhất giữ gấp đôi mức lý tưởng; lên 16 partition thì 3 partition không nhận khoá nào.
Bài tiếp theo trả lời nửa còn lại của câu chuyện: ai đọc partition nào, và vì sao có lúc 9 partition mà chỉ 3 trong 10 consumer làm việc.