Apache Kafka 指南
前言:为什么你需要这份文档?
作为 Java 开发者,你可能已经会用 @KafkaListener,但当面对以下问题时是否依然困惑:
-
❓ Kafka 为什么能扛住百万 TPS?底层存储如何设计?
-
❓ Consumer 扩容后,为什么所有服务突然停止消费?
-
❓ 如何实现“消费 → 处理 → 再生产”的端到端 Exactly-Once?
-
❓ 线上积压百万消息,该看哪些监控指标?
一、Kafka 核心原理:不只是队列,而是高性能日志引擎
1、分区(Partition):并行处理的基石
graph TB
T[Topic: user-events] --> P0[Partition 0]
T --> P1[Partition 1]
T --> P2[Partition 2]
P0 --> S0["segment-0.log\noffset 0-999"]
P0 --> S1["segment-1.log\noffset 1000-1999"]
P1 --> S2["segment-0.log\noffset 0-888"]
-
每个 Partition 是一个有序、不可变的消息日志
-
日志被切分为多个 Segment 文件(默认 1GB)
-
Producer 通过 key 决定消息路由:
// 相同 key → 相同 Partition → 顺序保证
kafkaTemplate.send("topic", userId, event);
2、存储引擎:.log + .index 如何协同工作?
Kafka 内部有个计数器,每当新写入的消息使 Segment 的总大小增加 ≥ 4096 字节(4KB)时,就为这条消息建一条索引。
📍 什么是 position?
position = 消息在 .log 文件中的物理字节偏移量(从文件开头算起)
📍核心思想:文件名 = Segment 的起始 offset
Kafka 为每个 Partition 创建多个 Segment 文件,文件名就是该 Segment 第一条消息的 offset。
Kafka 的高性能秘诀在于 .log 顺序 I/O + .index 稀疏索引:
graph LR
A[请求 offset=150] --> B{定位 Segment}
B --> C["在 .index 中<br/>二分查找"]
C --> D["找到最近索引<br/>offset=100 → position=4096"]
D --> E["从 .log 的<br/>position=4096 开始扫描"]
E --> F["找到 offset=150 的消息"]
graph LR
O[Offset 1500] --> I["Index File<br/>(每4KB一条)"]
I --> L["Log File<br/>(Position 6144)"]
L --> M[Message Content]
subgraph Segment Files
I
L
end
-
.log 文件:存储实际消息(顺序追加)
-
.index 文件:稀疏索引(
offset → position映射) -
查询流程:
-
二分查找 .index 定位最近 offset
-
顺序扫描 .log 找到目标消息
-
💡 优势:
-
写入:纯顺序 I/O(磁盘最快模式)
-
读取:O(log N) 定位 + 顺序扫描(避免随机读)
3、副本同步:ISR 机制保障高可用
graph LR
P[Partition] --> L[Leader]
P --> F1[Follower 1]
P --> F2[Follower 2]
L -->|同步| F1
L -->|同步| F2
style L fill:#b7eb8f
style F1 fill:#ffe58f
style F2 fill:#ffe58f
-
Leader:处理所有读写请求
-
Follower:从 Leader 拉取数据
-
ISR(In-Sync Replicas):与 Leader 保持同步的副本集合
⚠️ 关键配置:→ 配合 Producer acks=all,实现强一致性
二、消息有序性:原理、陷阱与生产方案
1、有序性的唯一保证:Partition 内有序
graph LR
K1[key=userA] --> P0[Partition 0]
K2[key=userB] --> P1[Partition 1]
K3[key=userA] --> P0
P0 --> M1["userA: login (offset 0)"]
P0 --> M2["userA: logout (offset 1)"]
P1 --> M3["userB: login (offset 0)"]
✅ 结论:
-
相同 key → 相同 Partition → 严格有序
-
不同 key → 可能不同 Partition → 全局无序
2、三大破坏顺序的陷阱与解决方案
陷阱 1:Producer 重试导致乱序
-
场景:网络抖动,msg1 超时重试,msg2 先写入
-
解决方案:启用幂等 Producer
spring:
kafka:
producer:
enable-idempotence: true # 自动分配 PID + 序列号
retries: 2147483647 # 无限重试(安全)
Broker 为每个 (PID, Partition) 维护一个高水位序列号:
-
如果收到的消息
seq <= 高水位→ 重复消息,丢弃 -
如果
seq == 高水位 + 1→ 新消息,接受并更新高水位
sequenceDiagram
participant P as Producer
participant B as Broker
P->>B: 发送 msg1 (PID=100, seq=1)
B-->>P: 成功
B->>B: 记录 (PID=100, seq=1)
P->>B: 发送 msg2 (PID=100, seq=2)
Note right of P: 网络超时,未收到响应
P->>B: 重试 msg2 (PID=100, seq=2)
B->>B: seq=2 == 1+1 → 接受
B-->>P: 成功
P->>B: 发送 msg3 (PID=100, seq=3)
B->>B: seq=3 == 2+1 → 接受
陷阱 2:多线程消费破坏顺序
-
场景:异步线程池处理消息
-
解决方案:单 Partition 单线程
spring:
kafka:
listener:
concurrency: 1 # 每个实例 1 个消费线程
陷阱 3:Consumer Rebalance 导致重复消费
-
场景:扩容 Consumer,触发 Rebalance
-
解决方案:延长处理超时 + 减少单次负载
spring:
kafka:
consumer:
max-poll-interval-ms: 300000 # 默认 5 分钟 → 延长至 5 分钟
listener:
max-poll-records: 50 # 默认 500 → 减少单次处理量
三、Consumer Rebalance:代价与规避(生产必读)
1、什么是 Rebalance?
当 Consumer Group 成员变化时,Kafka 会重新分配 Partition,期间所有消费者暂停消费(Stop The World)。
sequenceDiagram
Consumer1->>Kafka: 加入 Group
Kafka->>All Consumers: 触发 Rebalance
All Consumers->>All Consumers: 停止消费
Kafka->>Consumer1: 分配 Partition
Kafka->>Consumer2: 重新分配 Partition
All Consumers->>Kafka: 恢复消费
2、触发条件
-
新 Consumer 加入或退出
-
订阅的 Topic 变化
-
单次 poll() 处理超时(
max.poll.interval.ms)
3、规避方案
-
避免频繁启停 Consumer
-
优化业务逻辑(确保在
max.poll.interval.ms内完成) -
调整参数:
spring:
kafka:
consumer:
# 延长最大处理时间(单位:毫秒)
max-poll-interval-ms: 300000
listener:
# 减少单次拉取消息数
max-poll-records: 50
四、Exactly-Once 语义(EOS):端到端实现
1、场景:消费 → 处理 → 再生产
例如:
订单 Topic → 处理 → 发送到风控 Topic
2、实现方案:事务性消费
@Service
public class TransactionalConsumer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@KafkaListener(topics = "input-topic")
public void listen(List<ConsumerRecord<String, String>> records) {
// 在事务中执行:消费 + 处理 + 再生产
kafkaTemplate.executeInTransaction(t -> {
// 1. 处理业务
List<String> outputs = process(records);
// 2. 发送新消息(同一事务)
outputs.forEach(msg -> t.send("output-topic", msg));
// 3. 手动提交 offset(事务内)
return true;
});
}
private List<String> process(List<ConsumerRecord<String, String>> records) {
// 业务逻辑
return records.stream().map(r -> "processed: " + r.value()).collect(Collectors.toList());
}
}
3、必须配置
spring:
kafka:
producer:
transaction-id-prefix: tx- # 启用事务
consumer:
# 只读已提交消息(避免脏读)
properties:
isolation.level: read_committed
✅ 效果:
-
消息不会丢失
-
消息不会重复
-
消息不会乱序(同一 Partition 内)
五、生产监控:关键指标与告警阈值
1、必监控指标清单
| 指标 | 说明 | 告警阈值 | 工具 |
|---|---|---|---|
kafka.consumer.lag | 消费延迟(未消费消息数) | > 10,000 | Prometheus + Grafana |
request.handler.avg.idle.pct | Broker 繁忙度 | < 0.2 | JMX |
under_replicated_partitions | 副本不同步分区数 | > 0 | Kafka Manager |
record.error.rate | Producer 发送失败率 | > 0.01 | Micrometer |
2、Spring Boot 集成监控
management:
endpoints:
web:
exposure:
include: health,metrics
metrics:
tags:
application: ${spring.application.name}
访问 http://localhost:8080/actuator/metrics 查看 Kafka 指标。
六、技术选型:Kafka vs 其他消息系统
1、适用场景对比
| 系统 | 吞吐量 | 延迟 | 持久化 | 适用场景 |
|---|---|---|---|---|
| Kafka | ⭐⭐⭐⭐⭐ (10w+/s) | 毫秒级 | 磁盘(高性能) | 日志聚合、流处理、事件溯源 |
| RabbitMQ | ⭐⭐⭐ (万级) | 微秒级 | 内存+磁盘 | 任务队列、RPC、低延迟场景 |
| RocketMQ | ⭐⭐⭐⭐ (10w+) | 毫秒级 | 磁盘 | 金融级事务、顺序消息 |
| Pulsar | ⭐⭐⭐⭐ | 毫秒级 | 分层存储 | 多租户、跨地域复制 |
2、何时不选 Kafka?
-
需要点对点队列模型 → RabbitMQ
-
消息需 TTL 自动过期 → RocketMQ
-
强多租户隔离 → Pulsar
-
超低延迟(<1ms) → RabbitMQ
七、生产 Checklist:Java 开发必备配置
1、Producer 配置
spring:
kafka:
producer:
enable-idempotence: true
acks: all
retries: 2147483647
batch-size: 16384
linger-ms: 20
compression-type: snappy
2、Consumer 配置
spring:
kafka:
consumer:
enable-auto-commit: false
max-poll-records: 50
max-poll-interval-ms: 300000
properties:
isolation.level: read_committed
spring.json.trusted.packages: com.yourcompany.model
listener:
ack-mode: manual_immediate
concurrency: 3
3、业务代码规范
-
Key 必须设:用业务 ID(如 user_id)
-
手动 ACK:先处理,再提交
-
幂等处理:数据库唯一索引 / 状态机
-
异常捕获:不提交 offset,允许重试
八、总结:Kafka 的核心哲学
-
高性能:靠 顺序 I/O + 批量 + 压缩
-
高可靠:靠 ISR + 幂等 + 手动 ACK
-
高可用:靠 多副本 + 自动 Leader 选举
-
易扩展:靠 Partition 水平拆分