Kafka mang tiếng nặng nề và khó dựng. Phần này đo xem một broker thật sự tốn bao nhiêu, và tìm ra một cái bẫy khiến rất nhiều phép đo độ trễ Kafka trên mạng sai gấp một nghìn lần.

Thời gian khởi động, ba trạng thái, bẫy đo bằng CLI, và bộ nhớ theo tải

Dựng

Từ Kafka 3.3, KRaft đã ổn định và không cần ZooKeeper nữa. Một broker là một container:

docker run -d --name kf \
  -e KAFKA_NODE_ID=1 \
  -e KAFKA_PROCESS_ROLES=broker,controller \
  -e KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
  -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kf:9092 \
  -e KAFKA_CONTROLLER_QUORUM_VOTERS=1@kf:9093 \
  -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
  -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
  -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
  apache/kafka:3.9.0

PROCESS_ROLES=broker,controller nghĩa là node này vừa lưu dữ liệu vừa tham gia bầu chọn. Với một node thì bắt buộc phải thế; cụm thật thì tách ra, và phần 21 sẽ đo.

Ba biến cuối là ba thứ hay bị quên. ADVERTISED_LISTENERS là địa chỉ mà broker nói với client rằng hãy kết nối vào đây — nếu nó sai, client kết nối được lần đầu rồi thất bại ở lần thứ hai, và thông báo lỗi không chỉ ra nguyên nhân. OFFSETS_TOPIC_REPLICATION_FACTOR=1 phải đặt vì mặc định là 3, và một node thì không tạo nổi ba bản sao.

Khởi động và bộ nhớ

Dựng lại từ đầu ba lần:

Lần Thời gian Bộ nhớ
1 2,2 s 288,4 MB
2 3,0 s 290,6 MB
3 2,0 s 294,5 MB

Hai giây và 290 MB. Nhẹ hơn nhiều so với tiếng tăm của nó — phần lớn ấn tượng "Kafka nặng" đến từ thời còn phải chạy kèm một cụm ZooKeeper riêng.

Log khởi động cho thấy ba trạng thái:

The broker has caught up. Transitioning from STARTING to RECOVERY
The broker has been unfenced. Transitioning from RECOVERY to RUNNING
Kafka Server started

Chỉ ở RUNNING broker mới nhận yêu cầu. Trước đó nó bị "rào" lại — client kết nối vào sẽ bị từ chối. Đó là lý do kịch bản khởi động phải đợi bằng một lệnh thật (kafka-topics.sh --list) chứ không phải đợi cổng 9092 mở.

Cái bẫy: công cụ dòng lệnh

Tôi đo độ trễ gửi tin bằng cách gọi kafka-console-producer.sh năm lần:

tin 1: 1,03 s
tin 2: 0,90 s
tin 3: 0,90 s
tin 4: 0,89 s
tin 5: 0,88 s

Gần một giây cho một tin 6 byte. Con số đó vô lý, nên tôi đo tiếp: chạy một lệnh CLI không làm gì cả.

kafka-topics.sh --list   ->  0,86 giây

Đó là thời gian khởi động JVM của chính công cụ. Kafka không liên quan.

Đo lại bằng công cụ đo hiệu năng — nó khởi động một JVM rồi gửi 5.000 tin trong đó:

0,89 ms độ trễ trung bình     p50 0 ms     p95 1 ms     p99 20 ms

0,89 mili giây, không phải 0,88 giây. Chênh nhau khoảng một nghìn lần.

Đây là cái bẫy tôi thấy trong rất nhiều bài viết và câu hỏi trên mạng: người ta đo Kafka bằng cách gọi lặp lại công cụ dòng lệnh, rồi kết luận Kafka chậm. Cái họ đo là JVM khởi động.

Quy tắc: mọi phép đo độ trễ phải nằm trong một tiến trình duy nhất. Công cụ dòng lệnh dùng để kiểm tra, không dùng để đo.

Cùng lý do đó áp cho ứng dụng thật: tạo một KafkaProducer rồi dùng lại, đừng tạo mới cho mỗi tin. Producer giữ kết nối, giữ siêu dữ liệu topic, và giữ bộ đệm gộp lô — tạo mới là vứt hết.

Bộ nhớ theo tải

Bộ nhớ
Lúc rỗng 316,5 MB
Sau khi ghi 500.000 tin 577,4 MB

Và trên đĩa: 102 MB cho 500.000 tin × 200 byte = 100 MB dữ liệu thật. Phần dôi ra là phần đầu mỗi bản ghi và tệp chỉ mục.

Kafka không giữ tin trong bộ nhớ để phục vụ. Nó ghi xuống tệp và để bộ đệm trang của hệ điều hành lo phần đọc lại — đó là lý do bộ nhớ JVM không tăng theo lượng dữ liệu. Phần 19 sẽ đo cơ chế này và giải thích vì sao nó là chìa khoá của hiệu năng Kafka.

Hệ quả thực dụng: đừng cấp heap lớn cho Kafka. 4–6 GB là đủ cho hầu hết trường hợp; phần RAM còn lại để hệ điều hành làm bộ đệm trang thì có ích hơn nhiều.

Dừng broker

docker stop     0,9 giây, tắt sạch
khởi động lại   RECOVERY -> RUNNING, không cần khôi phục

Kafka nhận SIGTERM và tắt đàng hoàng trong thời gian chờ mặc định 10 giây của Docker.

Nhưng đó là broker có 102 MB dữ liệu. Trên broker thật với hàng chục gigabyte chưa flush, việc tắt sạch lâu hơn — và nếu vượt thời gian chờ, Docker gửi SIGKILL và lần khởi động sau phải chạy khôi phục log. Phần 20 sẽ đo con số đó.

Đặt sẵn trong Compose:

services:
  kafka:
    stop_grace_period: 120s

Kiểm broker còn sống

Bốn lệnh đáng thuộc:

# broker có nhận kết nối không
kafka-broker-api-versions.sh --bootstrap-server kf:9092 | head -1

# danh sách topic
kafka-topics.sh --bootstrap-server kf:9092 --list

# chi tiết một topic: partition, leader, ISR
kafka-topics.sh --bootstrap-server kf:9092 --describe --topic ten-topic

# offset đầu và cuối của từng partition
kafka-get-offsets.sh --bootstrap-server kf:9092 --topic ten-topic

Lệnh thứ ba là lệnh dùng nhiều nhất khi có sự cố — nó cho biết partition nào đang thiếu bản sao, và phần 13 sẽ đo ý nghĩa của cột ISR.

Thử ba mươi giây

Dựng broker bằng lệnh ở đầu bài, rồi tự kiểm cái bẫy:

# thời gian của một lệnh CLI không làm gì
time docker exec kf /opt/kafka/bin/kafka-topics.sh --bootstrap-server kf:9092 --list

# độ trễ gửi tin thật, trong một tiến trình
docker exec kf /opt/kafka/bin/kafka-producer-perf-test.sh \
  --topic thu --num-records 5000 --record-size 200 --throughput 500 \
  --producer-props bootstrap.servers=kf:9092 linger.ms=0 batch.size=1

Nếu hai con số của bạn cũng chênh nhau khoảng một nghìn lần, bạn vừa xác nhận rằng mọi phép đo Kafka gọi qua công cụ dòng lệnh đều vô nghĩa.

Phần sau đo topic, partition và offset: dữ liệu thật sự nằm thế nào trên đĩa.