Series Apache Kafka · Bài 2/10. Trong bài này, bạn sẽ chạy Kafka bằng Docker Compose ở chế độ KRaft, tạo topic orders, gửi và đọc các sự kiện đầu tiên. Lab dùng image chính thức apache/kafka:4.1.2 và các lệnh Bash trên Linux, macOS hoặc WSL.
Mục lục
1. Chuẩn bị và hiểu KRaft
Cài Docker Engine và Docker Compose v2. Kiểm tra bằng docker version và docker compose version. KRaft quản lý metadata cluster mà không cần ZooKeeper. Broker lưu dữ liệu và phục vụ client; controller quản lý metadata và điều phối cluster. Lab một node kết hợp hai vai trò để dễ học. Production cần thiết kế replication, controller, bảo mật và giám sát riêng.

Phân biệt Docker, image, container và Kafka broker
Hãy hình dung bạn muốn mở một quầy nhận đơn hàng. Image giống bộ thiết kế đã đóng gói sẵn mọi thứ cần thiết để dựng quầy; container là một quầy cụ thể được mở từ bộ thiết kế đó. Kafka broker là chương trình đang hoạt động bên trong quầy, tiếp nhận và lưu các sự kiện. Docker là công cụ tạo và quản lý container. Docker Compose là cách mô tả các dịch vụ và cấu hình của chúng trong một file YAML để khởi động bằng một lệnh.
Vì vậy, tải image chưa đồng nghĩa Kafka đang chạy. Container đang chạy cũng chưa chắc broker đã khởi động xong. Ta sẽ kiểm tra theo ba lớp: Docker có hoạt động không, container có hoạt động không, và broker có trả lời yêu cầu Kafka không. Phân biệt ba lớp này giúp bạn biết cần xem lỗi ở đâu.
KRaft giải quyết phần điều phối metadata: cluster có những broker nào, topic có những partition nào và ai đang giữ vai trò leader. Metadata khác với nội dung JSON đơn hàng mà producer gửi. Trong lab, hai trách nhiệm cùng chạy trên một node; vì vậy nếu node dừng, cả việc phục vụ dữ liệu lẫn điều phối đều dừng. Đây là lý do mô hình một node phù hợp để học nhưng không đáp ứng yêu cầu chịu lỗi của doanh nghiệp.
Tạo thư mục làm việc đúng chỗ
mkdir -p ~/kafka-lab
cd ~/kafka-labDấu ngã đại diện thư mục home của tài khoản hiện tại. mkdir -p tạo thư mục nếu chưa có; cd chuyển terminal vào thư mục đó. Tạo file compose.yaml ở đây bằng trình soạn thảo bạn quen, ví dụ VS Code hoặc nano. Các lệnh docker compose tiếp theo cần chạy tại thư mục chứa file, để Compose tìm đúng cấu hình dự án. Nếu xuất hiện thông báo không tìm thấy configuration file, kiểm tra vị trí bằng pwd và danh sách file bằng ls.
2. Tạo cấu hình Docker Compose
Tạo thư mục kafka-lab và file compose.yaml bên trong. Cấu hình dưới đây dùng volume do Docker quản lý để giữ dữ liệu khi tạo lại container.
services:
kafka:
image: apache/kafka:4.1.2
container_name: kafka-lab
ports:
- "127.0.0.1:9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_LOG_DIRS: /tmp/kafka-logs
volumes:
- kafka_data:/tmp/kafka-logs
volumes:
kafka_data:Cổng 9092 chỉ được mở trên loopback của máy host. Client chạy trên máy dùng localhost:9092. Client trong container khác cần listener và hostname phù hợp; localhost trong container đó trỏ về chính nó. Replication factor bằng 1 vì lab chỉ có một broker. Cấu hình này dành cho học local.
Đọc cấu hình từ trên xuống
| Cấu hình | Ý nghĩa trong lab |
|---|---|
| services và kafka | Khai báo dịch vụ tên kafka. Tên dịch vụ được dùng trong các lệnh như docker compose logs kafka. |
| image | Chọn bản đóng gói Kafka chính thức, pin phiên bản 4.1.2 để người đọc dùng cùng một môi trường. |
| container_name | Đặt tên container là kafka-lab, giúp lệnh docker exec xác định đúng nơi chạy Kafka CLI. |
| ports | Chuyển kết nối cổng 9092 của máy host vào cổng 9092 của container, chỉ bind trên 127.0.0.1. |
| environment | Truyền cấu hình Kafka cho image khi khởi động. |
| volumes | Gắn named volume kafka_data vào thư mục chứa log Kafka để dữ liệu tồn tại qua việc tạo lại container. |
Trong ports, chuỗi 127.0.0.1:9092:9092 được đọc lần lượt là địa chỉ host, cổng host và cổng container. Hai cổng có cùng số nhưng thuộc hai môi trường khác nhau. Nếu máy đã có dịch vụ chiếm 9092, không chỉ đổi một con số tùy ý: địa chỉ quảng bá cho client trong advertised.listeners cũng phải phù hợp với nơi client thực sự kết nối.
Listeners và advertised listeners khác nhau thế nào?
Listeners là các cửa mà Kafka mở để nhận kết nối. Trong cấu hình này, PLAINTEXT nhận kết nối client ở 9092; CONTROLLER nhận trao đổi của controller ở 9093. Listener security protocol map ánh xạ hai tên đó sang giao thức PLAINTEXT. Đây là tên cấu hình, không phải hai topic.
Advertised listeners là địa chỉ Kafka giới thiệu lại cho client sau bước kết nối ban đầu. Có thể ví listeners là quầy tiếp tân đang mở, còn advertised listeners là địa chỉ phòng làm việc ghi trên phiếu hướng dẫn. Nếu client nhận một địa chỉ không truy cập được, kết nối ban đầu có thể thành công nhưng thao tác sau vẫn lỗi. Với client chạy trên máy host và Kafka CLI chạy trong chính container này, localhost:9092 dùng được. Khi đưa Python vào một container khác, cần thiết kế listener cho mạng đó.
Vì sao nhiều tham số replication đều bằng 1?
KAFKA_NODE_ID đặt định danh node là 1; PROCESS_ROLES cho node kiêm broker và controller. CONTROLLER_QUORUM_VOTERS chỉ ra controller mang ID 1 tại localhost:9093. Controller listener names xác định listener dành cho điều phối; inter broker listener name xác định listener dùng cho trao đổi giữa broker.
Kafka còn dùng các topic nội bộ để lưu offset consumer group và trạng thái transaction. Các tham số OFFSETS_TOPIC_REPLICATION_FACTOR, TRANSACTION_STATE_LOG_REPLICATION_FACTOR và TRANSACTION_STATE_LOG_MIN_ISR được đặt bằng 1 để phù hợp cluster một broker. Chúng không biến lab thành hệ thống có bản sao dự phòng. Khi xây production, số broker, replication factor và min ISR phải được thiết kế cùng nhau.
KAFKA_LOG_DIRS chỉ nơi Kafka lưu log dữ liệu bên trong container. Named volume được gắn đúng vào thư mục này. Không nhầm Kafka log dữ liệu với log chữ mà docker compose logs hiển thị: một bên chứa bản ghi và metadata lưu trữ, bên kia chủ yếu là thông báo vận hành.
3. Khởi động và kiểm tra
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail=100 kafkaconfig kiểm tra YAML; up -d chạy nền; ps xem container; logs giúp tìm lỗi khởi động. Container Up chưa chứng minh broker đã nhận yêu cầu. Dùng Kafka CLI để kiểm tra:
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --listNếu lệnh hoàn thành và chưa có topic, kết quả có thể rỗng. Nếu connection refused, kiểm tra log và chờ broker khởi động hoàn tất.
Hiểu từng lệnh kiểm tra
docker compose config --quiet kiểm tra khả năng đọc và tính hợp lệ của cấu hình Compose; nó không khởi động Kafka và không chứng minh ứng dụng sẽ chạy thành công. Nếu lệnh kết thúc không in lỗi, bạn có thể chuyển sang bước khởi động. YAML dùng thụt dòng để biểu diễn cấu trúc; hãy giữ khoảng trắng theo mẫu và tránh dùng tab.
docker compose up -d tạo tài nguyên cần thiết rồi chạy dịch vụ nền. Lần đầu phải tải image nên thường chậm hơn các lần sau. docker compose ps trả trạng thái container; docker compose logs --tail=100 kafka trả 100 dòng log gần nhất của dịch vụ kafka. Đọc lỗi đầu tiên có ý nghĩa thường hữu ích hơn chỉ nhìn dòng cuối sau khi tiến trình đã thoát.
Trong lệnh kiểm tra topic, docker exec yêu cầu chạy một chương trình bên trong container kafka-lab. Đường dẫn /opt/kafka/bin/kafka-topics.sh nằm trong image, nên bạn không cần cài Kafka CLI riêng trên máy host. bootstrap-server là địa chỉ ban đầu để CLI liên hệ cluster. --list yêu cầu liệt kê topic. Lệnh hoàn thành thành công với danh sách rỗng vẫn là kết quả hợp lệ trước khi tạo topic.
Mốc kiểm tra: chỉ chuyển sang tạo topic sau khi CLI liên hệ được broker. Nếu đang thấy lỗi kết nối, hãy sửa ở bước này; chạy tiếp producer sẽ tạo thêm triệu chứng nhưng không giải quyết nguyên nhân.
4. Tạo topic orders
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic orders --partitions 3 --replication-factor 1
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic ordersTopic tổ chức các sự kiện cùng chủ đề. Partition là phần của topic; offset là vị trí bản ghi trong từng partition. Lệnh describe cho biết partition, leader và replica. Topic already exists nghĩa là topic đã được tạo trước đó; bạn có thể tiếp tục dùng nó.

