← 返回文章列表

Kafka 核心指南:从原理到实践

深入理解 Kafka 的核心概念、架构设计、持久化机制和消费模式,通过电商场景全面解析消息队列的工作原理。

24 分钟阅读
字号

Kafka 是 Apache 基金会旗下的分布式事件流平台,被广泛应用于日志收集、实时数据处理、消息队列等场景。本文将系统讲解 Kafka 的核心概念和原理。

一、Kafka 是什么

1.1 基本定义

Kafka 官网定义:Kafka 是一个事件流平台(Event Streaming Platform)。

通俗理解:Kafka 是一个高效、可靠的消息中间件,用于在不同系统之间传递消息。

1.2 数据库 vs 消息中间件

数据库Kafka
存储内容软件的状态(用户信息、订单、课程)任务协同的通知(事件、消息)
使用方式增删改查,长期保存发消息、收消息,消费即删除(或过期删除)
类比仓库(长期存货)快递柜(临时存包裹)

核心区别

  • 数据库存储的是状态
  • Kafka 存储的是事件,事件是通知机制,用于触发后续流程

1.3 为什么要用 Kafka

在分布式高并发系统中,组件之间的交互本质上就是生产者-消费者模型

生产者 (Producer) ────→ [消息队列] ←─── 消费者 (Consumer)

四大优点

优点说明
解耦合生产者和消费者不直接对话,通过队列中转,各自改代码互不影响
并行处理生产者和消费者同时工作,不用互相等待,效率大幅提升
削峰填谷峰值流量先存队列里,后面慢慢处理,系统不崩溃
灵活扩缩流量大了加消费者,不用改代码,直接加机器

典型场景

  • 电商下单:订单服务下单后,库存、支付、物流服务异步处理
  • 日志收集:各服务产生日志,发到 Kafka,统一采集存储分析
  • 实时处理:用户行为数据实时分析,实时推荐

二、Kafka 核心概念

2.1 Broker

定义:Broker = Kafka 集群中的一个节点(一台服务器或一个 Kafka 进程)。

Kafka 集群 = 多台 Broker 组成
 
服务器1 → 装了 Kafka → 它就是 Broker 0
服务器2 → 装了 Kafka → 它就是 Broker 1
服务器3 → 装了 Kafka → 它就是 Broker 2

在容器环境下

  • 一个 Kafka Pod = 一个 Broker
  • 一个 Kafka Docker 容器 = 一个 Broker

2.2 Topic(主题)

定义:Topic 是消息的逻辑分类,相当于"图书专线"、"服装专线"传送带。

Kafka 集群可以创建多个 Topic:
 
Topic: order_created    → 订单创建消息
Topic: order_payed      → 订单支付消息
Topic: system_logs       → 系统日志

注意:Topic 是逻辑概念,底层实际存储靠的是 Partition。

2.3 Partition(分区)

定义:Partition 是 Topic 的物理分片,把数据分成多份,并行处理。

Topic: order_created(有 3 个 Partition)
 
┌─────────────────────────────────────────────────────────────┐
│  Partition 0 │ 订单A │ 订单D │ 订单G │ ...                │
├─────────────────────────────────────────────────────────────┤
│  Partition 1 │ 订单B │ 订单E │ 订单H │ ...                │
├─────────────────────────────────────────────────────────────┤
│  Partition 2 │ 订单C │ 订单F │ 订单I │ ...                │
└─────────────────────────────────────────────────────────────┘

为什么需要 Partition

场景单 Partition多 Partition
10 万条订单1 个线程处理,很慢3 个线程并行,快 3 倍
机器挂了数据全丢只有部分数据受影响

一句话总结:Partition = 把一个大文件切成多份,多线程并行读写。

2.4 Offset(偏移量)

定义:Offset 是消息在 Partition 内的唯一递增序号,从 0 开始。

Partition 0:
 
[订单A][订单B][订单C][订单D][订单E][订单F]...
   0      1      2      3      4      5   ← Offset

                          已消费到 Offset=4
                          下次从 Offset=5 开始

