Có một tham số Kafka mà bật lên là bạn đồng ý mất dữ liệu trong một số tình huống. Bài này dựng đúng tình huống đó và đếm số tin mất.
Dựng kịch bản
Ba broker, replication-factor=3, min.insync.replicas=1. Bốn bước:
- Ghi 1.000 tin khi cả ba broker sống.
Isr: 13,11,12, offset cuối 1.000. - Giết hai follower. ISR co lại còn
[13]. - Ghi tiếp 500 tin vào riêng leader kb3.
min.insync.replicas=1nên broker chấp nhận. Offset cuối 1.500. - Giết kb3, bật lại kb1 — broker đang dừng ở offset 1.000.
Bây giờ kb1 là bản sao duy nhất còn sống, và nó chưa bao giờ thấy 500 tin kia. Câu hỏi: nó có được làm leader không?
Bật: mất 500 tin
unclean.leader.election.enable=true:
Topic: uc Partition: 0 Leader: 11 Replicas: 13,11,12 Isr: 11
kb1 lên làm leader. Đọc lại toàn bộ topic:
offset cuối: 1.500 -> 1.000
tin đọc được: A-1 .. A-1000
tin B-1001..1500: không còn
500 tin đã được xác nhận ghi thành công, biến mất vĩnh viễn. Producer đã nhận ack, ứng dụng đã coi là xong, và không có gì trong hệ thống ghi lại rằng chúng từng tồn tại.
Chi tiết đáng chú ý hơn cả số tin mất: bộ đếm offset đi lùi. Từ 1.500 về 1.000.
Đây là thứ Kafka vốn hứa không bao giờ xảy ra. Offset tăng đơn điệu là giả định nền của rất nhiều thứ xây trên Kafka — bảng theo dõi tiến độ, phép tính lag, cơ chế chống trùng dựa vào offset. Sau một lần bầu bẩn, mọi giả định đó sai.
Hậu quả cho consumer: một nhóm đã chốt offset 1.400 giờ trỏ vào chỗ vượt quá cuối log. Lần poll() tiếp theo ném OffsetOutOfRangeException, và consumer áp auto.offset.reset:
earliest→ đọc lại từ đầu, xử lý trùng 1.000 tinlatest→ nhảy tới cuối, im lặng bỏ qua
Cả hai đều sai, và cả hai đều không có lỗi nào hiện ra ngoài một dòng cảnh báo.
Điểm cuối: công cụ kafka-consumer-groups.sh --reset-offsets --to-offset 1400 tự cắt xuống 1.000 khi tôi thử đặt. Nó biết offset 1.400 không tồn tại. Nhưng offset đã lưu trong __consumer_offsets từ trước thì không ai cắt hộ.
Tắt: partition đứng hẳn
unclean.leader.election.enable=false — mặc định từ Kafka 0.11:
Topic: cln Partition: 0 Leader: none Replicas: 13,11,12 Isr: 13
Leader: none. Đọc trả TimeoutException, 0 tin. Ghi không được.
Partition đứng hẳn, chờ kb3 quay lại. Đó là lựa chọn đúng: 1.500 tin vẫn nằm nguyên trên đĩa của kb3, và khi nó khởi động lại thì mọi thứ tiếp tục như chưa có gì.
Dừng thì đảo ngược được. Mất thì không.
Vậy có bao giờ nên bật
Rất hiếm, và luôn phải là quyết định có ý thức chứ không phải giá trị mặc định để quên.
Bật hợp lý khi có sẵn dữ liệu ở nơi khác: topic chứa số đo, dấu vết, log truy cập — thứ mất vài trăm bản ghi không ai biết, mà dừng hệ thống thì mọi bảng theo dõi tắt ngóm. Ở đó, "chạy tiếp với dữ liệu thiếu" tốt hơn "dừng cho tới khi máy kia sống lại".
Tắt (giữ mặc định) với mọi thứ còn lại: đơn hàng, thanh toán, sự kiện thay đổi từ cơ sở dữ liệu, nhật ký kiểm toán. Với những topic đó, mất một bản ghi là hỏng dữ liệu, và hỏng dữ liệu thì không có cách sửa từ Kafka.
Đặt riêng cho từng topic, đừng đặt ở mức broker:
kafka-configs.sh --bootstrap-server kb1:9092 --alter \
--entity-type topics --entity-name topic-so-do \
--add-config unclean.leader.election.enable=true
Điều kiện để kịch bản này xảy ra
Bốn thứ phải cùng đúng, và đó là lý do nó hiếm:
min.insync.replicas=1— hoặc producer dùngacks=1. Nếu làmin.insync.replicas=2vớiacks=all, bước 3 đã bị chặn và không có 500 tin nào để mất.- ISR co lại còn một.
- Bản sao duy nhất trong ISR chết.
- Một bản sao ngoài ISR sống lại trước nó.
Điều kiện thứ nhất là chỗ chữa được. min.insync.replicas=2 khiến kịch bản này không dựng được — broker sẽ từ chối bước 3 với NOT_ENOUGH_REPLICAS, và 500 tin kia không bao giờ được xác nhận, nên cũng không mất.
Đó là lý do phần 14 gọi bộ ba replication-factor=3 + min.insync.replicas=2 + acks=all là mặc định thực dụng: nó chặn từ gốc chứ không xử lý hậu quả.
Bầu leader ưu tiên: một chuyện khác hẳn
Đừng nhầm unclean với preferred. Đây là hai loại bầu lại khác nhau:
| Bầu bẩn (unclean) | Bầu ưu tiên (preferred) | |
|---|---|---|
| Chọn từ | Bản sao ngoài ISR | Bản sao trong ISR |
| Mất dữ liệu | Có thể | Không bao giờ |
| Khi nào | ISR rỗng | Cân bằng lại tải |
| Mặc định | Tắt | Bật, chu kỳ 300 giây |
Phần 13 đã đo bầu ưu tiên: nó chỉ chuyển leader sang một broker đã có đủ dữ liệu, nên hoàn toàn an toàn và nên chạy sau mỗi lần khởi động lại broker.
Hai lệnh nên thuộc
# cảnh báo sớm: có follower đang tụt lại
kafka-topics.sh --bootstrap-server kb1:9092 --describe --under-replicated-partitions
# sự cố thật: Leader: none
kafka-topics.sh --bootstrap-server kb1:9092 --describe --unavailable-partitions
Lệnh thứ hai trong phép đo của tôi liệt kê cả 50 partition của __consumer_offsets. Đó là chi tiết đáng nhớ: khi topic nội bộ đó mất leader, mọi nhóm consumer đều không chốt được offset — kể cả những nhóm đang đọc topic vẫn hoàn toàn khoẻ mạnh. Sự cố lan rộng hơn nhiều so với danh sách topic bị ảnh hưởng trực tiếp.
Thử ba mươi giây
Xem topic nào của bạn đang bật bầu bẩn:
for t in $(kafka-topics.sh --bootstrap-server kb1:9092 --list); do
kafka-configs.sh --bootstrap-server kb1:9092 --describe \
--entity-type topics --entity-name "$t" \
| grep -q "unclean.leader.election.enable=true" && echo "$t"
done
Mỗi topic hiện ra là một topic bạn đã đồng ý mất dữ liệu. Nếu bạn không nhớ đã đồng ý, đó là chỗ nên xem lại.
Phần sau mở tệp .index ra xem Kafka tìm một offset thế nào — và vì sao tìm ở giữa nhanh bằng tìm ở đầu.