Kafka
Architecture
Messaging
Distributed Systems
DevOps

Apache Kafka từ A–Z: kiến trúc, flow hoạt động và lỗi thường gặp 🗞️

Pin Nguyen

Pin Nguyen

Software Developer

July 19, 2026

17 min read

Apache Kafka từ A–Z: kiến trúc, flow hoạt động và lỗi thường gặp 🗞️

Kafka có mặt ở gần như mọi hệ thống backend quy mô lớn, nhưng nhiều người dùng nó như một hộp đen: cứ produce rồi consume, tới lúc lag tăng vọt hay mất message giữa production mới cuống. Bài này gom lại toàn bộ kiến trúc Kafka — từ topic/partition/offset tới Kafka Connect — kèm flow hoạt động end-to-end và các lỗi thực tế hay gặp, giải thích bằng ví von dễ hình dung.

1. Kafka là gì?

Apache Kafka là một distributed event streaming platform — nền tảng truyền và lưu trữ luồng sự kiện (events) phân tán, với 3 khả năng chính:

  1. Publish/Subscribe — ghi và đọc stream of events (giống message queue nhưng mạnh hơn).
  2. Store — lưu events bền vững (durable), có thể replay lại bất cứ lúc nào.
  3. Process — xử lý stream real-time (Kafka Streams, ksqlDB) hoặc kết nối hệ thống ngoài (Kafka Connect).

Ví dụ dễ hiểu: Hãy tưởng tượng Kafka như một tòa soạn báo khổng lồ:

  • Producer = phóng viên gửi bài về tòa soạn.
  • Topic = chuyên mục báo (Thể thao, Kinh tế...).
  • Broker = tòa soạn lưu trữ tất cả bài viết.
  • Consumer = độc giả đọc báo. Mỗi độc giả tự đánh dấu mình đã đọc đến bài nào (offset), báo cũ vẫn nằm trong kho lưu trữ chứ không bị xé đi sau khi đọc.

Điểm khác biệt lớn nhất so với message queue truyền thống (RabbitMQ, ActiveMQ): message không bị xóa sau khi consume — nó được giữ lại theo retention policy, cho phép nhiều consumer độc lập đọc cùng một data, và replay lại từ đầu khi cần.

2. Kiến trúc tổng quan

                        ┌─────────────────────────────────────┐
                        │           KAFKA CLUSTER             │
                        │                                     │
 ┌──────────┐           │  ┌────────┐ ┌────────┐ ┌────────┐  │          ┌──────────┐
 │ Producer │──produce──▶  │Broker 1│ │Broker 2│ │Broker 3│  ──consume──▶ Consumer │
 └──────────┘           │  └────────┘ └────────┘ └────────┘  │          │  Group   │
 ┌──────────┐           │       ▲                            │          └──────────┘
 │ Producer │──produce──▶       │                            │          ┌──────────┐
 └──────────┘           │  ┌────┴─────────┐                  ──consume──▶ Consumer │
                        │  │ KRaft Quorum │                  │          │  Group   │
                        │  │ (metadata)   │                  │          └──────────┘
                        │  └──────────────┘                  │
                        └─────────────────────────────────────┘
  • Producer ghi message vào Topic trên Broker.
  • Consumer (thường nhóm thành Consumer Group) đọc message từ topic.
  • Cluster gồm nhiều broker; metadata (topic, partition, leader...) được quản lý bởi KRaft (Kafka Raft — thay thế ZooKeeper từ Kafka 3.x, ZooKeeper đã bị loại bỏ hoàn toàn từ Kafka 4.0).

3. Các thành phần cốt lõi

3.1. Topic

Topickênh logic để phân loại message, giống tên bảng trong database hoặc tên thư mục.

topic: order.created
topic: payment.completed
topic: tracking.events

3.2. Partition

Mỗi topic được chia thành nhiều partition — đây là đơn vị song song hóa (parallelism)phân tán của Kafka.

Topic "orders" (3 partitions):

Partition 0:  [msg0][msg1][msg2][msg3] ──▶ append tiếp vào cuối
Partition 1:  [msg0][msg1][msg2]       ──▶
Partition 2:  [msg0][msg1][msg2][msg3][msg4] ──▶