特点

  • 每个 Partition 内的 Offset 独立递增
  • Offset 永不重复
  • 消费者通过 Offset 记录消费进度

2.5 Replica(副本)

定义:每个 Partition 可以有多个副本,实现高可用。

Partition 0 有 3 个副本:
 
┌─────────────────────────────────────────────────────────────┐
│  Broker 0 (Partition 0 Leader)     ← 读写都找他           │
│  Broker 1 (Partition 0 Follower)   ← 实时同步主本数据     │
│  Broker 2 (Partition 0 Follower)   ← 实时同步主本数据     │
└─────────────────────────────────────────────────────────────┘

两种角色

角色作用
Leader主副本,所有读写都找他
Follower从副本,实时从 Leader 同步数据,只做备份

故障转移

  • Leader 挂了 → Kafka 从 Follower 中选举新 Leader
  • 数据不丢失,服务继续

副本分配规则

  • 同一 Partition 的多个副本分布在不同 Broker 上
  • 副本数 ≤ Broker 数

2.6 Producer(生产者)

定义:Producer 是往 Kafka 发消息的客户端。

订单服务(Producer)发送消息:
 
订单A → Kafka → Partition 0 → Broker 0 (Leader)
订单B → Kafka → Partition 1 → Broker 1 (Leader)
订单C → Kafka → Partition 2 → Broker 2 (Leader)

分区策略(消息发到哪个 Partition):

策略规则场景
指定 Partition明确指定需要保证顺序的场景
按 Key 哈希hash(key) % partition_num相同 Key 的消息去同一 Partition
轮询依次发到 Partition 0, 1, 2均匀分布

ACK 机制(可靠性级别):

配置行为速度可靠性
acks=0发完就不管了最快可能丢数据
acks=1Leader 写入即可较快可能丢(Follower 未同步)
acks=all所有 ISR 副本确认最慢最高

2.7 Consumer(消费者)

定义:Consumer 是从 Kafka 拉取消息的客户端。

Kafka Topic
 
→ [订单A][订单B][订单C][订单D]... → 消费者组

消费者组(Consumer Group)

消费组 order_group 有 3 个消费者:
 
消费者1 → 消费 Partition 0
消费者2 → 消费 Partition 1
消费者3 → 消费 Partition 2
 
规则:
- 同一消费组内,一个 Partition 只被一个 Consumer 消费
- 不同消费组可以同时消费同一消息(发布订阅模式)

Offset 提交

消费者记录消费到哪了:
 
上次消费到 Offset=5
下次从 Offset=6 开始

提交方式:

  • 自动提交:默认每 5 秒提交一次
  • 手动提交:处理完一条提交一条

2.8 ZooKeeper / KRaft

作用:管理 Kafka 集群元数据、Broker 注册、Leader 选举。

模式说明
ZooKeeper(旧版)依赖外部 ZooKeeper 集群
KRaft(Kafka 3.x+)用 Kafka 自身实现 Raft 协议,无需外部依赖

三、架构详解

