Bài 5

Tự viết Producer / Consumer

Kafka Connect rất mạnh để di chuyển dữ liệu giữa hệ thống và Kafka chỉ bằng cấu hình, không cần viết code. Nhưng khi cần logic xử lý riêng — biến đổi dữ liệu phức tạp, gọi API ngoài, áp dụng nghiệp vụ, hay xây ứng dụng real-time — bạn sẽ phải tự viết ứng dụng Kafka client. Bài này hướng dẫn dùng thư viện Python để viết Producer (gửi message) và Consumer (đọc message), kèm các vấn đề về độ bền và đảm bảo phân phối.

Connect vs tự viết client — chọn cái nào
Cấu hình hay code?

Cả hai đều dùng Kafka, nhưng giải quyết hai bài toán khác nhau. Hãy chọn đúng công cụ ngay từ đầu:

Tiêu chíKafka ConnectTự viết Producer/Consumer
Mục đíchTích hợp, di chuyển dữ liệu giữa hệ thống ↔ KafkaỨng dụng có logic xử lý riêng, app real-time
Cách dùngCấu hình JSON, không cần codeViết code (Python, Java, Go...)
Connector có sẵnRất nhiều (JDBC, Debezium, S3, Elastic...)Không — tự xây từ đầu
Scale & chịu lỗiCó sẵn (distributed mode, rebalance)Phải tự lo (consumer group, retry, restart)
Business logicHạn chế (chỉ SMT đơn giản)Toàn quyền: transform, gọi API, nghiệp vụ phức tạp
Offset & monitoringWorker quản lý tự độngTự quản offset, commit, giám sát
Nguyên tắc chọn: chỉ di chuyển dữ liệu thuần (DB → Kafka → DB/kho) thì dùng Kafka Connect. Có xử lý nghiệp vụ trên từng message thì tự viết Producer/Consumer (hoặc Kafka Streams nếu cần xử lý luồng có trạng thái).
Thư viện & kết nối
Python client cho Kafka

Hai thư viện Python phổ biến để nói chuyện với Kafka:

Thư việnĐặc điểm
confluent-kafkaHiệu năng cao, là client chính thức của Confluent, bọc thư viện C librdkafka. Khuyến nghị cho production.
kafka-pythonThuần Python, dễ đọc, dễ cài, hợp để học và dự án nhỏ. Hiệu năng thấp hơn.

Bài này dùng confluent-kafka. Cài bằng pip:

# cài client chính thức của Confluent
pip install confluent-kafka

Tham số quan trọng nhất là bootstrap.servers — địa chỉ broker để client kết nối lần đầu:

# từ máy host (chạy script Python ngoài Docker)
bootstrap.servers = "localhost:29092"

# từ trong cùng Docker network (vd container khác trong cdc-net)
bootstrap.servers = "kafka:9092"
Khớp với cdc-lab: Kafka của lab expose ra host ở localhost:29092, còn listener nội bộ trong network cdc-netkafka:9092. Chạy script trên máy bạn thì dùng localhost:29092; đóng gói thành container chung mạng thì dùng kafka:9092.
Viết Producer
Gửi message vào topic

Producer chịu trách nhiệm gửi message lên topic. Dưới đây là một producer hoàn chỉnh gửi vài bản ghi JSON vào topic demo:

# producer.py
import json
from confluent_kafka import Producer

# cấu hình kết nối broker
conf = {
    "bootstrap.servers": "localhost:29092",
    "acks": "all",              # chờ mọi replica xác nhận (bền nhất)
    "enable.idempotence": True,   # tránh ghi trùng khi retry
    "linger.ms": 10,             # gom message ~10ms để batch
    "batch.size": 32768,         # kích thước batch (bytes)
}

producer = Producer(conf)

# callback báo kết quả gửi (thành công / thất bại)
def delivery_report(err, msg):
    if err is not None:
        print(f"Gửi THẤT BẠI: {err}")
    else:
        print(f"OK -> topic={msg.topic()} "
              f"partition={msg.partition()} offset={msg.offset()}")

orders = [
    {"id": 1, "user": "an",  "amount": 120},
    {"id": 2, "user": "binh", "amount": 340},
    {"id": 3, "user": "an",  "amount": 90},
]

for o in orders:
    producer.produce(
        topic="demo",
        key=str(o["user"]).encode("utf-8"),     # key quyết định partition
        value=json.dumps(o).encode("utf-8"),       # value serialize sang JSON bytes
        callback=delivery_report,
    )
    producer.poll(0)   # gọi callback cho các message đã gửi xong

producer.flush()        # chặn cho tới khi gửi hết buffer

Các điểm cốt lõi của Producer:

Tham số acks đánh đổi giữa độ bền và tốc độ:

acksÝ nghĩa
0Không chờ xác nhận — nhanh nhất, dễ mất message
1Chờ leader ghi xong — cân bằng, mất nếu leader chết trước khi replica kịp
allChờ mọi in-sync replica — bền nhất, chậm hơn (khuyến nghị production)
Viết Consumer
Đọc message từ topic