Đặc điểm quan trọng:

  • Mỗi partition là một append-only log: message chỉ được ghi thêm vào cuối, không sửa/xóa.
  • Thứ tự (ordering) chỉ được đảm bảo trong phạm vi 1 partition, KHÔNG đảm bảo giữa các partition.
  • Message có cùng key sẽ luôn vào cùng 1 partition (hash key % số partition) → dùng key để giữ ordering. Ví dụ: dùng user_id làm key thì mọi event của cùng 1 user luôn được xử lý theo đúng thứ tự. Lưu ý: điều này chỉ đúng chừng nào số partition không đổi — tăng partition sẽ làm key nhảy sang partition khác.
  • Message không có key được rải đều qua các partition (sticky partitioner) → throughput tốt hơn nhưng không giữ được thứ tự theo thực thể.

3.3. Offset

Offsetsố thứ tự của message trong partition (0, 1, 2, ...). Consumer dùng offset để biết mình đã đọc đến đâu.

Partition 0: [0][1][2][3][4][5][6][7][8][9]
                          ▲              ▲
                  committed offset    log end offset
                  (consumer đã xử lý  (message mới nhất)
                   đến msg 3)

                  └──── LAG = 9 - 3 = 6 ────┘
  • Committed offset: vị trí consumer đã xác nhận xử lý xong.
  • Consumer lag = log end offset − committed offset → chỉ số quan trọng nhất để monitor.

3.4. Broker

Broker là một server Kafka — nhận message từ producer, lưu xuống disk, phục vụ consumer. Một cluster production thường có tối thiểu 3 brokers.

3.5. Replication (Leader và Follower)

Mỗi partition có nhiều replica nằm trên các broker khác nhau để chống mất data:

Topic "orders", replication.factor = 3:

              Broker 1        Broker 2        Broker 3
Partition 0:  LEADER          follower        follower
Partition 1:  follower        LEADER          follower
Partition 2:  follower        follower        LEADER
  • Leader: replica duy nhất nhận write, và mặc định cũng phục vụ toàn bộ read cho partition đó (từ Kafka 2.4 có thể cấu hình cho consumer đọc từ follower gần nhất để tiết kiệm băng thông cross-rack).
  • Follower: liên tục sao chép (replicate) data từ leader.
  • ISR (In-Sync Replicas): tập các replica (gồm cả leader) đang bắt kịp leader. Khi leader chết, một follower trong ISR được bầu làm leader mới → cluster tự phục hồi, không mất data.
  • min.insync.replicas: số replica trong ISR tối thiểu phải ghi thành công thì write mới được chấp nhận — chỉ có tác dụng khi producer dùng acks=all. Với replication.factor=3 + min.insync.replicas=2: chết 1 broker vẫn ghi bình thường, chết 2 thì broker từ chối ghi (thà báo lỗi còn hơn âm thầm mất data).

3.6. Producer

Producer là client ghi message vào Kafka. Các config quan trọng:

Config Ý nghĩa
acks=0 Gửi xong không chờ xác nhận — nhanh nhất, có thể mất data
acks=1 Chờ leader ghi xong — cân bằng
acks=all Chờ toàn bộ ISR ghi xong — an toàn nhất, chậm hơn; đi kèm min.insync.replicas mới thực sự chống mất data
retries Số lần retry khi gửi fail
enable.idempotence=true Chống duplicate khi retry (exactly-once ở phía produce) — mặc định true từ Kafka 3.0
batch.size / linger.ms Gom message thành batch để tăng throughput
compression.type Nén batch (lz4, snappy, zstd) — giảm băng thông và disk

Flow produce một message:

1. producer.send(topic="orders", key="user_123", value={...})
2. Serializer chuyển value thành bytes (JSON/Avro/Protobuf)
3. Partitioner tính: hash("user_123") % 3 = partition 1
4. Message vào buffer, gom batch theo batch.size/linger.ms
5. Batch gửi tới LEADER của partition 1
6. Leader ghi log, followers replicate
7. Leader trả ack về producer (tùy acks config)