3.1 完整架构图

                                    Kafka 集群
                    ┌─────────────────────────────────────────────────────────────┐
                    │                                                              │
                    │   Broker 0               Broker 1               Broker 2      │
                    │  (192.168.1.10)         (192.168.1.11)         (192.168.1.12)│
                    │                                                              │
                    │   ┌─────────────┐        ┌─────────────┐        ┌─────────────┐│
                    │   │            │        │            │        │            ││
                    │   │ Partition 0│        │ Partition 1│        │ Partition 2││
                    │   │ [订单A..]  │        │ [订单B..]  │        │ [订单C..]  ││
                    │   │  Leader    │        │  Leader    │        │  Leader    ││
                    │   │            │        │            │        │            ││
                    │   └─────────────┘        └─────────────┘        └─────────────┘│
                    │          │                      │                      │        │
                    │          │ 同步                  │ 同步                  │ 同步  │
                    │          ▼                      ▼                      ▼        │
                    │   ┌─────────────┐        ┌─────────────┐        ┌─────────────┐│
                    │   │ Partition 1 │        │ Partition 2 │        │ Partition 0 ││
                    │   │ (副本)       │        │ (副本)       │        │ (副本)       ││
                    │   └─────────────┘        └─────────────┘        └─────────────┘│
                    │          │                      │                      │        │
                    │          │ 同步                  │ 同步                  │ 同步  │
                    │          ▼                      ▼                      ▼        │
                    │   ┌─────────────┐        ┌─────────────┐        ┌─────────────┐│
                    │   │ Partition 2 │        │ Partition 0 │        │ Partition 1 ││
                    │   │ (副本)       │        │ (副本)       │        │ (副本)       ││
                    │   └─────────────┘        └─────────────┘        └─────────────┘│
                    │                                                              │
                    └──────────────────────────────────────────────────────────────┘
 


     ┌─────────────────────┴──────────────────────────────┐
     │            订单服务 (Producer)                       │
     │                                                       │
     │  订单A (Key=user_10086, hash=0) → Partition 0 → Broker 0 │
     │  订单B (Key=user_10087, hash=1) → Partition 1 → Broker 1 │
     │  订单C (Key=user_10088, hash=2) → Partition 2 → Broker 2 │
     └────────────────────────────────────────────────────────┘
 
                    库存服务、支付服务、物流服务
                    各自作为独立消费组,消费所有 Partition

3.2 副本分配示例

3 个 Broker + 3 个 Partition + 副本数 3:
 
Broker 0 存:
  - Partition 0 (Leader)
  - Partition 1 (Follower)
  - Partition 2 (Follower)
 
Broker 1 存:
  - Partition 0 (Follower)
  - Partition 1 (Leader)
  - Partition 2 (Follower)
 
Broker 2 存:
  - Partition 0 (Follower)
  - Partition 1 (Follower)
  - Partition 2 (Leader)

四、持久化机制

4.1 写入流程

Producer → Broker (Leader) → Page Cache → 磁盘

         Follower 同步(ISR 机制)

步骤详解

1. Producer 发送消息到 Leader Partition 所在 Broker
 
2. Broker 接收消息,先写入 Page Cache(内存缓冲区)
 
3. 异步刷盘:操作系统定期将 Page Cache 刷到磁盘
 
4. 写入日志段:消息追加到 .log 文件末尾
 
5. Follower 从 Leader 拉取数据,保持数据冗余

4.2 Log Segment(日志段)

为什么需要 Log Segment?

假设一个 Partition 存了1年数据:
  - 文件巨大(可能几TB)
  - 查找一条消息要扫描整个文件
  - 删掉过期数据要在巨大文件里操作
 
Kafka 解决方案:按大小或时间切分成多个 Segment(片段)

触发条件

满足任一条件即创建新 Segment:
  1. 大小达到 log.segment.bytes = 1GB(默认)
  2. 时间达到 log.roll.hours = 168小时(默认7天)

物理存储结构

/data/
└── order_created-0/           ← Partition 0 的文件夹
    ├── 00000000000000000000.log      ← Segment 1 数据文件
    ├── 00000000000000000000.index    ← Segment 1 偏移量索引
    ├── 00000000000000000000.timeindex ← Segment 1 时间索引
    ├── 00000000000012345678.log      ← Segment 2 数据文件
    ├── 00000000000012345678.index    ← Segment 2 偏移量索引
    └── 00000000000012345678.timeindex ← Segment 2 时间索引

文件名规则

文件名 = 当前 Segment 第一条消息的 Offset
 
00000000000000000000.log  → 第一条消息 Offset = 0
00000000000012345678.log  → 第一条消息 Offset = 12345678
00000000000023456789.log  → 第一条消息 Offset = 23456789
 
这样可以根据文件名快速确定查找范围

.log 文件(消息如何存储)

每条消息在 .log 文件中存储格式:
 
┌──────────────────────────────────────────────────────────────┐
│ Offset │ Timestamp │ Key │ Value │ Headers │ CRC           │
└──────────────────────────────────────────────────────────────┘
字段说明
Offset消息在 Partition 内的序号(递增)
Timestamp消息创建时间
Key用来决定发到哪个 Partition(可为空)
Value实际的消息内容
Headers消息头元数据
CRC校验码,检测数据是否损坏