Consumer đăng ký (subscribe) một hay nhiều topic rồi lặp poll để lấy message. Đây là consumer hoàn chỉnh đọc topic demo với commit thủ công:

# consumer.py
import json
from confluent_kafka import Consumer, KafkaException

conf = {
    "bootstrap.servers": "localhost:29092",
    "group.id": "my-group",          # tên consumer group
    "auto.offset.reset": "earliest",  # chưa có offset thì đọc từ đầu topic
    "enable.auto.commit": False,      # tự commit để xử lý xong mới đánh dấu
}

consumer = Consumer(conf)
consumer.subscribe(["demo"])

try:
    while True:
        msg = consumer.poll(1.0)   # chờ tối đa 1 giây
        if msg is None:
            continue
        if msg.error():
            raise KafkaException(msg.error())

        # deserialize JSON về dict
        data = json.loads(msg.value().decode("utf-8"))
        key = msg.key().decode("utf-8") if msg.key() else None

        print(f"key={key} value={data} "
              f"partition={msg.partition()} offset={msg.offset()}")

        # ... xử lý nghiệp vụ tại đây ...

        consumer.commit(msg)   # xử lý xong mới commit offset
except KeyboardInterrupt:
    pass
finally:
    consumer.close()        # rời group gọn gàng, kích hoạt rebalance

Các khái niệm then chốt của Consumer:

Delivery semantics
Đảm bảo phân phối message

Thứ tự giữa xử lýcommit offset quyết định bạn nhận được mức đảm bảo nào:

Mức đảm bảoCách làmHệ quả
At-most-onceCommit offset trước khi xử lýKhông bao giờ trùng, nhưng có thể mất nếu chết giữa chừng
At-least-onceXử lý xong rồi mới commitKhông mất, nhưng có thể trùng khi retry — phổ biến nhất
Exactly-onceTransaction + idempotent producer (read-process-write)Không mất, không trùng — nhưng phức tạp và tốn hiệu năng

Thực tế đa số dùng at-least-once (commit sau khi xử lý) rồi làm cho việc xử lý idempotent để chịu được message trùng: ví dụ dùng khóa nghiệp vụ (order id) với UPSERT hoặc kiểm tra "đã xử lý chưa" trước khi tác động — kết quả cuối cùng giống như chỉ xử lý đúng một lần.

Xử lý lỗi & độ bền
Không mất, không kẹt pipeline

Một consumer chạy production cần bao bọc xử lý lỗi cẩn thận: bắt exception cho từng message, retry với lỗi tạm thời, đẩy message hỏng sang Dead Letter Topic, và tắt êm khi nhận tín hiệu dừng.

# consumer có DLQ tự xây + commit sau khi xử lý
import json, signal
from confluent_kafka import Producer, Consumer

running = True
def stop(*_):           # graceful shutdown
    global running
    running = False
signal.signal(signal.SIGINT, stop)
signal.signal(signal.SIGTERM, stop)

base = {"bootstrap.servers": "localhost:29092"}
consumer = Consumer({**base, "group.id": "my-group",
                     "auto.offset.reset": "earliest",
                     "enable.auto.commit": False})
dlq = Producer(base)
consumer.subscribe(["demo"])

try:
    while running:
        msg = consumer.poll(1.0)
        if msg is None or msg.error():
            continue
        try:
            data = json.loads(msg.value().decode("utf-8"))
            handle(data)                # nghiệp vụ của bạn
            consumer.commit(msg)         # chỉ commit khi thành công
        except Exception as e:
            # gửi message lỗi sang Dead Letter Topic để điều tra
            dlq.produce("demo.DLT", key=msg.key(), value=msg.value())
            dlq.flush()
            consumer.commit(msg)         # bỏ qua, đi tiếp
            print(f"Đẩy DLT: {e}")
finally:
    consumer.close()
Cẩn thận: commit trước khi xử lý có thể mất message; xử lý xong mới commit có thể trùng nếu chết ngay trước lúc commit. Hãy chọn at-least-once + xử lý idempotent cho đa số trường hợp.
Kafka Streams / Faust (mở rộng)
Khi cần xử lý luồng có trạng thái
Vượt qua Producer/Consumer thuần: nếu cần xử lý luồng có trạng thái — gộp (aggregate), nối (join) nhiều topic, hay cửa sổ thời gian (windowing) — đừng tự quản lý state thủ công. Hãy dùng Kafka Streams (Java) hoặc Faust (Python) để có sẵn state store, windowing, fault-tolerance; hoặc dùng Spark Structured Streaming cho xử lý luồng quy mô lớn.
Thực hành nhanh
Chạy thử với cdc-lab
Mẹo: gửi nhiều message cùng một key rồi quan sát chúng vào cùng một partition trên Kafka UI — đó là cách Kafka giữ thứ tự theo key.
← Quay lại
Bài 4: Vận hành & Xử lý lỗi