---
title: Kafka 架构
url: https://doc.liz6.com/distributed-systems/07-messages-and-streams/02-kafka-architecture
locale: zh
area: distributed-systems
tags:
- distributed-systems
- 消息与流
date: 2026-06-30
modified: 2026-07-11
description: Kafka 把消息系统建模为不可变的、分区的、可回放的 log——不是"消费完就删"的队列。分区内有序保证同 key 消息的顺序,ISR 机制控制复制和持久性的权衡,consumer group 把分区所有权协商变成分布式协调问题。
---

# Kafka 架构

> Kafka 把消息系统建模为不可变的、分区的、可回放的 log——不是"消费完就删"的队列。分区内有序保证同 key 消息的顺序,ISR 机制控制复制和持久性的权衡,consumer group 把分区所有权协商变成分布式协调问题。

## Partition: 有序不可变 Log

```
Topic = 多 partition:
  Partition 0: [offset 0][offset 1][offset 2]...[offset N]  ← append only
  Partition 1: [offset 0][offset 1][offset 2]...[offset N]
  Partition 2: [offset 0][offset 1][offset 2]...[offset N]

Producer: choose partition by key: partition = murmur2(key) % N_partitions
  → 相同 key 去同一 partition → 该 key 的消息有序
  → key=null → round-robin (负载均衡, 无序)

Consumer: pull-based, offset 由 consumer 管理
```

## ISR (In-Sync Replica)

每个 partition 有 1 个 leader replica + N 个 follower replicas。只有 ISR 中的 replicas 被视为"已同步"：

```
Leader: producer writes → leader: append to log → followers: fetch from leader
  → follower acknowledges: I've replicated up to offset X
  → replica.lag.time.max.ms 内 follower 没确认 → 从 ISR 移除
  → min.insync.replicas: 每条消息至少需这么多 ISR 副本确认才算 committed
```

如果 leader crash，新的 leader 从 ISR 中的 follower 选举。不在 ISR 中的 follower 有截断风险（log diverged from leader）。

## Consumer Group

一个 topic 的 partition 只能被同一 consumer group 中的**一个** consumer 消费。partition 在 group members 之间分配。

<svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Consumer Group 分区分配与 crash 后的 rebalance">
  <defs><marker id="kfarr" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="320" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Consumer Group:一个 partition 只能被组内 1 个 consumer 消费</text>
  <rect x="190" y="46" width="340" height="34" rx="8" fill="#4f46e5"/>
  <text x="360" y="68" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Topic:3 个 partition</text>
  <line x1="250" y1="80" x2="120" y2="122" stroke="#475569" stroke-width="1.6" marker-end="url(#kfarr)"/>
  <line x1="360" y1="80" x2="360" y2="122" stroke="#475569" stroke-width="1.6" marker-end="url(#kfarr)"/>
  <line x1="470" y1="80" x2="600" y2="122" stroke="#475569" stroke-width="1.6" marker-end="url(#kfarr)"/>
  <text x="172" y="98" text-anchor="middle" font-size="10" fill="#64748b">P0</text>
  <text x="378" y="98" text-anchor="middle" font-size="10" fill="#64748b">P1</text>
  <text x="548" y="98" text-anchor="middle" font-size="10" fill="#64748b">P2</text>
  <rect x="30" y="124" width="180" height="58" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="120" y="150" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">Consumer 1</text>
  <text x="120" y="170" text-anchor="middle" font-size="11" fill="#0f766e">消费 P0</text>
  <rect x="270" y="124" width="180" height="58" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="150" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">Consumer 2</text>
  <text x="360" y="170" text-anchor="middle" font-size="11" fill="#0f766e">消费 P1</text>
  <rect x="510" y="124" width="180" height="58" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="600" y="150" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">Consumer 3</text>
  <text x="600" y="170" text-anchor="middle" font-size="11" fill="#0f766e">消费 P2</text>
  <text x="360" y="200" text-anchor="middle" font-size="11" fill="#64748b">每个 partition 只由 1 个 consumer 消费 → 该 partition 内消息顺序有保证</text>
  <rect x="110" y="218" width="220" height="44" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="220" y="238" text-anchor="middle" font-size="12" font-weight="700" fill="#9a3412">Consumer 2 crash</text>
  <text x="220" y="254" text-anchor="middle" font-size="11" fill="#c2410c">触发 rebalance</text>
  <line x1="330" y1="240" x2="386" y2="240" stroke="#475569" stroke-width="1.6" marker-end="url(#kfarr)"/>
  <rect x="390" y="218" width="220" height="44" rx="8" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="500" y="238" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">分配给 Consumer 1 or 3</text>
  <text x="500" y="254" text-anchor="middle" font-size="11" fill="#0f766e">接管 P2</text>
  <rect x="30" y="278" width="660" height="34" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="299" text-anchor="middle" font-size="12" fill="#3730a3">rebalance 只重新分配 partition 归属,同一时刻每个 partition 仍只被 1 个 consumer 消费,顺序保证不受影响</text>
</svg>

Consumer group 保证了**顺序**：每个 partition 只有 1 个 consumer → consumer 看到的该 partition 的消息是顺序的。

### Offset Commit

Consumer 自己管理 offset——不依赖 broker push。通常 offset 存储在 Kafka 内部的 `__consumer_offsets` topic 中：

```
Consumer: poll() → 获取 N 条消息 → process → commitSync(offsets)
  → enable.auto.commit=false: 手动 commit (推荐, after successful processing)
  → enable.auto.commit=true: 定期自动 commit (可能丢消息)
```

## Log Compaction

基于 key 保留 last value，而非基于时间保留：

```
Log before compaction:
  key=A, value=X
  key=B, value=Y
  key=A, value=Z    ← key A 的最新值

Log after compaction:
  key=B, value=Y
  key=A, value=Z

→ 适合: KTable (更新流, 最新状态)
→ 不适合: KStream (事件流, 每次变更都是独立事件)
```

## 参考

- **Kafka**: kafka.apache.org/documentation/#design
- **ISR**: kafka.apache.org/documentation/#replication

*Keywords: Kafka, partition, ISR, consumer group, rebalance, offset commit, log compaction*