追加写入:消息顺序追加到 .log 末尾,不修改已有数据。这种顺序写入速度接近内存。

4.3 稀疏索引

为什么需要稀疏索引?

方案A(全量索引):每条消息都建索引
  → 索引文件 = 数据文件大小
  → 全部加载不到内存
  → 不可行
 
方案B(稀疏索引):每隔 4KB 数据建一条索引
  → 索引文件只有数据文件的 1/100
  → 可以全部放内存
  → 可行!

稀疏索引原理

.log 文件内容:
 
  字节 0-4095:       [消息0-99](100条消息,占4KB)
  字节 4096-8191:    [消息100-199](100条消息) ← 索引点
  字节 8192-12287:   [消息200-299](100条消息) ← 索引点
  字节 12288-16383:  [消息300-399](100条消息) ← 索引点
.index 文件内容(稀疏索引):
 
  (200, 4096)    → Offset 200 的消息在 .log 的 4096 字节处
  (300, 8192)    → Offset 300 的消息在 .log 的 8192 字节处
  (400, 12288)   → Offset 400 的消息在 .log 的 12288 字节处
 
  每隔 4KB 数据,只建 1 条索引
  索引记录:(Offset号, .log中的物理字节位置)

查找 Offset=368801 的完整过程

步骤1:确定在哪个 Segment
  → 根据文件名快速定位(文件名是起始 Offset)
  → 假设在 Segment 2(起始 Offset = 368000)
 
步骤2:在 .index 中二分查找
  → 找"最大 ≤ 368801"的索引
  → 找到 (368800, 1048576)
  → 说明 Offset 368800 的消息在 .log 的 1048576 字节位置
 
步骤3:从 1048576 字节位置往后顺序查找
  → 往后读几条消息
  → 很快找到 Offset = 368801 的消息

图解稀疏索引

                        .log 文件(数据)
┌────────────┬────────────┬────────────┬────────────┐
│  消息0-99  │消息100-199 │消息200-299 │消息300-399 │  ...
│  0-4KB    │  4KB-8KB   │ 8KB-12KB  │ 12KB-16KB  │
└────────────┴─────┬──────┴─────┬──────┴────────────┘
                   │            │
                   │ 索引点     │ 索引点
                   ▼            ▼
              .index 文件(稀疏索引)
        ┌────────────────┬────────────────┐
        │ (200, 4096)    │ (300, 8192)    │  ...
        └────────────────┴────────────────┘
 
查找 Offset=250:
  1. 二分找到最大≤250的索引 → (200, 4096)
  2. 去 .log 第 4096 字节位置
  3. 顺序读,很快找到 250

为什么快?

1. 索引文件极小(只有数据文件的 1/100),可全部放内存
2. 二分查找 .index → O(log n),比如 log2(1000) = 10
3. 定位到大概位置后,只需顺序读几 KB
4. 核心思想:缩小查找范围

4.4 .timeindex 文件(时间索引)

作用

.timeindex 用于按时间戳查找消息,而不是按 Offset。

典型场景consumer.seek(timestamp=1625097600000)

存储结构

.timeindex 文件内容:
 
  (时间戳T1, Offset 0)
  (时间戳T2, Offset 1000)
  (时间戳T3, Offset 2000)
  (时间戳T4, Offset 3000)
 
每条记录表示:在这个时间戳之后,消息从哪个 Offset 开始

按时间查找的过程

场景:用户想从 timestamp=1625097700000 开始消费
 
步骤1:在 .timeindex 中二分查找
  → 找到"最大 ≤ 1625097700000"的时间戳 → (T2, Offset 1000)
 
步骤2:用找到的 Offset=1000 去 .index 查找物理位置
  → .index 找到 (1000, xxx)
 
步骤3:去 .log 的物理位置开始消费

4.5 三者配合查找

