Hướng dẫn triển khai Apache Kafka chế độ KRaft và Kafka UI bằng Docker Compose

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Sự cố lúc 2 giờ 15 sáng

Hệ thống giám sát Prometheus réo chuông liên tục. Cụm Kafka trên môi trường staging mất kết nối diện rộng. Toàn bộ worker xử lý đơn hàng và gửi email thông báo đồng loạt treo cứng. Hơn 45.000 message bị tắc nghẽn trong hàng đợi.

Vừa mở laptop, mình gõ vội lệnh kiểm tra container:

docker compose ps
docker compose logs --tail=100 zookeeper

Container Zookeeper đã chết với mã lỗi OOMKilled (Exit Code 137). Zookeeper mất quorum khiến quá trình đồng bộ metadata đứt đoạn. Cả cụm rơi vào tình trạng split-brain. Các broker không thể bầu Leader partition và từ chối toàn bộ kết nối từ client. Một đêm mất ngủ chỉ vì cụm Zookeeper 3 node ngốn sạch 4GB RAM của VPS staging.

Gánh nặng mang tên Zookeeper

Trước bản Kafka 2.8 (và chính thức ổn định từ 3.3+), Kafka bắt buộc phải có Zookeeper để lưu metadata, bầu Controller và quản lý cấu hình topic. Kiến trúc hai tầng này bộc lộ ba điểm yếu lớn:

  • Lãng phí tài nguyên: Để chạy cụm tối thiểu trên production, bạn cần 3 node Zookeeper và 3 broker Kafka. Riêng cụm Zookeeper đã ngốn 2GB đến 4GB RAM chỉ để duy trì heartbeat.
  • Tắc nghẽn khi scale: Khi số partition vượt mức 100.000, metadata trở nên quá tải. Thời gian recovery sau khi broker restart có thể kéo dài từ 2 đến 10 phút vì phải sync hàng loạt state từ Zookeeper.
  • Vận hành phức tạp: Bạn phải quản lý hai hệ thống phân tán cùng lúc. Hai cụm cổng mạng, hai bộ file cấu hình bảo mật (SASL/TLS) và hai nguồn log riêng biệt.

So sánh các hướng xử lý

Khi đối mặt với bài toán tối ưu hạ tầng, team mình cân nhắc 3 hướng đi:

1. Cấp thêm RAM và tune lại JVM cho Zookeeper

Tăng -Xmx từ 1GB lên 4GB mỗi node và tinh chỉnh tham số snapshot. Cách này chỉ giải quyết phần ngọn. Chi phí hạ tầng đội lên mà kiến trúc cồng kềnh vẫn giữ nguyên.

2. Đổi sang RabbitMQ hoặc Redis Streams

Phương án này phù hợp với service nhỏ. Tuy nhiên, hệ thống của mình cần lưu trữ log sự kiện lâu dài (retention 30 ngày), replay message khi có lỗi và throughput đạt 20.000 msg/s. RabbitMQ không đáp ứng tốt bài toán này.

3. Chuyển sang Kafka KRaft (Kafka Raft Metadata)

Đây là hướng đi chuẩn nhất. KRaft đưa thuật toán đồng thuận Raft trực tiếp vào broker Kafka. Metadata được lưu trong topic nội bộ @metadata. Nhờ đó, thời gian phục hồi controller giảm từ 30 giây xuống dưới 500 mili-giây, đồng thời cắt giảm được 50% lượng RAM tiêu thụ.

Triển khai Kafka KRaft + Kafka UI bằng Docker Compose

Dưới đây là cấu hình 1 node Kafka KRaft (đóng cả vai trò Broker và Controller) kèm giao diện Kafka UI để test trên local hoặc môi trường staging.

Bước 1: Chuẩn bị file docker-compose.yml

Tạo thư mục làm việc:

mkdir -p ~/kafka-kraft-stack && cd ~/kafka-kraft-stack
nano docker-compose.yml

Dán cấu hình sau vào file:

services:
  kafka:
    image: apache/kafka:3.7.0
    container_name: kafka-kraft
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_CLUSTER_ID: "4L622nShTUiBenVKBpahAg"
      KAFKA_PROCESS_ROLES: "broker,controller"
      
      # Cấu hình listener cho máy host (9092) và giao tiếp nội bộ container (29092, 9093)
      KAFKA_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_INTERNAL://:29092"
      KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:29092"
      KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT"
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093"
      
      # Cấu hình replication cho cụm 1 node
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_LOG_DIRS: "/tmp/kraft-combined-logs"
    volumes:
      - kafka_data:/tmp/kraft-combined-logs
    networks:
      - kafka-net

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    ports:
      - "8080:8080"
    environment:
      KAFKA_CLUSTERS_0_NAME: "kraft-local-cluster"
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: "kafka:29092"
      DYNAMIC_CONFIG_ENABLED: "true"
    depends_on:
      - kafka
    networks:
      - kafka-net

volumes:
  kafka_data:
    driver: local

networks:
  kafka-net:
    driver: bridge

Bước 2: Khởi chạy cụm service

Khởi động container bằng Docker Compose v2:

docker compose up -d

Theo dõi log khởi động của Kafka:

docker compose logs -f kafka

Khi thấy dòng log [KafkaRaftServer id=1] Kafka Server started, broker đã sẵn sàng nhận kết nối. Thời gian khởi động chỉ mất khoảng 3 đến 5 giây.

Bước 3: Kiểm thử Produce và Consume

Tạo thử một topic tên order-events với 3 partition:

docker compose exec -it kafka /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server localhost:9092 \
  --create \
  --topic order-events \
  --partitions 3 \
  --replication-factor 1

Mở một tab terminal riêng để lắng nghe message:

docker compose exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server localhost:9092 \
  --topic order-events \
  --from-beginning

Mở tab terminal thứ hai để gửi message:

docker compose exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \
  --bootstrap-server localhost:9092 \
  --topic order-events

> {"order_id": 1001, "status": "PAID", "amount": 250000}
> {"order_id": 1002, "status": "PENDING", "amount": 120000}

Bên tab Consumer, dữ liệu JSON sẽ xuất hiện ngay sau khi bạn nhấn Enter.

Bước 4: Quản trị trực quan qua Kafka UI

Truy cập vào http://localhost:8080 trên trình duyệt. Giao diện Kafka UI cho phép bạn:

  • Kiểm tra trạng thái Controller và Broker ID 1.
  • Tạo mới, sửa partition hoặc thay đổi cấu hình retention time trực tiếp.
  • Đọc payload message theo từng partition và theo dõi Consumer Group Lag để phát hiện worker bị nghẽn.

Từ ngày loại bỏ Zookeeper và chuyển sang KRaft, cụm staging của bên mình chạy ổn định với chỉ ~700MB RAM, không còn cảnh báo OOM giữa đêm.

Share: