acks là tham số producer được bàn nhiều nhất, và cũng là tham số bị đo sai nhiều nhất. Bài này đo cả ba giá trị — kể cả lần đo đầu tiên của tôi, vốn hoàn toàn vô nghĩa.

Phép đo sai trên một broker, phép đo đúng trên ba broker, và thứ acks=all mua được

Ba giá trị

acks Producer coi là xong khi
0 Đã đẩy ra socket. Không chờ gì cả.
1 Leader đã ghi vào log của nó.
all Leader và mọi bản sao trong ISR đã ghi.

Câu chuyện quen thuộc là "0 nhanh nhất, all chậm nhất, chọn theo mức chịu mất dữ liệu". Nửa đầu câu đó sai.

Lần đo đầu tiên của tôi vô nghĩa

Tôi chạy 200.000 tin × 500 byte trên broker một node ở phần 2:

acks=0      254.777 tin/giây
acks=1      231.481 tin/giây
acks=all    273.972 tin/giây     <- nhanh nhất

acks=all nhanh nhất. Kết quả đó không phải phát hiện gì, nó là dấu hiệu phép đo hỏng: với replication-factor=1 thì không có bản sao nào để chờ, nên acks=all chính xác là acks=1. Ba con số trên là ba lần đo cùng một thứ, chênh nhau vì nhiễu.

Đây là lỗi dễ mắc vì môi trường phát triển gần như luôn là một broker. Kết luận rút ra ở đó không mang sang môi trường thật được.

Đo lại trên ba broker

Dựng cụm 3 node, topic replication-factor=3, min.insync.replicas=2. Chạy mỗi mức ba lần và lấy lần đã nóng máy:

acks Thông lượng p50 p99
0 249.066 tin/giây 65 ms 119 ms
1 262.123 tin/giây 123 ms 188 ms
all 131.926 tin/giây 434 ms 462 ms

Hai điều đáng chú ý.

acks=0 không nhanh hơn acks=1 — nó chậm hơn 5%. Chênh lệch nằm trong nhiễu, nhưng ba lần chạy đều không cho acks=0 dẫn trước. Lý do: producer vốn đã gửi bất đồng bộ và gộp lô. Nó không ngồi chờ từng phản hồi ở acks=1; nó gửi lô tiếp theo trong khi lô trước đang bay. Bỏ phản hồi đi thì tiết kiệm được một gói tin nhỏ, còn chi phí thật — nén, ghi đĩa, sao chép — vẫn nguyên.

Chỉ acks=all mới thật sự tốn: thông lượng còn một nửa, p50 gấp ba rưỡi. Đó là chi phí có thật và cần cân nhắc.

Cái mà nửa thông lượng đó mua được

Đo tốc độ mới là nửa câu chuyện. Nửa còn lại: chuyện gì xảy ra khi cụm hỏng.

Vẫn cụm 3 node, min.insync.replicas=2. Tôi dừng hai broker, chỉ để lại leader:

--- acks=1 ---
100 records sent, 775 records/sec, 8.23 ms avg latency

--- acks=all ---
org.apache.kafka.common.errors.TimeoutException:
  Expiring 100 record(s) for rf3-0: 10001 ms has passed since batch creation
0 records sent

Với acks=1, producer báo thành công. Ứng dụng của bạn đi tiếp, ghi log "đã gửi", trả 200 cho khách. Nhưng 100 bản ghi đó đang nằm trên đúng một máy, không có bản sao nào. Máy đó chết là chúng biến mất, và không ai biết vì phía gửi đã coi là xong từ lâu.

Với acks=all, producer từ chối và nói ra. Ứng dụng nhận ngoại lệ, có thể thử lại, có thể đưa vào hàng chờ dự phòng, có thể trả 503 cho khách. Nó vẫn xử lý được, chỉ là không giả vờ ổn.

Khác biệt không nằm ở tốc độ. Nó nằm ở chỗ acks=1 nói dối khi cụm đang hỏng, và đúng lúc cụm hỏng là lúc bạn cần biết sự thật nhất.

min.insync.replicas là nửa còn thiếu

acks=all một mình chưa đủ. Nó nghĩa là "chờ mọi bản sao trong ISR" — và nếu ISR co lại còn một, thì "mọi bản sao" là một, và bạn quay về acks=1 mà không hay biết.

min.insync.replicas=2 là thứ chặn chuyện đó: ISR dưới 2 thì broker từ chối ghi. Chính nó tạo ra TimeoutException ở trên.

Bộ ba dùng thật:

replication-factor = 3
min.insync.replicas = 2
acks = all

Chịu được một broker chết mà không mất tin và không dừng ghi. Hai broker chết thì dừng ghi — đó là lựa chọn có chủ ý: thà dừng còn hơn nhận tin rồi mất.

Đặt min.insync.replicas=3 với RF=3 là sai lầm phổ biến: khi ấy mọi lần bảo trì một broker đều làm dừng ghi, không còn dư địa nào.

Vài tham số đi kèm

acks=all chỉ có nghĩa nếu producer không tự làm hỏng nó:

acks=all
enable.idempotence=true      # mặc định từ 3.0
retries=2147483647
max.in.flight.requests.per.connection=5
delivery.timeout.ms=120000

enable.idempotence=true là thứ khiến việc thử lại không tạo bản ghi trùng. Trước Kafka 3.0 nó tắt mặc định, nên retries cao đồng nghĩa với nguy cơ nhân đôi tin — rất nhiều lời khuyên trên mạng vẫn viết theo thời đó.

delivery.timeout.ms là trần thật sự cho một lần gửi, tính cả thử lại. retries lớn không giữ tin lâu hơn con số này.

Chọn thế nào

  • acks=all — mặc định. Dữ liệu không dựng lại được: đơn hàng, thanh toán, nhật ký kiểm toán, sự kiện thay đổi từ cơ sở dữ liệu.
  • acks=1 — dữ liệu dựng lại được và mất vài trăm bản ghi không sao: số đo, dấu vết, sự kiện giao diện, log truy cập.
  • acks=0 — gần như không bao giờ. Nó không nhanh hơn acks=1 như phép đo ở trên cho thấy, mà lại mất tin trong im lặng. Nếu bạn thật sự không quan tâm tin có tới hay không, hãy dùng acks=1 và bỏ qua kết quả — bạn được cùng tốc độ và giữ được khả năng biết khi có chuyện.

Thử ba mươi giây

Trên cụm ba broker của bạn, tạo topic min.insync.replicas=2, dừng hai broker, rồi thử gửi:

kafka-producer-perf-test.sh --topic rf3 --num-records 100 --record-size 100 \
  --throughput -1 --producer-props bootstrap.servers=kb3:9092 \
  acks=1 delivery.timeout.ms=10000
# rồi đổi acks=1 thành acks=all và chạy lại

Lệnh đầu báo 100 records sent. Lệnh sau báo 0 records sent. Cả hai đều đúng — chỉ một trong hai nói cho bạn biết cụm đang hỏng.

Phần sau đo consumer group: nhóm chia partition thế nào, và cân bằng lại tốn bao lâu.