┌─────────────────────────────────────────────────────────────────┐
│                     Kafka 消息查找三种方式                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 按 Offset 查找:                                            │
│     .index → 找到物理位置 → .log 读取                          │
│                                                                 │
│  2. 按时间查找:                                                │
│     .timeindex → 找到 Offset → .index → 找到物理位置 → .log    │
│                                                                 │
│  3. 从头消费:                                                  │
│     从第一个 Segment 开始                                        │
│     → 按 Offset 顺序读 .log                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.6 Log Segment 完整结构图

┌─────────────────────────────────────────────────────────────────┐
│                     Partition 0 目录结构                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  /data/order_created-0/                                        │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Segment 1: 00000000000000000000                        │   │
│  │  起始 Offset = 0                                       │   │
│  │  ├── .log      → 实际消息数据                         │   │
│  │  │             [消息0][消息1][消息2]...               │   │
│  │  ├── .index    → 稀疏索引(Offset→物理位置)         │   │
│  │  │             (200, 4096)(300, 8192)...              │   │
│  │  └── .timeindex → 时间索引(timestamp→Offset)        │   │
│  │                  (T1, 0)(T2, 1000)...                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                            ↓ 1GB 或 7天后创建新 Segment          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Segment 2: 00000000000001234567                        │   │
│  │  起始 Offset = 1234567                                │   │
│  │  ├── .log                                             │   │
│  │  ├── .index                                           │   │
│  │  └── .timeindex                                       │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.7 为什么 Kafka 这么快?

┌─────────────────────────────────────────────────────────────────┐
│                        Kafka 高性能原因                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 顺序写入 .log:                                            │
│     → 磁盘磁头不需要来回移动                                    │
│     → 写入速度接近内存                                         │
│                                                                 │
│  2. 稀疏索引:                                                │
│     → 索引文件极小,可全部放内存                                │
│     → 查找时先二分索引定位,再顺序读几 KB                       │
│     → 大量减少磁盘 IO                                          │
│                                                                 │
│  3. 时间索引独立:                                            │
│     → 按时间查找不需要扫描全量数据                              │
│     → 二分 + 稀疏索引快速定位                                  │
│                                                                 │
│  4. 批量处理:                                                │
│     → Producer 和 Consumer 都支持批量操作                      │
│     → 减少网络往返次数                                         │
│                                                                 │
│  5. 零拷贝:                                                  │
│     → 使用 sendfile() 系统调用                                │
│     → 数据从磁盘直接到网卡,不经过用户态                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

五、消费模式

5.1 Pull vs Push

模式优点缺点适用场景
Pull(拉取)消费者按能力拉取,避免过载实时性稍差Kafka 采用这个
Push(推送)实时推送消费不过来会崩溃消费能力远大于生产

Kafka 采用 Pull 模式:消费者主动从 Broker 拉取消息,按自己能力处理。

5.2 点对点 vs 发布订阅

模式特点Kafka 实现
点对点一条消息只能被一个消费者处理同一消费组内,Partition 只能被一个 Consumer 消费
发布订阅一条消息可被所有消费者处理不同消费组可以同时消费同一消息

Kafka 同时支持两种模式

5.3 消费者组内分工

Topic 有 5 个 Partition,消费组有 3 个消费者:
 
消费者1 → Partition 0, 1
消费者2 → Partition 2, 3
消费者3 → Partition 4
 
原则:一个 Partition 只能被同一个组内的一个消费者消费

六、电商场景实战

6.1 场景描述

用户在天猫下单购买手机,全流程异步处理:

用户下单 → 库存扣减 → 支付扣款 → 物流发货 → 短信通知

6.2 系统架构

                         Kafka 集群
                        ┌──────────────────────────────┐
                        │  Topic: order_created         │
                        │  Partition 0, 1, 2           │
                        └──────────────────────────────┘

        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
        ↓                           ↓                           ↓
┌───────────────┐          ┌───────────────┐          ┌───────────────┐
│   库存服务     │          │   支付服务     │          │   物流服务     │
│ inventory_grp │          │ payment_grp  │          │ shipping_grp │
└───────────────┘          └───────────────┘          └───────────────┘
        │                           │                           │
        ↓                           ↓                           ↓
    扣减库存                   调起支付                  生成快递单

