Kafka 高吞吐原理:顺序写 + 零拷贝 + 批量处理 + 压缩
Kafka 为什么能跑出百万级 QPS?
有人说是因为用 Java 写的性能好,有人说是因为用了 Zookeeper。这些都不是关键。
Kafka 高吞吐的秘密,藏在四个核心设计上:顺序写、零拷贝、批量处理、压缩。
一、顺序写:磁盘最快读法
这是 Kafka 吞吐量高的最重要原因。
机械磁盘的真相
很多人以为磁盘很慢,这是对随机写的刻板印象。实际上,顺序写的磁盘 IO 速度可以接近内存。
随机写:
写位置1 → 磁头移动 → 写位置2 → 磁头移动 → 写位置3 ...
每次写入都要寻道 + 旋转延迟,IOPS 最多几百
顺序写:
持续追加写 → 磁头不用移动 → 旋转延迟极小
顺序写速度可达 500MB/s ~ 1GB/sKafka 的日志追加
Kafka 的 Topic 每个 Partition 对应一个日志文件,所有消息追加到文件末尾:
java
// Kafka 日志追加示意
public class LogSegment {
private final File file;
private final RandomAccessFile raf;
public void append(ByteBuffer buffer) {
// 追加写入,不会有任何随机 IO
// 文件指针一直向后移动
raf.seek(file.length()); // 定位到文件末尾
raf.write(buffer.array());
}
}为什么不用索引加快读取?
Kafka 的设计假设是:Consumer 按时间顺序消费,数据只会越读越多。这决定了顺序追加是最优策略。
二、零拷贝:减少数据复制
传统网络传输数据需要 4 次拷贝:
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 磁盘 │ →拷贝→│ 内核 │ →拷贝→│ 用户 │ →拷贝→│ Socket│
│ │ │ 缓冲区 │ │ 空间 │ │ 缓冲区 │
└────────┘ └────────┘ └────────┘ └────────┘
第1次拷贝 第2次拷贝 第3次拷贝
──────────────────────
Socket 发送时还要从内核复制到网卡Kafka 的零拷贝实现
Kafka 使用 transferTo() 方法,利用 DMA(Direct Memory Access) 直接在磁盘和网络之间传输数据:
java
// 传统方式:4 次拷贝
FileInputStream fis = new FileInputStream("data.txt");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
while (fis.read(buffer) != -1) {
baos.write(buffer); // 第1次:磁盘→内核,第2次:内核→用户
}
socket.getOutputStream().write(baos.toByteArray());
// 第3次:用户→内核,第4次:内核→网卡
// Kafka 零拷贝:2 次拷贝
FileChannel channel = FileChannel.open(path);
SocketChannel socketChannel = SocketChannel.open();
// transferTo 使用 DMA,磁盘直接→网卡
channel.transferTo(0, fileSize, socketChannel);
// 只剩:磁盘→内核缓冲区→网卡零拷贝的代价:
传统方式:
磁盘 → 内核缓冲区 → 用户空间 → Socket 缓冲区 → 网卡
1次拷贝 2次拷贝 3次拷贝 4次拷贝
Kafka 零拷贝:
磁盘 → 内核缓冲区 ──────────────────────→ 网卡
1次拷贝 2次拷贝(DMA)三、批量处理:合并小 IO
生产者批量发送
Kafka Producer 不会每条消息都发一次,而是先在客户端缓存:
java
// Producer 配置
properties.put("bootstrap.servers", "localhost:9092");
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 关键参数:批量发送
properties.put("batch.size", 16384); // 每批次最大 16KB
properties.put("linger.ms", 10); // 等待时间,0~100ms
properties.put("buffer.memory", 33554432); // 32MB 缓冲区
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
// 消息进入缓冲区,不立即发送
ProducerRecord<String, String> record = new ProducerRecord<>("topic", "key", "value");
producer.send(record); // 批量发送批量发送的原理:
消息1 → 缓冲区(积累)
消息2 → 缓冲区(积累)
消息3 → 缓冲区(积累)
...等待 linger.ms 或 batch.size 满了...
[消息1|消息2|消息3|...] → 一次网络 IO 发送消费者批量拉取
Consumer 也是批量拉取,不是一条一条取:
java
// Consumer 配置
properties.put("bootstrap.servers", "localhost:9092");
properties.put("group.id", "test-group");
properties.put("fetch.min.bytes", 1024); // 最小拉取字节数
properties.put("fetch.max.wait.ms", 500); // 最多等待时间
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(Arrays.asList("topic"));
while (true) {
// 一次拉取多条消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
process(record);
}
}四、消息压缩:节省带宽
Kafka 支持多种压缩算法,在 CPU 和带宽之间做权衡:
| 压缩算法 | 压缩比 | CPU 开销 | 适用场景 |
|---|---|---|---|
| gzip | 高 | 高 | 带宽紧张 |
| snappy | 中 | 中 | 平衡场景 |
| lz4 | 中低 | 低 | 高吞吐优先 |
| zstd | 高 | 中 | 新算法,兼顾两者 |
java
// Producer 配置压缩
properties.put("compression.type", "lz4"); // 启用压缩
// 压缩发生在批量发送时
[msg1|msg2|msg3|...] → compress → [压缩数据] → 发送为什么压缩在批量时做更高效?
- 批量数据有大量重复字段(消息头、Key)
- 压缩算法对重复模式压缩率更高
- 批量压缩比分批压缩效率更高
五、综合效果:Kafka 网络分层
Kafka 高吞吐网络架构
┌────────────────────────────────────────────────────────────────┐
│ Producer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Interceptor │ → │ Serializer │ → │ Partitioner│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │
│ └──────────┌──────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ RecordAccumulator │ ← 批量缓冲 │
│ │ (32MB) │ │
│ └──────────┬──────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ Sender Thread │ ← 后台发送线程 │
│ │ (批量 + 压缩 + 零拷贝) │ │
│ └──────────┬──────────┘ │
└────────────────────┼────────────────────────────────────────────┘
↓
┌────────────────────────┐
│ Linux Kernel Space │
│ ┌──────────────────┐ │
│ │ Socket Buffer │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ Page Cache │ ← 操作系统缓存 │
│ └──────────────────┘ │
└───────────┬────────────┘
↓
┌────────────────────────┐
│ Disk (顺序写) │
└────────────────────────┘六、性能数据对比
| 操作 | 传统方式 | Kafka 优化后 |
|---|---|---|
| 写入磁盘 | 随机 IO,~200 IOPS | 顺序写,~30万 IOPS |
| 网络传输 | 4次拷贝 | 零拷贝,2次拷贝 |
| 单次发送 | 1条消息 | 批量发送 N 条 |
| 带宽利用 | 低 | 高(压缩) |
Benchmark 测试结果(3 台机器,12 块盘):
| 指标 | 数值 |
|---|---|
| Producer QPS | 170 万/秒 |
| Consumer QPS | 250 万/秒 |
| 端到端延迟 | < 5ms |
| 磁盘利用率 | > 80% |
总结
Kafka 高吞吐的四板斧:
| 优化点 | 原理 | 效果 |
|---|---|---|
| 顺序写 | append-only 日志,避免随机 IO | 磁盘 IOPS 提升 1000 倍 |
| 零拷贝 | DMA 直接传输,绕过用户空间 | CPU 拷贝减少 50% |
| 批量处理 | 合并小 IO,减少网络往返 | 网络效率提升 10 倍 |
| 压缩 | 批量压缩,算法优化 | 带宽节省 50%~80% |
理解这四个原理,你就能明白为什么 Kafka 能用普通硬件支撑百万 QPS。
留给你的问题
假设你在做一个实时数据管道,从 MySQL 同步数据到 Elasticsearch:
- 如果不用消息队列,直接用 Canal 同步,数据量大了会出什么问题?
- 如果用 Kafka 同步,批量大小(batch.size)应该设为多少?太大了有什么风险?
- 如果 Kafka Broker 的 Page Cache 满了,新写入的消息会怎样?应该怎么配置?
- 压缩算法 lz4 和 zstd 各有什么优缺点,什么时候用 zstd 更合适?
思考这些问题,能帮助你更好地调优 Kafka 生产环境。