Đọc kết quả describe
Với topic orders mới tạo, bạn cần đối chiếu PartitionCount bằng 3 và ReplicationFactor bằng 1. Mỗi dòng partition cho biết ID partition, leader, danh sách replica và ISR. Leader là broker phục vụ đọc/ghi của partition; replica là những broker giữ bản sao; ISR là các replica đang theo kịp theo tiêu chí của Kafka. Trong lab một broker, các giá trị broker thường đều trỏ về node 1.
Ba partition tạo ba phần log độc lập trong cùng topic. Nếu partition 0 có offset 5 và partition 1 có offset 5, đó là hai vị trí khác nhau; chỉ riêng số offset chưa xác định duy nhất một message. Cần biết topic, partition và offset. Đừng hiểu replication factor 1 là mỗi message chỉ được phép đọc một lần: replication nói về số bản sao lưu trữ, còn số lần đọc phụ thuộc consumer.
Trong bài này, chọn 3 partition để chuẩn bị cho bài học chia tải. Bạn chưa cần tính số partition tối ưu cho doanh nghiệp. Điều quan trọng trước mắt là hiểu rằng việc phân chia này tác động đến song song và phạm vi bảo đảm thứ tự.
5. Gửi JSON bằng Console Producer
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic ordersNhập từng dòng dưới đây rồi nhấn Enter. Mỗi dòng trở thành một message. Nhấn Ctrl+C khi hoàn tất.
{"order_id":"ORD-1001","amount_vnd":850000,"status":"created"}
{"order_id":"ORD-1002","amount_vnd":420000,"status":"created"}
{"order_id":"ORD-1003","amount_vnd":1290000,"status":"created"}Console producer gửi text, không tự kiểm tra JSON hay quy tắc nghiệp vụ. Không có thông báo thành công cho từng dòng là bình thường. Bài 3 sẽ dùng Python để serialize JSON và xử lý kết quả gửi.
Vì sao cần -it và vì sao mỗi dòng là một message?
Trong docker exec -it, -i giữ đầu vào tiêu chuẩn mở để bạn nhập dữ liệu, còn -t cấp terminal tương tác. Console producer đọc các dòng từ đầu vào; nhấn Enter kết thúc một dòng để chương trình gửi nó. Vì vậy JSON trong lab phải nằm trên một dòng cho mỗi event. Dán một JSON được xuống dòng nhiều hàng sẽ không tự động tạo thành một JSON event hoàn chỉnh.
Ví dụ ORD-1001 mô tả một đơn có số tiền 850.000 đồng. Tên amount_vnd nêu rõ đơn vị tiền để người xử lý không nhầm với USD hay giá trị tính theo nghìn đồng. Trong hệ thống thật, thường bổ sung event_id, event_type, occurred_at và schema_version để nhận diện, giải thích và quản lý phiên bản sự kiện. Bài này giữ dữ liệu ngắn để tập trung vào đường truyền.
Khi nhấn Ctrl+C ở producer, bạn dừng chương trình gửi đang chạy trong container, không dừng broker Kafka. Có thể kiểm tra lại bằng docker compose ps. Đây là hai tiến trình khác nhau: broker tiếp tục giữ dữ liệu; producer chỉ là một client.
6. Đọc message
Mở terminal thứ hai và chạy:
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --from-beginningBạn sẽ thấy các JSON đã gửi. Consumer tiếp tục chờ dữ liệu mới đến khi nhấn Ctrl+C. from-beginning áp dụng khi chưa có offset đã lưu cho group; nó không tự đặt lại offset của một group đã đọc. Kafka bảo đảm thứ tự trong partition, không bảo đảm thứ tự toàn cục giữa ba partition.

