Suốt mười năm Kafka cần ZooKeeper để lưu siêu dữ liệu. Từ Kafka 4.0 thì không còn. Bài này dựng cả hai kiểu cạnh nhau và đo.

So sánh thời gian dựng, khởi động lại, và chuyện gì xảy ra khi ZooKeeper chết

Dựng cả ngăn xếp từ con số không

ZooKeeper (confluentinc/cp-kafka:7.6.1) Thời gian Bộ nhớ
ZooKeeper sẵn sàng 16,5 giây 91 MB
broker sẵn sàng 9,8 giây 349 MB
tổng 26,3 giây 450 MB, 2 container
KRaft (apache/kafka:3.9.0), cùng máy, cùng lúc
sẵn sàng 2,8 giây 339 MB, 1 container

Chín lần nhanh hơn, ít hơn một trăm megabyte, và bớt một tiến trình phải vận hành.

Con số 26,3 giây kia là phần lớn lý do ai từng dựng Kafka trước 2022 đều nhớ nó là "nặng". Rất nhiều ấn tượng đó thuộc về ZooKeeper chứ không thuộc về Kafka.

(Hai image khác nhau nên phép so sánh không hoàn toàn công bằng ở mức từng mili giây. Nhưng khoảng cách chín lần thì lớn hơn nhiều so với mọi khác biệt do đóng gói.)

Khởi động lại với 200 topic: chênh ít hơn tôi tưởng

KRaft      (200 topic)     2,8 giây
ZooKeeper  (200 topic)     3,7 giây

Chỉ 1,3 lần. Tôi chờ đợi con số lớn hơn nhiều, dựa trên những gì hay được viết về KRaft.

Lợi thế lớn của KRaft nằm ở quy mô hàng chục nghìn partition: ở đó, controller kiểu cũ phải đọc lại toàn bộ trạng thái từ ZooKeeper mỗi lần bầu lại, và việc đó tính bằng phút. Ở 200 topic thì chưa thấy gì, và tôi ghi lại đúng như đo được thay vì mượn con số của người khác.

Còn một phép đo tôi đã làm và nó vô nghĩa: tạo 200 topic mất 160,3 giây với KRaft và 158,9 giây với ZooKeeper. Hai con số gần bằng nhau vì cả hai đều là 200 lần khởi động JVM của công cụ dòng lệnh — 200 × 0,86 giây ≈ 172 giây, đúng cỡ đó. Phép đo này đo kafka-topics.sh, không đo Kafka. Đây là chính cái bẫy đã nói ở phần 2, và tôi vẫn vấp lại.

ZooKeeper chết thì Kafka còn làm được gì

Đây là câu hỏi thú vị nhất, và câu trả lời không phải "mọi thứ dừng".

docker kill zk, rồi thử từng thao tác:

Thao tác Kết quả
ghi 50 tin OK — offset 200 → 250
đọc theo partition OK — x1 x2 x3
đọc bằng nhóm consumer OK — 250 tin
liệt kê topic OK — 201 dòng
tạo topic mới HỎNGTimeoutException

Mặt phẳng dữ liệu chạy tiếp. Mặt phẳng điều khiển dừng.

Broker giữ một bản sao siêu dữ liệu trong bộ nhớ, nên nó vẫn biết partition nào ở đâu, vẫn nhận ghi, vẫn phục vụ đọc. Cái nó không làm được là thay đổi siêu dữ liệu: tạo topic, đổi cấu hình, bầu lại leader khi có broker chết.

Đây chính là chỗ nguy hiểm. Sự cố ZooKeeper thường bị phát hiện rất muộn — mọi thứ vẫn chạy bình thường cho tới lần đầu tiên có ai đó cần tạo topic, hoặc tới lần đầu tiên một broker chết và không ai bầu được leader thay. Lúc đó thì đã có hai sự cố chồng lên nhau.