3.7. Consumer và Consumer Group

Consumer đọc message từ topic. Consumer Group là cơ chế scale:

Topic "orders" (4 partitions) — Consumer Group "billing-service":

Partition 0 ──▶ Consumer A
Partition 1 ──▶ Consumer A       Mỗi partition chỉ được gán cho
Partition 2 ──▶ Consumer B       ĐÚNG 1 consumer trong group
Partition 3 ──▶ Consumer C

Quy tắc vàng:

  • 1 partition chỉ được 1 consumer trong group xử lý tại một thời điểm.
  • Số consumer > số partition → consumer thừa sẽ ngồi chơi (idle).
  • Nhiều group khác nhau đọc cùng topic thì độc lập hoàn toàn — mỗi group có bộ offset riêng. Ví dụ: billing-serviceanalytics-service cùng đọc topic orders nhưng không ảnh hưởng nhau.

3.8. Rebalancing

Khi consumer join/leave group (deploy, crash, scale), Kafka phân chia lại partition giữa các consumer — gọi là rebalance. Trong lúc rebalance (kiểu eager cũ), toàn bộ group tạm dừng consume → gây "stop-the-world". Các phiên bản mới dùng cooperative sticky / incremental rebalance để giảm gián đoạn.

3.9. Retention và Compaction

  • Retention theo thời gian/dung lượng: retention.ms=604800000 (7 ngày) → message cũ hơn 7 ngày bị xóa.
  • Log compaction (cleanup.policy=compact): chỉ giữ message mới nhất của mỗi key — phù hợp lưu state/snapshot (ví dụ: số dư ví mới nhất của mỗi user).

4. Kafka Connect và Connector

4.1. Khái niệm

Kafka Connect là framework chuẩn để stream data giữa Kafka và hệ thống ngoài mà không cần viết producer/consumer code — chỉ cần config JSON.

                    KAFKA CONNECT
┌──────────┐   ┌─────────────────┐   ┌───────┐   ┌────────────────┐   ┌───────────────┐
│  MySQL   │──▶│ Source Connector│──▶│ Kafka │──▶│ Sink Connector │──▶│ S3 / ES /     │
│ Postgres │   │ (vd: Debezium)  │   │ Topics│   │ (vd: S3 Sink)  │   │ BigQuery ...  │
└──────────┘   └─────────────────┘   └───────┘   └────────────────┘   └───────────────┘
  • Source Connector: kéo data từ ngoài vào Kafka (VD: Debezium đọc binlog MySQL → CDC events).
  • Sink Connector: đẩy data từ Kafka ra ngoài (VD: S3 Sink ghi Parquet lên data lake).

Muốn đào sâu Source/Sink, CDC với Debezium và SMT, xem bài riêng: Kafka Connect: bơm dữ liệu vào/ra Kafka không cần viết code.

4.2. Kiến trúc Kafka Connect

Connect Cluster (distributed mode):

┌─────────── Worker 1 ──────────┐  ┌─────────── Worker 2 ──────────┐
│ Connector A                   │  │                               │
│  ├─ Task A-0 (đọc table X,Y)  │  │  ├─ Task A-1 (đọc table Z)    │
│ Connector B                   │  │                               │
│  ├─ Task B-0                  │  │  ├─ Task B-1                  │
└───────────────────────────────┘  └───────────────────────────────┘
         Worker chết → tasks tự động chuyển sang worker còn sống
Thành phần Vai trò
Connector Định nghĩa job mức cao (kết nối đâu, topic nào), deploy qua REST API
Task Đơn vị thực thi thật sự, chạy song song (tasks.max)
Worker JVM process chạy connectors/tasks; distributed mode tự rebalance khi worker chết
Converter Serialize/deserialize (JSON, Avro, Protobuf) — thường đi kèm Schema Registry
SMT Single Message Transform — transform nhẹ inline: rename field, mask PII, route topic

4.3. Ví dụ config

Source — Debezium CDC từ MySQL:

{
  "name": "mysql-cdc-orders",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "database.hostname": "mysql-prod",
    "database.include.list": "shop",
    "table.include.list": "shop.orders",
    "topic.prefix": "cdc",
    "tasks.max": "1"
  }
}