6.3 完整流程

14:00:00 用户下单
  → 订单服务发送消息到 order_created
  → 用户看到"下单成功"(延迟 50ms)
  → 订单已存入 Kafka
 
14:00:01 库存服务消费 order_created
  → 扣减库存
 
14:00:02 支付服务消费 order_created
  → 调起支付
  → 用户完成支付
 
14:00:10 物流服务消费 order_payed(订单支付成功的消息)
  → 生成快递单
  → 用户看到"已发货"
 
14:00:11 短信服务消费 order_shipped
  → 发送发货短信

6.4 削峰填谷示例

双十一秒杀:10 万人同时下单
 
不加 Kafka:
  → 数据库同时收到 10 万请求
  → 数据库连接数爆满
  → 系统崩溃
 
加 Kafka:
  → 10 万条消息快速写入 Kafka
  → 返回"下单成功"
  → 后台 Consumer 按每秒 1000 单处理
  → 10 万单需要 100 秒处理完
  → 数据库稳稳的

七、部署方式

7.1 各场景选择

场景推荐方式原因
本地开发/测试Docker Compose快速启动
生产环境Kubernetes + Strimzi自动化运维、故障恢复
无 K8s 环境直接安装物理机性能好
学习原理Docker 单机模拟多 Broker

7.2 Docker Compose 快速部署

version: '3'
services:
  zookeeper:
    image: wurstmeister/zookeeper
    ports:
      - "2181:2181"
 
  kafka:
    image: wurstmeister/kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_HOST_NAME: localhost
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
docker-compose up -d

7.3 Kubernetes 部署(Strimzi)

apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
  name: my-kafka
spec:
  kafka:
    replicas: 3
    storage:
      type: persistent-claim
      size: 100Gi
    config:
      offsets.topic.replication.factor: 3
      transaction.state.log.replication.factor: 3
  zookeeper:
    replicas: 3

八、常见面试问题

Q1: Kafka 怎么保证消息不丢?

生产端:acks=all(所有 ISR 副本确认)
存储端:副本数 ≥ 2,不跨机房
消费端:手动提交 Offset,处理完再提交

Q2: Kafka 怎么保证消息顺序?

同一 Partition 内有序(Offset 递增)
全局有序:只能用单 Partition(失去并行能力)
同 Key 有序:按 Key 哈希到同一 Partition

Q3: Consumer 挂了怎么办?

其他 Consumer 触发 Rebalance,重新分配 Partition
之前没处理完的消息会被重新消费
需要业务方做幂等处理

Q4: Broker 和 Partition 的关系?

Broker = 一台服务器
Partition = 数据分片
 
3 个 Broker + 3 个 Partition:
→ 每台服务器存 1/3 数据
→ 3 个 Partition 并行读写,快 3 倍

九、总结

┌─────────────────────────────────────────────────────────────────┐
│                        Kafka 核心概念                            │
├─────────────────────────────────────────────────────────────────┤
│  Broker     │ 一台服务器(Kafka 节点)                           │
│  Topic      │ 消息的逻辑分类                                     │
│  Partition  │ 数据分片,并行处理                                 │
│  Replica    │ 数据备份,高可用                                   │
│  Leader     │ 主副本,读写找他                                   │
│  Follower   │ 从副本,实时同步                                   │
│  Producer   │ 生产者,发消息                                     │
│  Consumer   │ 消费者,收消息                                     │
│  Offset     │ 消息在 Partition 内的位置编号                       │
└─────────────────────────────────────────────────────────────────┘

Kafka 的核心优势

  • 高吞吐:顺序写 + 批量处理 + 零拷贝
  • 高可用:副本机制 + 故障自动转移
  • 灵活扩展:Partition 分片 + 消费者组
  • 持久化:磁盘存储 + 稀疏索引快速查找

掌握这些核心概念,就能更好地理解和使用 Kafka 了。

分享

// RELATED_POSTS

0%