Thử quan sát dữ liệu cũ và dữ liệu mới
Giữ consumer mở ở terminal B, rồi mở lại producer ở terminal A và gửi thêm một JSON với order_id ORD-1004. Consumer sẽ in message khi đọc được. Đây là cách quan sát một luồng liên tục: consumer không cần đợi tất cả đơn hàng được gửi xong mới bắt đầu hoạt động.
Tiếp theo, dừng consumer bằng Ctrl+C nhưng giữ producer và broker chạy. Gửi thêm ORD-1005. Message không biến mất chỉ vì lúc đó chưa có consumer hoạt động. Mở một console consumer mới với --from-beginning và không chỉ định group cũ; bạn có thể đọc lại dữ liệu vẫn còn trong retention của topic. Bài 4 sẽ làm rõ việc lưu offset theo consumer group và cách tiếp tục đúng vị trí đã đọc.
Trong Kafka, đọc message thường không xóa message ngay. Dữ liệu được giữ theo chính sách retention hoặc cleanup của topic. Do đó, thấy ORD-1001 lần nữa khi đọc lại là hành vi mong đợi của bài lab, không phải bằng chứng producer đã gửi trùng. Ứng dụng thật vẫn cần nhận diện event và thiết kế xử lý lặp an toàn.
Quan sát partition và offset ngay trong console
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --from-beginning --property print.partition=true --property print.offset=trueLệnh này bổ sung thông tin partition và offset cạnh nội dung. Giá trị cụ thể phụ thuộc lần gửi và dữ liệu đã có, nên không đối chiếu máy móc với một ảnh mẫu. Hãy kiểm tra message của mình xuất hiện ở partition nào và offset thay đổi như thế nào trong cùng partition. Producer trong bài chưa đặt key; không suy luận rằng ba đơn sẽ tự động chia đều một đơn mỗi partition.
7. Dừng lab, xử lý lỗi và bài tập
docker compose downLệnh này dừng và xóa container, network của Compose; named volume kafka_data vẫn còn. Chạy up -d để tiếp tục. Không dùng down -v nếu muốn giữ message, vì tùy chọn -v xóa volume của lab.
- Connection refused: kiểm tra container và log; broker có thể chưa sẵn sàng.
- Port already allocated: cổng 9092 đang được dịch vụ khác sử dụng.
- Không thấy dữ liệu: kiểm tra tên topic và from-beginning; kiểm tra producer có thực sự gửi dòng dữ liệu.
- Permission denied với Docker socket: kiểm tra quyền tài khoản dùng Docker theo hướng dẫn cài đặt của hệ điều hành.
Bài tập: tạo topic payments với một partition, gửi hai payment event rồi đọc lại. Dừng container, khởi động lại và xác nhận các event vẫn còn. Giải thích tại sao replication factor không thể đặt bằng 3 trên cluster chỉ có một broker.
Bài tập có hướng dẫn giải
Bước 1: tạo payments bằng lệnh dưới đây. Dùng một partition để việc quan sát thứ tự trong lab đơn giản hơn.
docker exec kafka-lab /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic payments --partitions 1 --replication-factor 1Bước 2: mở console producer cho topic payments và nhập hai dòng sau. Sau mỗi dòng nhấn Enter rồi kết thúc producer bằng Ctrl+C.
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic payments{"payment_id":"PAY-01","order_id":"ORD-1001","amount_vnd":850000}
{"payment_id":"PAY-02","order_id":"ORD-1002","amount_vnd":420000}Bước 3: đọc payments từ đầu bằng consumer. Hai payment phải giữ đúng nội dung đã nhập. Dừng các client, chạy docker compose down rồi docker compose up -d. Khi broker sẵn sàng, đọc lại bằng một console consumer mới từ đầu. Nếu volume vẫn được giữ và retention chưa loại bỏ dữ liệu, bạn vẫn thấy hai payment.
docker exec -it kafka-lab /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic payments --from-beginningGiải thích: container có thể được tạo lại, còn named volume được Compose giữ qua down thông thường. Replication factor 3 cần ba broker riêng để đặt ba replica của mỗi partition; ba partition trên một broker không tạo ra ba broker và không thay thế ba bản sao trên các máy khác nhau.
Khi nào coi bài lab đã hoàn thành?
Bạn có thể tự giải thích được image và container khác nhau thế nào; chỉ ra broker và controller làm gì; tạo rồi describe topic; gửi JSON, đọc lại và quan sát dữ liệu mới; dừng client mà broker vẫn hoạt động; và khởi động lại container mà dữ liệu trong volume vẫn còn. Đây là các dấu hiệu hiểu được luồng thao tác, thay vì chỉ sao chép lệnh đến khi hết lỗi.
Bài tiếp theo: Python Producer và Consumer gửi, nhận JSON để chuyển từ thao tác terminal sang ứng dụng thực tế.
Tài liệu tham khảo
@techdata.ai

Comments are closed!