Config trên đã lược bớt database.user / database.password và cấu hình schema history cho gọn. tasks.max luôn là 1 với Debezium MySQL vì chỉ có một binlog để đọc — Source connector không tự chia task tùy ý như Sink.

Sink — đổ data lake S3:

{
  "name": "s3-sink-events",
  "config": {
    "connector.class": "io.confluent.connect.s3.S3SinkConnector",
    "tasks.max": "4",
    "topics": "tracking.events",
    "s3.bucket.name": "datalake-raw",
    "format.class": "io.confluent.connect.s3.format.parquet.ParquetFormat",
    "flush.size": "10000",
    "rotate.interval.ms": "600000"
  }
}

4.4. Khi nào dùng Connect, khi nào tự viết consumer?

Dùng Kafka Connect Tự viết consumer / stream processing
Ingest/egress thuần (DB → Kafka, Kafka → S3) Business logic phức tạp
Transform đơn giản (SMT: mask, rename) Enrich, join nhiều topic, aggregate
Muốn tận dụng offset management, retry sẵn có Cần kiểm soát chi tiết error handling, DLQ tùy biến
Connector có sẵn trên Confluent Hub Không có connector phù hợp

5. Flow hoạt động end-to-end (ví dụ minh họa)

Bài toán: Hệ thống thanh toán — user bấm nút thanh toán, cần: trừ tiền, gửi notification, ghi analytics.

                                    ┌──────────────────────────────────┐
                                    │      Topic: payment.events       │
┌──────────┐    produce             │  P0: [e1][e4][e7]                │
│ Payment  │ ──(key=user_id)──────▶ │  P1: [e2][e5]                    │
│ API      │                        │  P2: [e3][e6][e8]                │
└──────────┘                        └──────────────────────────────────┘
                                        │              │           │
                          ┌─────────────┘              │           └─────────────┐
                          ▼                            ▼                         ▼
                 Group: "wallet-svc"          Group: "notify-svc"       Sink Connector
                 (trừ tiền, 3 consumers)      (gửi push, 2 consumers)   (đổ BigQuery)

Diễn giải từng bước:

  1. Produce: Payment API tạo event {user_id: "u123", amount: 50000, status: "success"}, dùng user_id làm key → mọi event của u123 luôn vào cùng partition → wallet-svc xử lý đúng thứ tự (không bị race trừ tiền).
  2. Store: Broker (leader) ghi event vào log, followers replicate. Với acks=all, producer chỉ nhận OK khi toàn bộ ISR đã ghi.
  3. Consume song song, độc lập: 3 group đọc cùng data, mỗi group có offset riêng:
    • wallet-svc trừ tiền.
    • notify-svc gửi push notification.
    • Sink connector đổ BigQuery cho team analytics.
  4. Commit offset: mỗi consumer sau khi xử lý xong commit offset. Nếu consumer crash trước khi commit → sau restart sẽ đọc lại message đó (at-least-once → cần xử lý idempotent, ví dụ check transaction_id đã tồn tại chưa trước khi trừ tiền).
  5. Replay khi cần: analytics phát hiện bug tính toán → reset offset của group analytics về 7 ngày trước, chạy lại toàn bộ mà không ảnh hưởng wallet-svc hay notify-svc.

Delivery semantics:

Semantics Ý nghĩa Cách đạt được
At-most-once Có thể mất message, không duplicate Commit offset TRƯỚC khi xử lý
At-least-once Không mất, có thể duplicate Commit offset SAU khi xử lý (phổ biến nhất)
Exactly-once Không mất, không duplicate Idempotent producer + transactions (isolation.level=read_committed), hoặc consumer tự idempotent

6. Các lỗi thường gặp và cách xử lý

6.1. Consumer Lag — consume không theo kịp produce ⭐

Đây là vấn đề phổ biến nhất. Producer ghi 10.000 msg/s nhưng consumer chỉ xử lý được 4.000 msg/s → lag tăng liên tục.

Lag tăng dần theo thời gian:

msg/s │  produce rate ────────────────── 10k
      │
      │  consume rate ────────────────── 4k
      │
 lag  │            ╱╱╱╱  ← lag = tích lũy 6k msg/s, càng lúc càng xa
      └──────────────────────────▶ time

Nguyên nhân & giải pháp:

Nguyên nhân Giải pháp
Xử lý mỗi message quá chậm (gọi API ngoài, ghi DB từng record) Batch processing: gom N messages rồi bulk insert; dùng async I/O; cache
Ít consumer hơn khả năng scale Thêm consumer instance (tối đa = số partition)
Số partition quá ít → không scale thêm consumer được Tăng số partition (lưu ý: làm thay đổi mapping key→partition, ảnh hưởng ordering)
Fetch config chưa tối ưu Tăng max.poll.records, fetch.min.bytes, fetch.max.bytes
Một message "độc" (poison pill) làm consumer retry mãi Dead Letter Queue (DLQ): đẩy message lỗi sang topic riêng, xử lý sau
Traffic spike tạm thời Chấp nhận lag tạm, đảm bảo retention đủ dài để không mất data; autoscale consumer theo lag (KEDA trên Kubernetes)

Monitoring bắt buộc: theo dõi consumer_lag per partition (Burrow, Kafka Exporter + Prometheus/Grafana), alert khi lag vượt ngưỡng hoặc tăng liên tục.

6.2. Rebalance liên tục (rebalance storm)

Triệu chứng: consumer group liên tục rebalance, throughput tụt về gần 0, log đầy Attempt to heartbeat failed since group is rebalancing.

Nguyên nhân phổ biến: xử lý 1 batch lâu hơn max.poll.interval.ms (mặc định 5 phút) → group coordinator coi consumer là đã chết → kick khỏi group → rebalance → consumer quay lại → rebalance tiếp → vòng lặp.

Phân biệt 2 timeout hay bị nhầm: session.timeout.ms (mặc định 45s) đo heartbeat — chạy ở thread nền, chỉ báo consumer còn sống; còn max.poll.interval.ms đo khoảng cách giữa 2 lần gọi poll() — tức tốc độ xử lý message thật sự. Rebalance storm gần như luôn do cái thứ hai.

Giải pháp:

  • Giảm max.poll.records (xử lý ít message hơn mỗi vòng poll).
  • Tăng max.poll.interval.ms nếu job thật sự cần xử lý lâu.
  • Tách phần xử lý nặng ra worker thread riêng, thread poll chỉ lo poll + commit.
  • Dùng partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor (phải ghi đủ tên class) để rebalance không stop-the-world.
  • Đặt group.instance.id (static membership) để restart/deploy không trigger rebalance.

6.3. Message bị duplicate

Nguyên nhân: consumer xử lý xong nhưng crash trước khi commit offset → restart đọc lại; hoặc producer retry khi network timeout nhưng message đầu thực ra đã ghi thành công.

Giải pháp:

  • Phía producer: enable.idempotence=true.
  • Phía consumer: thiết kế idempotent processing — dùng unique key (event_id) check trước khi ghi DB, hoặc upsert thay vì insert.

6.4. Mất message

Tình huống Nguyên nhân Phòng tránh
Producer mất msg acks=0/1 + leader chết trước khi replicate acks=all, min.insync.replicas=2
Consumer "mất" msg Auto-commit offset trước khi xử lý xong, rồi crash Tắt auto-commit, commit thủ công SAU khi xử lý
Msg bị xóa trước khi đọc Lag lớn hơn retention → message hết hạn bị xóa Tăng retention.ms, alert lag sớm
Bật unclean.leader.election.enable=true Follower ngoài ISR được bầu làm leader → mất msg chưa replicate Giữ mặc định false

6.5. Message quá lớn

Lỗi: RecordTooLargeException (mặc định giới hạn ~1MB).

Giải pháp: nén (compression.type=zstd); hoặc claim-check pattern — lưu payload lớn lên S3, chỉ gửi reference qua Kafka; hạn chế nới giới hạn vì ảnh hưởng memory và latency toàn cluster. Nếu buộc phải nới thì phải nới đồng bộ cả 3 phía, chỉ sửa một chỗ sẽ vẫn lỗi: broker message.max.bytes (hoặc max.message.bytes ở mức topic), producer max.request.size, consumer fetch.max.bytes.

