Có một sự thật khó chịu mà mọi hệ phân tán phải sống chung: bạn không bao giờ biết tức thời rằng một máy đã chết. Một máy không trả lời có thể đã chết, cũng có thể chỉ đang kẹt một nhịp GC hay một chớp mạng — và hai tình huống đó nhìn từ bên ngoài giống hệt nhau. Nên muốn chắc nó chết, bạn phải chờ. Con số 9,142 giây Kafka mất để bầu lại leader không phải vì bầu chậm — nó gần như toàn bộ là khoảng chờ đó. Tôi sẽ giết máy thật và bấm giờ.
Bản sao là cơ chế Kafka dùng để không mất dữ liệu khi máy chết. Bài này giết máy thật và bấm giờ.
Đọc một dòng describe
Topic: r3 Partition: 0 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2
Ba cột, ba nghĩa khác nhau:
Replicas— broker nào giữ bản sao. Cố định, do lúc tạo topic quyết định.Isr— broker nào đang theo kịp (in-sync replica). Thay đổi liên tục.Leader— broker nào nhận ghi và phục vụ đọc. Luôn là một phần tử củaIsr.
Một chi tiết quan trọng và hay bị bỏ qua: phần tử đầu tiên của Replicas là "leader ưu tiên". Kafka cố gắng giữ nó làm leader, và lúc tạo topic nó rải các leader ưu tiên đều trên các broker. Cuối bài sẽ thấy vì sao chỗ này đáng nhớ.
"Theo kịp" nghĩa là follower đã yêu cầu dữ liệu trong vòng replica.lag.time.max.ms (mặc định 30.000 ms). Không phải "đã có đủ mọi tin" — một follower chậm 5 giây vẫn nằm trong ISR.
Giết một broker
Cụm 3 broker, topic 6 partition, replication-factor=3. kb3 đang làm leader cho 2 partition. docker kill kb3:
bầu xong leader mới cho mọi partition: 9.142 ms
kb3 bị loại khỏi ISR: 9.142 ms
Chín giây. Con số đó không ngẫu nhiên: nó là broker.session.timeout.ms, mặc định 9.000 ms. Đó là thời gian controller đợi trước khi tuyên bố một broker đã chết.
Trước khi hết thời gian đó, controller vẫn coi kb3 còn sống và không làm gì cả. Mọi partition mà kb3 làm leader đều không phục vụ được trong chín giây đó.
Producer thấy gì trong chín giây ấy
Cho producer gửi đều 2.000 tin/giây với acks=all, rồi giết kb3 giữa chừng. Số liệu theo từng khoảng báo cáo:
| Giai đoạn | Thông lượng | Độ trễ lớn nhất |
|---|---|---|
| trước | 1.999 tin/giây | 917 ms |
| lúc giết | 481 tin/giây | 8.759 ms |
| ngay sau | 5.503 tin/giây | 11.766 ms |
| sau đó | 2.000 tin/giây | 5 ms |
Ba điều đọc được từ bảng này.
Không mất tin nào. Tổng số tin gửi đi vẫn đủ. Producer giữ chúng trong bộ đệm và gửi lại khi có leader mới.
Nhưng có tin mất 11,7 giây mới tới nơi. Nếu ứng dụng của bạn có ngưỡng thời gian chờ dưới 12 giây cho việc gửi, nó sẽ báo lỗi trong khoảng này — dù Kafka cuối cùng vẫn ghi được.
Khoảng bù dồn cũng là một vấn đề. Khoảng ngay sau chạy ở 5.503 tin/giây, gấp 2,75 lần bình thường. Consumer phía sau nhận đột ngột gấp ba lưu lượng, và nếu nó không chịu nổi thì sự cố lan sang chỗ khác.
Muốn phát hiện nhanh hơn thì hạ broker.session.timeout.ms. Cái giá: một nhịp mạng chậm hoặc một lần GC dài cũng đủ bị coi là chết, và bầu lại leader không cần thiết. Mặc định 9 giây là chỗ cân bằng hợp lý cho mạng trong một trung tâm dữ liệu.
Broker sống lại nhưng không lấy lại việc
Đây là phần bất ngờ nhất. Cho kb2 khởi động lại, đợi tới khi nó vào lại đủ ISR của cả 6 partition:
p0 Leader: 3 p1 Leader: 1 p2 Leader: 3
p3 Leader: 3 p4 Leader: 1 p5 Leader: 3
kb3 gánh 4 partition, kb1 gánh 2, kb2 gánh 0
kb2 nằm trong ISR của cả sáu partition — nó đang sao chép đủ mọi thứ, tốn đủ băng thông và đĩa — nhưng không phục vụ một yêu cầu nào. Toàn bộ tải đọc và ghi dồn lên hai broker còn lại.
Theo cột Replicas, kb2 là leader ưu tiên của p2 và p5. Nó chỉ cần được trao lại:
kafka-leader-election.sh --bootstrap-server kb1:9092 \
--election-type preferred --all-topic-partitions
Successfully completed leader election (PREFERRED) for partitions r3-2, r3-5
Sau lệnh này tải về đều 2–2–2.
Kafka có cơ chế tự làm việc này — auto.leader.rebalance.enable bật mặc định — nhưng nó chạy theo chu kỳ leader.imbalance.check.interval.seconds, mặc định 300 giây. Nghĩa là sau mỗi lần khởi động lại một broker, cụm chạy lệch tải tới năm phút trước khi tự cân lại.
Với triển khai cuốn chiếu ba broker, đó là mười lăm phút chạy lệch — và nếu bạn khởi động lại broker tiếp theo trước khi chu kỳ đó chạy, tình trạng lệch chồng lên nhau. Chạy tay lệnh trên sau mỗi lần khởi động lại là thói quen đáng có.
Cách nhìn nhanh trạng thái
Một partition thiếu bản sao là partition có Isr ngắn hơn Replicas:
kafka-topics.sh --bootstrap-server kb1:9092 --describe --under-replicated-partitions
Lệnh này im lặng là cụm khoẻ. Có dòng nào hiện ra là có follower đang tụt lại — và với min.insync.replicas=2 trên replication-factor=3, bạn chỉ còn một mức dự phòng nữa trước khi việc ghi bị chặn.
Còn một lệnh nữa quan trọng hơn nhiều người nghĩ:
kafka-topics.sh --bootstrap-server kb1:9092 --describe --unavailable-partitions
Nó liệt kê partition không có leader — tức là không đọc được, không ghi được. Đây là chỉ báo sự cố thật, còn under-replicated chỉ là cảnh báo sớm.
Đặt replication-factor bao nhiêu
- 1 — không có bản sao. Broker chết là mất dữ liệu vĩnh viễn. Chỉ dùng cho dữ liệu tạm dựng lại được.
- 2 — chịu được một broker chết, nhưng không kết hợp được với
min.insync.replicas=2: mất một broker là ghi bị chặn ngay. Vị trí khó chịu. - 3 — mặc định thực dụng. Với
min.insync.replicas=2, chịu được một broker chết mà vẫn ghi bình thường. - Trên 3 — chỉ khi dữ liệu cực kỳ quan trọng hoặc bản sao trải trên nhiều vùng. Mỗi bản sao thêm là thêm một lần ghi đĩa và một luồng sao chép qua mạng.
Cần ít nhất replication-factor broker để tạo topic. Cụm 3 broker thì replication-factor=3 là trần.
Cùng một sự thật "không phân biệt được chết với chậm", đổi ngăn xếp
Con số 9 giây — phải chờ hết một timeout mới dám tuyên bố một broker đã chết — không phải đặc sản Kafka. Nó là hệ quả của một giới hạn nền tảng: trong một mạng bất đồng bộ, một lần không trả lời là nhập nhằng, không cách nào phân biệt tức thời "đã chết" với "chỉ chậm".
Trong thế giới hệ phân tán, mọi bộ phát hiện hỏng đều trả giá này. Raft có election timeout: ngắn quá thì một chớp mạng gây bầu lại giả, dài quá thì failover chậm. ZooKeeper và etcd có session TTL đúng cùng đánh đổi. Cassandra thậm chí dùng phi-accrual failure detector để tự chỉnh ngưỡng theo độ trễ quan sát được — nhưng vẫn không thoát được bản chất: nó chỉ đoán khéo hơn, không biết chắc hơn. Ở tầng thấp nhất, TCP keepalive cũng là một timeout.
Trong thế giới ứng dụng, đây là health-check và circuit breaker: đặt ngưỡng ngắn thì một lần GC dài làm một dịch vụ khoẻ bị đánh dấu hỏng và bị rút khỏi vòng, đặt dài thì một dịch vụ thật sự chết vẫn nhận lưu lượng thêm một lúc.
Mẫu số chung: trong một mạng bất đồng bộ, bạn không bao giờ phân biệt tức thời được "đã chết" với "chỉ chậm hoặc mất liên lạc tạm" — một lần không trả lời là nhập nhằng — nên mọi bộ phát hiện hỏng đều buộc phải chờ hết một khoảng timeout trước khi hành động, và khoảng đó là một đánh đổi trực tiếp không có lời giải hoàn hảo: ngắn thì phát hiện nhanh nhưng báo chết nhầm (failover vô ích vì một lần GC hay chớp mạng), dài thì ít báo nhầm nhưng phục hồi chậm. Và có một hệ quả đi kèm: sau khi phát hiện và phục hồi, phần việc bị dồn trong lúc hỏng ập tới một lúc (ở đây là đợt 2,75 lần bình thường), nên hạ nguồn phải chịu được đợt bù đó — phục hồi không phải là "về như cũ" mà là "về cộng một cơn sóng". Nên câu hỏi đúng khi chỉnh bất kỳ ngưỡng phát hiện hỏng nào không phải "đặt nhanh nhất" mà là "một lần GC dài hay chớp mạng của tôi kéo bao lâu — vì ngưỡng phải dài hơn thế để khỏi báo chết nhầm, và đó chính là thời gian tối thiểu tôi sẽ mất khi có sự cố thật".
Muốn kiểm cụm của bạn có đang chạy lệch leader không, chạy thử:
kafka-topics.sh --bootstrap-server kb1:9092 --describe \
| grep -oE "Leader: [0-9]+" | sort | uniq -c | sort -rn
Nếu một broker có nhiều partition hơn hẳn broker khác, gần như chắc chắn vừa có một lần khởi động lại và chu kỳ cân bằng chưa chạy tới. Lệnh --election-type preferred chữa trong vài giây.
Phần sau đo min.insync.replicas: chính xác lúc nào producer bị chặn, và một cách hỏng mà con số đó không cứu được.