Một chuyến đi dài thường không được nhớ bằng bản đồ, mà bằng cuốn sổ tay ghi lại những chỗ thực địa khác hẳn bản đồ. Bốn mươi phần vừa rồi, mỗi phần tôi dựng Kafka bằng container thật và đo bằng số thật, chính là để lấp đầy cuốn sổ đó — và điều đáng nói nhất là gần như không con số nào trong đây đoán trước được từ tài liệu. Bài cuối này gom lại những gì đáng mang theo.
Mười con số của cả sê-ri
| p2 | Công cụ dòng lệnh tốn 0,86 giây; gửi tin thật tốn 0,89 mili giây |
| p3 | 4 khoá vào 4 partition → 5.000 / 0 / 0 / 15.000 |
| p5 | batch.size 1 → 16384: thông lượng gấp 14, độ trễ giảm 1.567 lần |
| p8 | Cùng một lần sập: 1.150 tin trùng, hay 50 tin trùng, hay 50 tin mất |
| p10 | fetch.min.bytes=65536 đẩy độ trễ từ 0,35 ms lên 503,68 ms |
| p13 | Giết một broker: 9.142 ms để bầu leader mới |
| p22 | Một tin hỏng → 8.306.744 ngoại lệ trong 12 giây |
| p26 | Một tin một giao dịch = 96 tin/giây, chậm hơn 6.280 lần |
| p31 | Lag tăng 57 lần trong 60 giây, mọi chỉ số broker vẫn xanh |
| p38 | Thiếu một dòng volume: 10.000 tin và cả topic biến mất |
Không con số nào trong bảng này đoán được từ tài liệu. Tất cả đều phải đo.
Cấu hình mặc định cho một cụm thật
# độ bền — p4, p14, p15
replication.factor = 3
min.insync.replicas = 2
acks = all
unclean.leader.election.enable = false
enable.idempotence = true
# thông lượng — p5
batch.size = 65536
linger.ms = 0
compression.type = lz4
# vận hành — p9, p14, p24
auto.create.topics.enable = false
max.poll.records = 100
group.instance.id = <tên pod>
Mọi giá trị ở đây đến từ một phép đo trong loạt bài, không từ thói quen:
min.insync.replicas = 2— vì phần 14 đo được=3làm mọi lần bảo trì dừng ghi, còn=1để lọt kịch bản mất dữ liệu của phần 15.linger.ms = 0— vì phần 5 đo được nó gần như không ảnh hưởng ở tải cao, và phần 10 đo được nó cộng 54 ms ở tải thấp.compression.type = lz4— vì phần 5 đo được nó giữ 99% thông lượng mà đĩa nhỏ đi 6,7 lần.max.poll.records = 100— vì phần 24 đo được lô lớn khiến thời gian xử lý vượtmax.poll.interval.ms.group.instance.id— vì phần 27 đo được khởi động lại nhanh mất 44 giây nếu không có nó.
Sáu cách hỏng im lặng
Đây là phần đáng nhớ nhất của loạt bài. Không cái nào báo lỗi, ghi log, hay hiện lên bảng theo dõi — và đó chính là điều làm chúng nguy hiểm.
p12 — Tăng partition làm 45% khoá đổi chỗ. Tin cũ và tin mới của cùng một khoá nằm ở hai partition khác nhau, và thứ tự giữa chúng hỏng vĩnh viễn.
p25 — --to-datetime với mốc sai báo Partition is empty rồi nhảy tới cuối log. Bạn nghĩ vừa tua lại, thực ra vừa bỏ qua toàn bộ dữ liệu.
p28 — Cửa sổ nối quá ngắn cho 0 kết quả. Topic đích chỉ đơn giản là rỗng, trông y hệt "chưa có dữ liệu".
p29 — JDBC mode=incrementing bỏ qua 50.000 UPDATE và 10.000 DELETE. Connector vẫn RUNNING, và Kafka phục vụ dữ liệu cũ mãi mãi.
p36 — MirrorMaker chép dữ liệu ngon lành, offset thì không sang. Kế hoạch chuyển vùng chỉ hỏng vào đúng ngày cần dùng.
p38 — Không có volume thì docker rm xoá sạch, và broker lên lại như vừa cài.
Điểm chung: mỗi cái đều có một lệnh kiểm phát hiện được trong ba mươi giây. Chạy chúng một lần còn hơn tin rằng mọi thứ ổn.
Danh sách kiểm trước khi lên môi trường thật
Độ bền
-
replication.factor ≥ 3trên mọi topic nghiệp vụ -
min.insync.replicas = 2, và nó nhỏ hơnreplication.factor - Producer dùng
acks=all— kiểm ở phía ứng dụng, broker không ép được (p14) -
unclean.leader.election.enable=falsetrên mọi topic không dựng lại được (p15) -
auto.create.topics.enable=false(p14)
Vận hành
- Volume gắn đúng vào thư mục dữ liệu (p38)
-
stop_grace_periodđủ dài, không dùngkill -9(p20, p38) - Giới hạn bộ nhớ container lớn hơn hẳn heap (p38)
- Bầu lại leader ưu tiên sau mỗi lần khởi động lại broker (p13, p37)
-
retention.msđặt theo yêu cầu xử lý lại, không để mặc định (p17, p25)
Giám sát
-
OfflinePartitionsCount,ActiveControllerCount,UnderReplicatedPartitions(p31) - Lag theo partition tệ nhất, cảnh báo theo xu hướng chứ không theo mức (p32)
- Lag tính bằng thời gian, không chỉ bằng số tin (p32)
- Dung lượng đĩa mỗi broker, cảnh báo ở 75% (p17)
Ứng dụng
- Consumer bắt
RecordDeserializationExceptionvà nhảy qua (p22) - Có topic tin chết, và có ai đó nhìn nó (p24)
- Phía tiêu thụ chịu được xử lý trùng (p8, p26)
-
delivery.timeout.mslớn hơn thời gian bị điều tiết tệ nhất (p33)
Bài học về cách đo
Sáu lần trong loạt bài này, một phép đo của tôi cho kết quả sai vì cùng hai lý do:
Nuốt lỗi bằng 2>/dev/null. Lệnh dựng môi trường hỏng, tôi không thấy, và phép đo chạy trên môi trường không như tôi nghĩ. Phần 20 là ca tệ nhất: hai topic không có cấu hình cần đo, và tôi suýt công bố một "quy luật hình chữ U" hoàn toàn không tồn tại.
Không xác minh cấu hình đã được áp dụng. Phần 10: kafka-e2e-latency.sh ghi đè fetch.min.bytes sau khi nạp tệp cấu hình, nên sáu lần chạy khác nhau đều ra 0,29 ms.
Quy tắc rút ra, và nó đúng ngoài Kafka:
Kết quả giống nhau qua nhiều cấu hình khác nhau không phải phát hiện. Nó là dấu hiệu phép đo hỏng.
Và quy tắc thứ hai, từ phần 8:
Nếu kết quả của bạn quá gọn gàng, hãy nghi ngờ nó trước đã. Điểm sập rơi đúng ranh giới lô cho ra ba con số hoàn hảo giống nhau — và che mất toàn bộ hiện tượng cần đo.
Cái loạt bài này không phủ tới
Để rõ ràng về phạm vi:
- Kafka trên Kubernetes — Strimzi và các toán tử khác, một chủ đề riêng.
- Tiered storage (từ 3.6) — đẩy segment cũ sang S3. Tôi không có môi trường để đo.
- Cụm quy mô lớn — mọi phép đo chạy trên một máy với tối đa ba broker. Con số về bầu lại leader và cân bằng lại ở quy mô hàng chục nghìn partition sẽ khác.
- Kafka Streams sâu hơn — mới chạm tới đếm, cửa sổ và nối.
Nếu chỉ nhớ ba điều
Kafka nhanh vì nó không làm gì phức tạp. Ghi nối tiếp, chỉ mục thưa, sendfile, xoá cả tệp. Mọi tối ưu đều đến từ việc không làm, và hiểu điều đó giải thích gần hết hành vi của nó (p16, p19).
Phần lớn sự cố Kafka không nằm ở Kafka. Consumer chậm, lược đồ đổi, cấu hình client sai, partition bị lệch khoá. Broker khoẻ trong khi đường ống chết là tình huống bình thường (p31).
Cái hỏng im lặng nguy hiểm hơn cái hỏng ồn ào. Sáu ca ở trên đều để hệ thống chạy tiếp với dữ liệu sai. Danh sách kiểm ba mươi giây ở mỗi phần tồn tại vì lý do đó.
Cùng một kỷ luật đo, đổi ngăn xếp
Bài học đắt nhất của cả loạt bài không phải về Kafka, mà về cách đo — và nó đúng ở mọi ngôn ngữ bạn từng viết benchmark hay test.
Trong thế giới Go, cái bẫy kinh điển là một benchmark bị trình biên dịch tối ưu mất: nếu bạn không tiêu thụ kết quả vào một biến cấp gói, compiler thấy phép tính vô dụng và bỏ luôn, rồi go test -bench báo ~0 ns/op cho mọi lần chạy. Đúng y quy tắc "kết quả giống nhau qua nhiều đầu vào = phép đo hỏng" — ở đây cái bạn đo là trình tối ưu, không phải thuật toán.
Trong thế giới Java, đó là một unit test xanh mà chẳng kiểm gì: assertion nằm trong một callback không bao giờ được gọi, hoặc một mock nuốt lời gọi thật, hoặc test bắt Exception rồi coi là qua. Test xanh, nhưng vùng nó thật sự phủ là số không — đúng kiểu "nuốt lỗi bằng 2>/dev/null". Đây là lý do người ta viết benchmark JMH với Blackhole để ép JIT không loại bỏ kết quả, hệt như Go.
Còn ở cơ sở dữ liệu, cùng cái bẫy: đo một truy vấn trên bảng rỗng rồi kết luận nó nhanh; chạy EXPLAIN ANALYZE với cache nóng rồi tưởng đó là số thật; hay một test bọc trong giao dịch rollback nên chẳng bao giờ chạm tới ràng buộc khoá ngoại — xanh trên test, vỡ trên production.
Mẫu số chung: một phép đo chỉ đáng tin bằng đúng bằng chứng rằng nó thật sự đo cái bạn nghĩ — kết quả giống hệt nhau qua nhiều đầu vào khác nhau, con số đẹp đến mức đáng ngờ, và lỗi dựng môi trường bị nuốt mất, không phải là phát hiện mà là triệu chứng rằng thứ bạn vừa đo là chính cái dụng cụ, không phải hệ thống. Nên câu hỏi đúng sau mỗi phép đo không phải "con số này đẹp không" mà là "tôi đã xác minh rằng đầu vào tôi nghĩ mình đang thay đổi thật sự có hiệu lực, và rằng kết quả thật sự phản ứng theo nó chưa". Kỷ luật đó là thứ khiến bốn mươi con số trong loạt bài này đáng để tin.
Và để khép lại, ba lệnh đáng chạy một lần trên bất kỳ cụm nào trước khi tin rằng nó khoẻ:
kafka-topics.sh --bootstrap-server kf:9092 --describe --under-replicated-partitions
kafka-topics.sh --bootstrap-server kf:9092 --describe --unavailable-partitions
kafka-consumer-groups.sh --bootstrap-server kf:9092 --all-groups --describe 2>/dev/null \
| awk 'NR>1 && $6 ~ /^[0-9]+$/ {print $6, $1, $2, $3, $7}' | sort -rn | head -5
Hai lệnh đầu phải im lặng. Lệnh thứ ba cho bạn năm chỗ đau nhất trong cụm, và nếu cột cuối là - thì partition đó không có ai đọc.
Cảm ơn bạn đã theo hết bốn mươi phần.