Skip to content

Kafka 高吞吐原理:顺序写 + 零拷贝 + 批量处理 + 压缩

Kafka 为什么能跑出百万级 QPS?

有人说是因为用 Java 写的性能好,有人说是因为用了 Zookeeper。这些都不是关键。

Kafka 高吞吐的秘密,藏在四个核心设计上:顺序写、零拷贝、批量处理、压缩

一、顺序写:磁盘最快读法

这是 Kafka 吞吐量高的最重要原因

机械磁盘的真相

很多人以为磁盘很慢,这是对随机写的刻板印象。实际上,顺序写的磁盘 IO 速度可以接近内存

随机写:
  写位置1 → 磁头移动 → 写位置2 → 磁头移动 → 写位置3 ...
  每次写入都要寻道 + 旋转延迟,IOPS 最多几百

顺序写:
  持续追加写 → 磁头不用移动 → 旋转延迟极小
  顺序写速度可达 500MB/s ~ 1GB/s

Kafka 的日志追加

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 → [压缩数据] → 发送

为什么压缩在批量时做更高效

  1. 批量数据有大量重复字段(消息头、Key)
  2. 压缩算法对重复模式压缩率更高
  3. 批量压缩比分批压缩效率更高

五、综合效果: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 QPS170 万/秒
Consumer QPS250 万/秒
端到端延迟< 5ms
磁盘利用率> 80%

总结

Kafka 高吞吐的四板斧:

优化点原理效果
顺序写append-only 日志,避免随机 IO磁盘 IOPS 提升 1000 倍
零拷贝DMA 直接传输,绕过用户空间CPU 拷贝减少 50%
批量处理合并小 IO,减少网络往返网络效率提升 10 倍
压缩批量压缩,算法优化带宽节省 50%~80%

理解这四个原理,你就能明白为什么 Kafka 能用普通硬件支撑百万 QPS。


留给你的问题

假设你在做一个实时数据管道,从 MySQL 同步数据到 Elasticsearch:

  1. 如果不用消息队列,直接用 Canal 同步,数据量大了会出什么问题?
  2. 如果用 Kafka 同步,批量大小(batch.size)应该设为多少?太大了有什么风险?
  3. 如果 Kafka Broker 的 Page Cache 满了,新写入的消息会怎样?应该怎么配置?
  4. 压缩算法 lz4 和 zstd 各有什么优缺点,什么时候用 zstd 更合适?

思考这些问题,能帮助你更好地调优 Kafka 生产环境。

基于 VitePress 构建