Hai nhóm tham số này quyết định phần lớn hiệu năng producer. Một trong hai cho kết quả ngược hẳn với cách người ta thường mô tả, và cái còn lại thì suýt khiến tôi viết một bài sai.

batch.size, linger.ms và bốn thuật toán nén trên hai loại dữ liệu

Gộp lô làm độ trễ giảm

Producer không gửi từng tin. Nó gom vào lô theo partition, gửi khi lô đầy batch.size byte hoặc khi hết linger.ms mili giây — cái nào tới trước.

Cách kể quen thuộc: "gộp lô đổi độ trễ lấy thông lượng". Đo 300.000 tin × 300 byte:

batch.size Thông lượng p50
1 46.203 tin/giây 1.567 ms
1.024 139.794 tin/giây 579 ms
16.384 (mặc định) 653.594 tin/giây 1 ms
65.536 755.667 tin/giây 0 ms
262.144 761.421 tin/giây 1 ms

Từ 1 lên 16.384: thông lượng gấp 14 lần, và độ trễ trung vị giảm từ 1.567 ms xuống 1 ms. Không có đánh đổi nào cả — cả hai cùng tốt lên.

Lý do: khi không gộp, mỗi tin là một yêu cầu mạng riêng, và producer chỉ cho phép một số yêu cầu bay cùng lúc. Tin thứ 300.000 phải xếp hàng sau 299.999 yêu cầu. Con số 1.567 ms kia là thời gian xếp hàng, không phải thời gian truyền.

Đánh đổi chỉ xuất hiện khi tải thấp, và nó nằm ở linger.ms chứ không ở batch.size.

Từ 65.536 trở lên thì hết tác dụng: 755.667 rồi 761.421, chênh chưa tới 1%. Điểm bão hoà đã tới.

linger.ms gần như không ảnh hưởng gì

Đây là tham số bị vặn nhiều nhất trong các bài hướng dẫn tối ưu. Đo ở tải cao:

linger.ms=0     617.283 tin/giây
linger.ms=1     617.283 tin/giây
linger.ms=5     641.025 tin/giây
linger.ms=20    591.715 tin/giây
linger.ms=100   598.802 tin/giây

Chênh lệch 4%, không theo chiều nào rõ rệt. Ở tải cao lô tự đầy trước khi hết giờ chờ, nên bộ hẹn giờ không bao giờ được dùng tới.

Ở tải thấp — 5.000 tin/giây — nó mới có tác dụng, và tác dụng đó là tiêu cực:

linger.ms Thông lượng p50
0 4.991 tin/giây 0 ms
20 4.985 tin/giây 6 ms
100 4.978 tin/giây 6 ms

Thông lượng y hệt, độ trễ tăng. Ở tải thấp không có gì để gộp nên linger.ms chỉ là thời gian ngồi đợi.

Kết luận thực dụng: vặn batch.size, để linger.ms yên. Nếu bạn đang chạy mặc định 16.384 và muốn nhanh hơn, tăng lên 65.536 và cấp thêm buffer.memory. Đặt linger.ms=20 như nhiều bài khuyên chỉ có ích khi tải của bạn thấp và bạn thật sự cần lô to hơn — nhưng lúc đó thông lượng đâu phải vấn đề.

Nén: suýt viết một bài sai

Tôi chạy bốn thuật toán bằng công cụ đo mặc định, 300.000 tin × 512 byte:

Thông lượng Trên đĩa
none 502.512 156.592.800 byte
snappy 493.421 156.685.056 byte
lz4 456.621 156.039.440 byte
gzip 96.277 99.588.025 byte
zstd 306.435 96.609.250 byte

snappy cho ra tệp lớn hơn bản không nén. lz4 tiết kiệm 0,4%. gzip nén được 36% nhưng thông lượng còn 19%.

Nếu dừng ở đây, bài này sẽ kết luận "nén không đáng". Kết luận đó sai, và lý do nằm ở dữ liệu: công cụ đo sinh byte ngẫu nhiên. Dữ liệu ngẫu nhiên không nén được — đó là định nghĩa của ngẫu nhiên. Bảng trên đo trường hợp xấu nhất tuyệt đối, không đo trường hợp của bạn.

