Có một loại cấu hình lật ngược dấu tuỳ theo tải: giúp bạn khi hệ thống bận, hại bạn khi hệ thống rảnh. Cơ chế "chờ gom thêm cho đủ một lô" là như vậy — khi tin đến dồn dập, lô tự đầy ngay và khoảng chờ gần như bằng không; khi tin thưa thớt, lô không bao giờ đầy nên bạn ngồi đợi hết giờ. Cái giá độ trễ của nó tỉ lệ nghịch với tốc độ đến, nên nó cắn mạnh nhất đúng vào lúc vắng nhất. Tôi sẽ đo đúng chỗ đó trên Kafka.
"Kafka có độ trễ cao" là câu hay được nói. Bài này đo thật, tách ra từng chặng, và tìm ra thứ gây độ trễ — nó không phải Kafka.
Cách đo
Gửi từng tin một, mỗi tin mang dấu thời gian nano giây trong thân, rồi đo hai mốc:
- ack — từ lúc
send()đến lúc callback của producer chạy. Chặng producer → broker. - e2e — từ lúc
send()đến lúc consumer nhận được. Cả đường.
Hiệu của hai số là chặng broker → consumer.
Gửi từng tin một là cố ý: nó là kịch bản tệ nhất cho gộp lô, và cũng là kịch bản của phần lớn ứng dụng phản hồi sự kiện thật.
Nền
Một broker, tin 200 byte, 2.000 lần:
| Trung bình | p50 | p99 | |
|---|---|---|---|
| producer → broker | 0,21 ms | 0,17 | 0,53 |
| producer → consumer | 0,35 ms | 0,29 | 1,08 |
| chặng consumer | 0,14 ms |
Dưới một phần ba mili giây, không chỉnh gì cả.
Kafka mặc định không chậm. Nếu hệ thống của bạn có độ trễ Kafka tính bằng trăm mili giây, gần như chắc chắn đó là cấu hình chứ không phải Kafka.
Hai bộ hẹn giờ
Hai tham số được khuyên vặn nhiều nhất trong các bài "tối ưu Kafka" đều là bộ hẹn giờ chờ dữ liệu:
| ack | e2e | So với nền | |
|---|---|---|---|
| mặc định | 0,21 ms | 0,35 ms | 1× |
linger.ms=50 |
54,10 ms | 55,53 ms | 159× |
fetch.min.bytes=65536 |
1,47 ms | 503,68 ms | 1.439× |
fetch.min.bytes=65536 + fetch.max.wait.ms=100 |
1,00 ms | 102,00 ms | 291× |
Hai con số nằm ở hai chặng khác nhau, và việc tách chặng ra làm rõ ngay chuyện gì đang xảy ra.
linger.ms=50 nằm hết ở chặng producer. ack đã là 54,10 ms — producer ngồi đợi 50 ms xem có tin nào nữa để gộp không. Không có, nên nó đợi đủ rồi mới gửi.
fetch.min.bytes=65536 nằm hết ở chặng consumer. ack vẫn 1,47 ms — tin đã nằm trên broker gần như ngay lập tức. Nhưng broker giữ lại phản hồi cho consumer tới khi gom đủ 64 KB, hoặc tới khi hết fetch.max.wait.ms. Mặc định của nó là 500 ms, và 503,68 ms đo được chính là con số đó.
Hạ fetch.max.wait.ms xuống 100 thì e2e xuống 102 ms — khớp chính xác. Không có gì bí ẩn, chỉ là một bộ hẹn giờ.
Điểm chung của cả hai: chúng chờ dữ liệu không bao giờ đến, vì tôi đang gửi từng tin một. Cả hai đều là tham số dành cho tải cao, và ở tải cao chúng gần như miễn phí — phần 5 đã đo linger.ms ở tải cao và chênh lệch chỉ 4%.
Nguy hiểm nằm ở chỗ chúng được đặt lúc tải cao rồi ở lại đó khi tải xuống thấp. Đêm khuya, ít giao dịch, và độ trễ tăng gấp nghìn lần — đúng lúc không ai nhìn bảng số.
Hai thứ hay bị đổ lỗi thì gần như không đáng kể
| ack | e2e | ||
|---|---|---|---|
| mặc định | 0,21 ms | 0,35 ms | |
acks=all |
0,22 ms | 0,36 ms | +3% |
compression.type=zstd |
0,24 ms | 0,39 ms | +11% |
acks=all không tốn gì ở đây — nhưng đó là vì replication-factor=1, không có bản sao nào để chờ. Phần 4 đã đo trên cụm ba broker và ở đó thông lượng còn một nửa. Con số +3% này chỉ đúng cho cụm một node, và tôi để nó ở đây để nhắc lại chính cái bẫy đó.
compression.type=zstd thêm 0,04 ms. Với tin 200 byte thì nén gần như không có gì để làm.
Ghi chú về phép đo: công cụ có sẵn không dùng được
Tôi bắt đầu bằng kafka-e2e-latency.sh — công cụ chính thức, nhận một tệp cấu hình. Chạy sáu cấu hình khác nhau, kể cả fetch.min.bytes=65536, và cả sáu đều ra 0,29 ms.
Sáu kết quả giống hệt nhau là dấu hiệu tệp cấu hình không có tác dụng. Công cụ này ghi đè fetch.min.bytes thành 1 sau khi nạp tệp — nó cố ý làm vậy để đo đúng độ trễ tối thiểu, và điều đó khiến nó không dùng được cho phép đo tôi cần.
Chỉ khi tự viết bộ đo, con số 503 ms mới hiện ra.
Bài học chung: kết quả giống nhau qua nhiều cấu hình khác nhau không phải phát hiện, nó là dấu hiệu phép đo hỏng. Trước khi kết luận "tham số này không ảnh hưởng gì", hãy kiểm rằng nó thật sự được áp dụng — đặt một giá trị vô lý và xem có gì thay đổi không.
Đặt thế nào
Cho hệ thống cần độ trễ thấp:
# producer
linger.ms=0
batch.size=16384 # vẫn giữ — nó không phải bộ hẹn giờ
# consumer
fetch.min.bytes=1 # mặc định
fetch.max.wait.ms=500 # không quan trọng khi fetch.min.bytes=1
max.poll.records=100
Điểm dễ nhầm: batch.size không phải bộ hẹn giờ. Với linger.ms=0, producer gửi ngay khi có thể; batch.size chỉ là trần của lô. Đặt nó lớn không làm tăng độ trễ — phần 5 đã đo, batch.size=262144 cho p50 là 1 ms.
Cho hệ thống cần thông lượng:
linger.ms=5..20
batch.size=65536
fetch.min.bytes=16384
fetch.max.wait.ms=100 # trần độ trễ khi tải xuống thấp
fetch.max.wait.ms ở đây là lưới an toàn: nó đặt trần cho độ trễ khi lưu lượng giảm. Để mặc định 500 ms là chấp nhận nửa giây độ trễ vào giờ thấp điểm.
Cái gì thật sự gây độ trễ trong hệ thống thật
Bốn con số trên đo Kafka trong một mạng cục bộ. Trong hệ thống thật, phần lớn độ trễ đầu-cuối không nằm ở Kafka:
- Consumer xử lý chậm và lag dồn lại. Đây là nguyên nhân số một, và nó không hiện ra trong bất kỳ phép đo độ trễ nào của Kafka.
- Cân bằng lại nhóm — phần 9 đo được 3 giây cho kiểu hợp tác.
- Mạng giữa các vùng, thường 50–150 ms mỗi chiều.
acks=alltrên cụm nhiều broker, phần 4 đo p50 434 ms ở tải cao.
Trước khi vặn linger.ms, hãy nhìn lag của consumer. Nếu lag đang tăng thì độ trễ của bạn là do xử lý không kịp, và không tham số producer nào chữa được.
Cùng một cái bẫy "cơ chế gộp lô có giá tỉ lệ nghịch với tải", đổi ngăn xếp
Chuyện fetch.min.bytes biến 0,35 ms thành 503 ms khi gửi thưa không phải đặc sản Kafka. Nó là bản chất của mọi cơ chế "chờ gom thêm cho đủ một lô".
Trong thế giới cơ sở dữ liệu, đây đúng là commit_delay và group commit của PostgreSQL: hệ nán lại một chút với hy vọng nhiều giao dịch cùng chung một lần fsync — dưới tải cao thì tuyệt vời (nhiều giao dịch chia nhau chi phí), nhưng lúc chỉ có một giao dịch mỗi giây thì nó chỉ cộng thêm độ trễ chờ đợi. Một write-behind cache "flush mỗi N mục hoặc N mili giây" cũng vậy: mệnh đề "hoặc N mili giây" chính là cái trần thời gian mà fetch.max.wait.ms đóng vai ở đây.
Trong thế giới ứng dụng, đây là debounce: một setTimeout chờ N mili giây xem còn sự kiện nào nữa không trước khi xử lý — mượt khi sự kiện dồn dập, nhưng thêm nguyên N mili giây độ trễ cho một sự kiện lẻ loi. Và ở tầng mạng, Nagle gộp gói nhỏ chờ ACK — cùng đúng đánh đổi đó.
Mẫu số chung: một cơ chế "chờ gom thêm việc thành lô" đánh đổi độ trễ lấy thông lượng, và cái giá độ trễ của nó tỉ lệ nghịch với tốc độ đến: gần như miễn phí khi bận (lô tự đầy tức thì), nhưng bằng đúng cả khoảng chờ khi rảnh (lô không bao giờ đầy, phải đợi hết giờ) — nên kiểu hỏng tệ nhất rơi vào lúc vắng nhất, ngược hẳn trực giác "tải thấp thì phải nhanh". Vì thế một cơ chế gộp lô luôn phải ghép với một trần thời gian để chặn trường hợp rảnh — fetch.max.wait.ms, commit_delay có giới hạn, hoặc N mili giây trong debounce. Nên câu hỏi đúng khi bật bất kỳ tối ưu gộp-lô nào không phải "nó nhanh hơn bao nhiêu lúc tải đỉnh" mà là "nó có trần thời gian chưa, và cái giá độ trễ của nó ở tải thấp là bao nhiêu — vì đó mới là chỗ nó cắn, không phải ở tải đỉnh".
Muốn tự đo, chạy thử:
kafka-e2e-latency.sh kf:9092 thu 10000 1 200
Nếu con số ra dưới 1 ms — như 0,31 ms tôi đo được — thì Kafka không phải nguồn độ trễ của bạn. Bước tiếp theo là so nó với độ trễ đầu-cuối mà ứng dụng thật sự thấy; phần chênh là chỗ đáng đi tìm.
Phần sau chuyển sang lưu trữ: dữ liệu ở lại bao lâu, và điều gì xảy ra khi hết hạn.