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.
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ơnacks=1như 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ùngacks=1và 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.