6.6. Partition skew — phân bố lệch

Triệu chứng: một partition chứa 80% traffic (hot partition), consumer của partition đó quá tải trong khi các consumer khác rảnh.

Nguyên nhân: chọn key có cardinality thấp hoặc phân bố lệch (VD: key = merchant_id mà 1 merchant chiếm phần lớn giao dịch).

Giải pháp: chọn key phân bố đều hơn (VD: user_id thay vì merchant_id), hoặc key composite (merchant_id + bucket ngẫu nhiên) nếu chấp nhận mất ordering theo merchant.

6.7. Offset out of range / OffsetOutOfRangeException

Nguyên nhân: consumer offline lâu, offset đã commit trỏ tới message đã bị xóa bởi retention.

Xử lý: config auto.offset.reset:

  • earliest → đọc lại từ message cũ nhất còn tồn tại (an toàn cho data pipeline).
  • latest → nhảy tới message mới nhất (chấp nhận bỏ qua data cũ).

6.8. Schema thay đổi làm vỡ consumer

Tình huống: producer đổi format (xóa field, đổi type) → consumer deserialize fail hàng loạt.

Giải pháp: dùng Schema Registry (Avro/Protobuf) — mọi schema mới phải được registry duyệt trước khi producer dùng, schema phá vỡ tương thích sẽ bị chặn ngay từ lúc đăng ký. Nhớ chọn đúng compatibility mode:

  • BACKWARD (mặc định): consumer dùng schema mới đọc được data ghi bằng schema cũ → phải nâng cấp consumer trước, producer sau. Cho phép xóa field và thêm field có default.
  • FORWARD: consumer dùng schema cũ vẫn đọc được data ghi bằng schema mới → nâng cấp producer trước. Chọn cái này khi bạn không kiểm soát được hết consumer (nhiều team dùng chung topic).
  • FULL: thỏa cả hai chiều — an toàn nhất nhưng gò bó nhất.

7. Checklist config production tham khảo

# ---- Producer ----
acks=all
enable.idempotence=true
compression.type=zstd
linger.ms=10
batch.size=65536

# ---- Consumer ----
enable.auto.commit=false          # commit thủ công sau khi xử lý
max.poll.records=200
max.poll.interval.ms=300000
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
auto.offset.reset=earliest

# ---- Topic (đặt lúc tạo topic, không phải file config của client) ----
replication.factor=3              # truyền qua --replication-factor khi tạo topic
min.insync.replicas=2             # chỉ có tác dụng cùng acks=all ở producer
retention.ms=604800000            # 7 ngày

8. Tóm tắt nhanh

Khái niệm Một câu ghi nhớ
Topic Kênh logic phân loại message
Partition Đơn vị song song hóa; ordering chỉ trong 1 partition
Offset Số thứ tự message; consumer tự quản lý vị trí đọc
Broker Server lưu trữ và phục vụ message
Replication/ISR Chống mất data; leader nhận write, follower sao chép
Consumer Group Scale consume; 1 partition ↔ 1 consumer trong group
Rebalance Chia lại partition khi group thay đổi thành viên
Kafka Connect Framework ingest/egress bằng config, không cần code
Source/Sink Connector Kéo data vào Kafka / đẩy data ra ngoài
Consumer Lag Chỉ số quan trọng nhất — produce nhanh hơn consume
DLQ Nơi chứa message lỗi để không chặn pipeline

Muốn tra nhanh từng thuật ngữ, ghé từ điển thuật ngữ — mọi khái niệm Kafka ở trên đều có mục riêng kèm ví von dễ nhớ. 🗞️

Pin Nguyen

Pin Nguyen

July 19, 2026
17 min read

Back to Blog

pin nguyen cute

Full-stack engineer building data platforms and big-data systems — from real-time pipelines and scalable APIs to polished user interfaces.

Available for work

Connect

nguyenthanhpin95@gmail.com

© 2026 Pin Nguyen. Built with Next.js & TypeScript.