Một chi tiết tôi quan sát được trong lúc đo: lần đọc đầu tiên ngay sau khi giết ZooKeeper trả về 0 tin, lần thử lại sau đó trả đủ 250. Có một khoảng chuyển tiếp ngắn khi việc điều phối nhóm chưa ổn định. Tôi không xác định được chính xác nó dài bao lâu, nên chỉ ghi lại là nó tồn tại.

Bật lại ZooKeeper thì tạo topic hoạt động lại ngay, không cần khởi động lại broker.

KRaft khác chỗ nào

ZooKeeper là một hệ thống riêng biệt: cụm riêng, giao thức riêng, cách theo dõi riêng, phiên bản riêng cần nâng cấp. Siêu dữ liệu Kafka nằm trong đó dưới dạng cây znode, và mỗi lần controller đổi thì nó phải đọc lại.

KRaft đưa siêu dữ liệu vào chính Kafka: một topic nội bộ tên __cluster_metadata, sao chép bằng giao thức Raft giữa các node controller. Không có hệ thống thứ hai.

Ba hệ quả thực tế:

Bầu lại controller nhanh hơn. Controller mới đã có sẵn siêu dữ liệu vì nó là một bản sao của log đó, không phải đọc lại từ đâu cả.

Một cách vận hành duy nhất. Cùng công cụ, cùng cách theo dõi, cùng cách nâng cấp. Trước đây phải học hai hệ thống.

Số node quorum vẫn cần đa số. Đây là chỗ dễ nhầm: KRaft không xoá bỏ yêu cầu về quorum, nó chỉ chuyển quorum đó vào Kafka. Ba controller thì chịu được một chết; năm thì chịu được hai. Phần 14 đã đo hậu quả khi mất quorum — kể cả min.insync.replicas=1 cũng không ghi được.

Gộp vai hay tách vai

Với cụm nhỏ, mỗi node mang cả hai vai:

process.roles=broker,controller

Đơn giản, nhưng phần 14 đã cho thấy cái giá: mất hai trong ba node là mất luôn quorum, và cả cụm dừng — không cấu hình topic nào cứu được.

Với cụm sản xuất, tách ra:

# 3 hoặc 5 node controller nhỏ
process.roles=controller

# các node broker
process.roles=broker

Khi ấy bảo trì broker không bao giờ chạm tới quorum, và controller có thể chạy trên máy nhỏ vì chúng chỉ giữ siêu dữ liệu. Phần 15 phải dựng đúng cấu hình này mới dựng nổi kịch bản mất dữ liệu — với cụm gộp vai thì không dựng được, vì cụm chết trước khi kịch bản kịp xảy ra.

Mốc thời gian

Phiên bản
Kafka 2.8 KRaft ở dạng xem trước
Kafka 3.3 KRaft ổn định cho môi trường thật
Kafka 3.5 ZooKeeper bị đánh dấu lỗi thời
Kafka 4.0 ZooKeeper bị gỡ hẳn

Với cụm đang chạy trên ZooKeeper, việc chuyển đổi có công cụ hỗ trợ (chế độ migration từ 3.4) nhưng là một quy trình nhiều bước cần lên kế hoạch. Cụm mới thì không có gì phải cân nhắc — dùng KRaft.

Thử ba mươi giây

Xem cụm của bạn đang chạy chế độ nào:

# có trả lời = KRaft
kafka-metadata-quorum.sh --bootstrap-server kf:9092 describe --status

# đếm dòng log nhắc ZooKeeper
docker compose logs kafka 2>&1 | grep -ic zookeeper

Trong phép đo của tôi, broker chạy ZooKeeper có 80 dòng log nhắc tới nó ngay trong lần khởi động đầu tiên. Broker KRaft thì không có dòng nào.

Phần sau đo lược đồ dữ liệu — và một tin hỏng duy nhất khiến consumer ném hơn tám triệu ngoại lệ trong mười hai giây.