Chạy lại với --payload-file chứa JSON đơn hàng thật:

{"eventType":"ORDER_CREATED","orderId":"ORD-000001","customerId":"CUST-00001",
 "status":"PENDING","currency":"VND","amount":137,"warehouse":"HCM-01",
 "createdAt":"2026-08-29T10:01:07Z","channel":"WEB"}
Thông lượng Trên đĩa Tỉ lệ Độ trễ tb
none 967.741 63.475.779 1,0× 19,26 ms
lz4 958.466 9.532.120 6,7× 1,10 ms
zstd 890.207 3.886.692 16,3× 1,72 ms
snappy 869.565 9.630.619 6,6× 1,10 ms
gzip 430.416 4.875.173 13,0× 3,27 ms

Bức tranh đảo hẳn.

lz4 giữ 99% thông lượng mà đĩa nhỏ đi gần bảy lần. Gần như miễn phí.

zstd mất 8% thông lượng đổi lấy 16,3 lần nhỏ hơn. Với dữ liệu phải giữ lâu hoặc phải sao chép qua vùng khác, đó là món hời rõ ràng.

gzip thua zstd ở mọi mặt — nén kém hơn và chậm hơn hai lần. Nó chỉ còn lý do tồn tại là tương thích với consumer rất cũ.

Và chi tiết đáng chú ý nhất: độ trễ trung bình của none là 19,26 ms, của lz4 là 1,10 ms. Nén xong lại nhanh hơn, vì thứ phải đẩy qua socket và ghi xuống đĩa nhỏ đi bảy lần. Chi phí CPU của lz4 rẻ hơn chi phí vận chuyển phần dôi ra.

Nén ở đâu

Producer nén cả lô, không nén từng tin. Đó là lý do lô càng to nén càng tốt: nhiều bản ghi giống nhau nằm cạnh nhau thì thuật toán tìm được nhiều mẫu lặp hơn. batch.sizecompression.type khuếch đại lẫn nhau.

Broker không giải nén — nó lưu nguyên lô đã nén và gửi nguyên lô đó cho consumer. Consumer mới là nơi giải nén. Nghĩa là nén tiết kiệm cả đĩa, cả băng thông sao chép giữa các broker, cả băng thông tới consumer, trong khi CPU broker gần như không đổi.

Có một ngoại lệ tốn kém: đặt compression.typetopic khác với ở producer. Khi ấy broker phải giải nén rồi nén lại từng lô. Để topic ở producer (mặc định) là để nó khỏi động vào.

Chọn thế nào

  • lz4 — mặc định hợp lý cho hầu hết trường hợp. Gần như không mất gì.
  • zstd — khi đĩa hoặc băng thông giữa các vùng là thứ đắt đỏ. Cần consumer từ Kafka 2.1 trở lên.
  • snappy — tương đương lz4 nhưng chậm hơn một chút trong phép đo này. Không có lý do chọn mới.
  • gzip — chỉ khi buộc phải tương thích ngược.
  • none — khi dữ liệu đã nén sẵn: ảnh, video, tệp zip, Avro đã nén. Nén hai lần chỉ tốn CPU.

Thử ba mươi giây

Đo trên dữ liệu của bạn, không phải dữ liệu ngẫu nhiên:

# lấy 2000 bản ghi thật ra một tệp, mỗi dòng một tin
kafka-console-consumer.sh --bootstrap-server kf:9092 --topic topic-that \
  --from-beginning --max-messages 2000 > /tmp/payload.txt

for C in none lz4 zstd; do
  kafka-topics.sh --bootstrap-server kf:9092 --create --topic thu-$C --partitions 1 --replication-factor 1
  kafka-producer-perf-test.sh --topic thu-$C --num-records 100000 \
    --payload-file /tmp/payload.txt --throughput -1 \
    --producer-props bootstrap.servers=kf:9092 batch.size=65536 compression.type=$C
done
du -sh /tmp/kafka-logs/thu-*-0

Tỉ lệ nén bạn nhận được có thể là 2× hoặc 16×. Chỉ dữ liệu của bạn mới trả lời được.

Phần sau đo ghi đúng một lần: idempotence tốn bao nhiêu, và một giao dịch bị huỷ để lại gì trên đĩa.