Chạy Kafka trong Docker để thử nghiệm chỉ cần một lệnh. Chạy nó thật thì cần vài dòng nữa, và bài này đo chuyện gì xảy ra khi thiếu từng dòng.
Thiếu volume: 10.000 tin biến mất
Chạy Kafka không khai volume, ghi 10.000 tin, rồi docker rm:
trước: quan-trong:0:10000
sau: Could not match any topic-partitions with the specified filters
danh sách topic sau khi dựng lại: (rỗng)
Không chỉ mất dữ liệu — mất luôn topic. Broker lên lại sạch sẽ như vừa cài, và không một dòng log nào nói rằng có gì đó vừa biến mất.
Đây là kiểu hỏng tệ nhất vì nó trông giống hệt một lần triển khai thành công. Ứng dụng kết nối được, auto.create.topics.enable tạo lại topic, producer ghi tiếp, và mọi thứ chạy — trên một topic rỗng.
volumes:
- kafka-data:/tmp/kafka-logs
Một dòng. docker compose down không xoá volume có tên, nên dữ liệu sống qua mọi lần triển khai — chỉ docker compose down -v mới xoá, và đó là lý do lệnh đó có chữ -v.
Một chi tiết đi kèm: volume Docker mới tạo thuộc về root, còn tiến trình Kafka trong ảnh apache/kafka chạy dưới appuser (uid 1000). Không sửa quyền thì broker chết ngay ở bước định dạng thư mục với thông báo về bootstrap.checkpoint.tmp — một thông báo không nói gì về quyền. Tôi vấp đúng chuyện này khi dựng phép đo cho bài trước.
Giới hạn bộ nhớ 400 MB giết broker
Ghi 1.000.000 tin × 500 byte:
| Kết quả | |
|---|---|
--memory 400m |
OOMKilled=true, container biến mất giữa chừng |
| không giới hạn | 574.712 tin/giây, 274 MB/giây |
Docker báo OOMKilled=true. Kafka không có dòng log nào — tiến trình bị SIGKILL, không kịp ghi gì.
Phần 2 đo được broker rỗng chỉ tốn 290 MB, nên 400 MB trông có vẻ rộng rãi. Nhưng dưới tải thì bộ đệm producer, bộ đệm mạng và vùng nhớ ngoài heap của JVM cộng lại vượt xa con số đó.
Hai dòng đi cùng nhau:
mem_limit: 6g
environment:
KAFKA_HEAP_OPTS: "-Xmx4g -Xms4g"
Heap nhỏ hơn hẳn giới hạn container, chừa chỗ cho vùng ngoài heap. Và phần 19 đã đo lý do đừng cấp heap lớn hơn: Kafka dựa vào bộ đệm trang của hệ điều hành, không dựa vào heap của chính nó.
-Xms bằng -Xmx để JVM cấp hết ngay từ đầu — thấy vấn đề bộ nhớ lúc khởi động tốt hơn thấy nó lúc ba giờ sáng.
Tắt sạch nhanh hơn tưởng
974 MB dữ liệu trên đĩa, docker stop:
1,2 giây
log kết thúc bằng "App info kafka.server for 1 unregistered"
Nhanh. Nhưng thời gian chờ mặc định của Docker là 10 giây, và broker lớn với nhiều dữ liệu chưa flush có thể vượt qua. Vượt là SIGKILL, và lần khởi động sau phải chạy khôi phục log — chậm hơn nhiều so với việc chờ thêm vài giây.
stop_grace_period: 120s
Con số này rẻ: nếu broker tắt xong trong 1,2 giây thì Docker đi tiếp ngay, không đợi đủ 120 giây.
advertised.listeners là chỗ hỏng nhiều nhất
Broker trả về địa chỉ này cho client, và client dùng nó cho mọi kết nối sau lần đầu.
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
Chạy được trong mạng Docker. Nhưng client ngoài host không phân giải được kafka, nên nó kết nối lần đầu thành công (qua localhost:9092 được ánh xạ) rồi thất bại ở lần thứ hai — với thông báo không nhắc gì tới tên máy.
Cách xử lý cho cả hai loại client là hai listener:
KAFKA_LISTENERS: INTERNAL://:9092,EXTERNAL://:9094,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://kafka.cong-ty.vn:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:SASL_SSL
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
Đây cũng là cách đúng để bật bảo mật cho client ngoài mà không bắt lưu lượng nội bộ trả giá TLS (phần 34).
Bốn thứ khác cho môi trường thật
restart: unless-stopped. Không có nó, broker chết vì bất kỳ lý do gì sẽ nằm im tới khi có người để ý.
Kiểm tra sức khoẻ phải hỏi Kafka, không hỏi cổng. Cổng 9092 mở từ trước khi broker sẵn sàng — phần 2 đo được ba trạng thái STARTING → RECOVERY → RUNNING, và chỉ ở trạng thái cuối nó mới phục vụ.
healthcheck:
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 || exit 1"]
interval: 30s
start_period: 60s
Giới hạn kích thước log của Docker. Kafka ghi log rất nhiều, và trình điều khiển json-file mặc định không có giới hạn. Phần 22 đo được một tin hỏng sinh 8,3 triệu ngoại lệ trong 12 giây — nếu mỗi ngoại lệ ghi một dòng, đó là hàng gigabyte.
logging:
driver: json-file
options: {max-size: "100m", max-file: "5"}
TZ đúng múi giờ. Dấu thời gian trong log là thứ bạn đọc lúc có sự cố; lệch bảy tiếng làm việc đối chiếu với log ứng dụng thành cực hình.
Nhiều broker trên một máy vật lý là ảo tưởng
Chạy ba container Kafka trên một máy cho replication-factor=3 không cho bạn khả năng chịu lỗi nào. Cùng đĩa, cùng nguồn điện, cùng nhân hệ điều hành. Máy đó chết là cả ba bản sao chết.
Nó có ích để thử nghiệm — phần lớn phép đo trong loạt bài này dựng như vậy — nhưng đừng nhầm với dự phòng thật.
Compose tối thiểu cho một broker thật
services:
kafka:
image: apache/kafka:3.9.0
restart: unless-stopped
mem_limit: 6g
stop_grace_period: 120s
volumes: ["kafka-data:/tmp/kafka-logs"]
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_HEAP_OPTS: "-Xmx4g -Xms4g"
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
TZ: Asia/Ho_Chi_Minh
logging:
driver: json-file
options: {max-size: "100m", max-file: "5"}
volumes:
kafka-data:
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" là dòng phần 14 giải thích: topic tạo tự động nhận replication-factor=1, và một lỗi gõ sai tên topic tạo ra một topic không có bản sao nào.
Thử ba mươi giây
Kiểm cụm Docker của bạn có giữ được dữ liệu không:
docker inspect <container> --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
Không có dòng nào trỏ tới /tmp/kafka-logs (hoặc /var/lib/kafka/data tuỳ ảnh) nghĩa là một lần docker rm sẽ xoá sạch mọi thứ — và không có gì cảnh báo bạn.
Phần sau so Kafka với RabbitMQ: hai công cụ giải hai bài toán khác nhau, và chọn nhầm thì đau ở đâu.