性能模型:吞吐、延迟与调优思路
前面的文章覆盖了 Kafka 的核心机制和生态组件。这一篇回到工程视角:Kafka 的性能模型到底长什么样。
性能调优容易变成一张参数清单。更有效的方式是先建立一个吞吐-延迟的基本模型,再用这个模型解释每个参数的作用方向和代价。
本文只抓一个问题:Kafka 的吞吐和延迟分别由哪些因素决定,调节一个参数时另一端会发生什么变化。
性能基础:四个底层机制
Kafka 的高吞吐不是来自某个单一优化,而是四个机制叠加的结果。
1 | |
顺序写盘:Kafka 的日志是 append-only 结构。磁盘的顺序写入速度远高于随机写入——HDD 上顺序写可以达到 200-600 MB/s,而随机写通常只有 1-10 MB/s。Kafka 利用这个特性,把所有消息追加到 segment 文件末尾。
Page cache:Kafka broker 不自己管理内存缓存,而是依赖操作系统的 page cache。写入时数据先进 page cache,由 OS 异步刷盘。读取时如果数据还在 page cache 中,直接命中内存,不需要磁盘 I/O。这个设计使得 Kafka 在重启后不需要"预热"缓存——page cache 属于 OS,broker 进程重启后仍然有效。
零拷贝:消费者拉取数据时,Kafka 使用 Java 的 FileChannel.transferTo(),底层调用 Linux 的 sendfile() 系统调用。数据从 page cache 直接传输到网卡的 socket buffer,不经过用户态的拷贝。传统路径需要四次拷贝(磁盘 -> page cache -> 用户态 buffer -> socket buffer -> 网卡),零拷贝减少到两次(page cache -> 网卡)。
攒批:Producer 端把多条消息攒成一个 batch 再发送,减少网络请求次数。Consumer 端类似,一次 fetch 请求拉取多条消息。
Producer 吞吐模型
Producer 的吞吐量取决于三个维度:单 batch 大小、并行度和压缩。
1 | |
这个公式是概念模型,不是精确计算。它表达的关系是:
batch.size 控制每个 batch 的最大字节数,默认 16384 字节(16 KB)。batch 越大,单次网络请求携带的数据越多,吞吐越高。代价是每条消息的等待时间增加。
linger.ms 控制 Producer 发送 batch 前等待的最长时间,默认 0(有数据就发)。增大 linger.ms 给了更多消息聚合进同一个 batch 的机会,但增加了每条消息的端到端延迟。
partition_count 提供并行度。Producer 按 partition 分别攒批,多个 partition 的 batch 可以并行发送。但 partition 数量不是越多越好——每个 partition 占用 broker 的文件句柄、内存和 controller 的元数据管理开销。
压缩在 batch 级别生效。Kafka 支持四种压缩算法:
| 算法 | 压缩比 | CPU 开销 | 适用场景 |
|---|---|---|---|
| lz4 | 中等 | 低 | 通用场景,吞吐优先 |
| zstd | 高 | 中等 | 带宽受限,愿意用 CPU 换带宽 |
| snappy | 中等 | 低 | 与 lz4 类似,Hadoop 生态常用 |
| gzip | 高 | 高 | 归档场景,对延迟不敏感 |
压缩减少了网络传输和磁盘写入的数据量,但增加了 Producer 端的 CPU 开销。对于文本类消息(JSON、日志),压缩比通常在 3:1 到 8:1 之间,收益显著。对于已经压缩过的二进制数据(图片、Protobuf),压缩可能反而增加 CPU 开销而几乎不减少体积。
Consumer 吞吐模型
Consumer 的吞吐量取决于两个方面:拉取效率和并行度。
1 | |
fetch.min.bytes 控制 broker 返回 fetch 响应前至少积累的数据量,默认 1 字节。增大这个值可以减少空拉取,提高每次请求的数据密度。代价是增加拉取延迟——broker 需要等待足够的数据积累。
max.partition.fetch.bytes 控制每个 partition 单次 fetch 返回的最大数据量,默认 1048576 字节(1 MB)。增大这个值可以减少拉取次数,但增加单次响应的内存占用。
Consumer 的并行度上限等于 partition 数量。一个 consumer group 中,每个 partition 最多分配给一个 consumer 实例。如果 consumer 数量超过 partition 数量,多出来的 consumer 处于空闲状态。
Broker 端的瓶颈点
Broker 的吞吐受三类资源约束:
磁盘 I/O:写入路径的瓶颈。虽然 Kafka 使用顺序写,但多个 partition 的并发写入仍然会产生一定程度的随机 I/O(不同 partition 的 segment 文件分布在不同位置)。JBOD 配置(每个磁盘独立挂载,log.dirs 配置多个目录)可以把不同 partition 分散到不同磁盘,减少争用。
网络 I/O:replication factor 放大了网络带宽消耗。一条消息写入后,需要从 leader 复制到所有 follower。如果 replication factor = 3,一条消息的网络传输量是消息体积的 3 倍(1 次写入 + 2 次复制)。加上 consumer 的 fetch 流量,broker 的网络带宽常常先于磁盘成为瓶颈。
CPU:主要消耗在压缩和解压。如果 Producer 使用压缩发送,broker 在某些场景下需要解压再重新压缩(例如消息格式转换,或 broker 端的压缩配置与 producer 不同)。自 Kafka 0.10 起,如果 producer 和 broker 的消息格式版本一致,broker 可以直接透传压缩后的 batch,不需要解压重压,CPU 开销大幅降低。
延迟模型
端到端延迟由四段组成:
1 | |
produce_latency:从消息进入 Producer 到 broker 确认收到。包含 linger.ms 等待、batch 排队、网络传输和 broker 写入。acks=0 时 producer 不等待确认,延迟最低但可能丢消息。acks=1 时等待 leader 写入。acks=all 时等待所有 ISR 副本写入,延迟最高但可靠性最强。
replication_latency:leader 收到消息后复制到 follower 的时间。受 replica.fetch.max.bytes 和 replica.fetch.wait.max.ms 控制。ISR 中最慢的 follower 决定了这个延迟的上界。
fetch_latency:consumer 发起 fetch 请求到收到数据。受 fetch.min.bytes 和 fetch.max.wait.ms 控制。fetch.min.bytes 设得大,broker 可能需要等待更多数据积累才返回。
processing_latency:业务代码处理消息的耗时。这部分不受 Kafka 配置控制。
关键参数的权衡方向
每个调优参数都在吞吐和延迟之间做权衡。下表总结了主要参数的作用方向:
| 参数 | 增大时吞吐 | 增大时延迟 | 增大时可靠性 | 默认值 |
|---|---|---|---|---|
| batch.size | 上升 | 上升 | 不变 | 16384 |
| linger.ms | 上升 | 上升 | 不变 | 0 |
| acks | 下降(all) | 上升(all) | 上升 | all(自 3.0) |
| compression.type | 通常上升 | 略上升 | 不变 | none |
| fetch.min.bytes | 上升 | 上升 | 不变 | 1 |
| max.in.flight.requests | 上升 | 不变 | 下降(>1 且无幂等) | 5 |
| replica.fetch.max.bytes | 上升 | 不变 | 不变 | 1048576 |
关键 JMX 指标
Kafka 通过 JMX 暴露大量运行时指标。以下是判断性能瓶颈时最值得关注的几个:
Broker 级别:
- kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec:入站字节速率,反映写入压力。
- kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec:出站字节速率,包含 consumer fetch 和 replication 流量。
- kafka.network:type=RequestMetrics,name=RequestsPerSec,request=Produce:produce 请求速率。
- kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions:副本未同步的 partition 数量。持续大于 0 表示有 follower 跟不上 leader。
- kafka.server:type=ReplicaManager,name=IsrShrinksPerSec:ISR 收缩速率。突然升高通常意味着某个 broker 出现性能问题。
- kafka.network:type=RequestChannel,name=RequestQueueSize:请求队列深度。持续增长表示 broker 处理能力不足。
Producer 客户端级别(通过 JMX 或 Metrics Reporter):
- record-send-rate:每秒发送的消息数。
- record-size-avg:平均消息大小。
- request-latency-avg:请求平均延迟。
Consumer 客户端级别:
- records-lag-max:所有 partition 中最大的消费滞后量。
- fetch-rate:每秒 fetch 请求数。
- records-consumed-rate:每秒消费的消息数。
实验:基准测试
Kafka 自带两个性能测试工具:kafka-producer-perf-test 和 kafka-consumer-perf-test。以下实验在一个 3 broker 集群上进行,topic 有 6 个 partition,replication factor = 3。
第一步,创建测试 topic:
1 | |
第二步,Producer 基准测试,对比不同 batch.size:
1 | |
典型输出格式:
1 | |
随着 batch.size 增大,records/sec 和 MB/sec 通常上升,avg latency 也同步上升。
第三步,对比不同压缩算法:
1 | |
对于 200 字节的随机消息,压缩收益有限。如果把 record-size 改为 1000 字节并使用文本内容,lz4 和 zstd 的吞吐量提升会更明显。
第四步,Consumer 基准测试:
1 | |
Consumer 端的吞吐量主要受 partition 数量和消费线程数的限制。增加 threads 参数直到等于 partition 数量为止,吞吐量通常线性增长;超过 partition 数量后不再增长。
模式提炼
Kafka 的攒批发送是一个通用的吞吐-延迟权衡模式。在分布式系统和网络编程中,这个模式反复出现:
TCP Nagle 算法:TCP 层把小包攒成大包再发送,减少网络包数量。Nagle 算法等待一个 RTT 或缓冲区满再发送,与 Kafka 的 linger.ms + batch.size 机制完全对应。禁用 Nagle(TCP_NODELAY)等价于设置 linger.ms=0。
数据库 group commit:数据库把多个事务的 WAL 写入攒成一次 fsync 调用。单个事务的延迟增加(需要等待其他事务凑齐),但磁盘 I/O 次数减少,整体吞吐上升。MySQL 的 binlog_group_commit_sync_delay 参数与 Kafka 的 linger.ms 作用相同。
网络 packet coalescing:网卡驱动把多个小包合并成一个大包再触发中断(interrupt coalescing),减少 CPU 中断次数。延迟增加但 CPU 利用率下降。
这三个场景和 Kafka 的攒批共享同一个抽象:在发送端引入一个时间窗口或空间窗口,把多个小请求合并成一个大请求,用单条请求的延迟换取整体的吞吐。
工程迁移表
| 概念 | Kafka | RocketMQ | 数据库 |
|---|---|---|---|
| 攒批参数 | batch.size + linger.ms | maxBatchMessageCount + accumulationTimeMs | binlog_group_commit_sync_delay |
| 顺序写 | partition segment 顺序追加 | CommitLog 顺序追加 | WAL 顺序追加 |
| 零拷贝 | sendfile() | mmap(ConsumeQueue) | 通常不使用 |
| 压缩 | producer 端 batch 级压缩 | producer 端消息级压缩 | 表级/页级压缩 |
| 并行度控制 | partition 数量 | MessageQueue 数量 | 连接数 / 并行查询数 |
| 吞吐监控 | BytesInPerSec / BytesOutPerSec | putTps / getTransferredTps | QPS / TPS |
| 延迟监控 | request-latency-avg | sendMessageRT | slow query log |
| 背压信号 | RequestQueueSize 增长 | broker busy 错误 | 连接池满 / 锁等待 |
常见误解
partition 越多吞吐越高:partition 数量增加确实提升了并行度,但每个 partition 对应一组 segment 文件和索引文件。Partition 数量过多会导致 broker 的文件句柄数暴增、controller 元数据管理压力增大、leader election 时间延长。在 KRaft 模式下,单集群可以支撑的 partition 数量比 ZooKeeper 模式高,但仍然不是无限的。通常建议先按 consumer 的处理能力估算所需并行度,再加一定余量,而不是盲目设置大量 partition。
压缩总是有帮助的:对于小消息(几十字节)或已经压缩过的二进制内容,压缩的 CPU 开销可能超过节省的网络和磁盘 I/O。是否使用压缩应该通过基准测试验证。
Kafka 必须用 SSD:Kafka 的 I/O 模式是顺序写 + 顺序读(大部分消费是 tail read,数据在 page cache 中)。HDD 在顺序 I/O 场景下的性能足以支撑大多数工作负载。SSD 的优势在随机 I/O 场景中更明显——例如 partition 数量非常多导致并发写入不再纯粹顺序、或大量历史数据回溯导致 page cache 失效时。LinkedIn 早期的 Kafka 集群大量使用 HDD。
练习
-
使用 kafka-producer-perf-test 分别测试 batch.size=1024、16384、131072 的吞吐量和延迟,画出 batch.size 与 throughput、avg latency 的关系图。观察是否存在收益递减的拐点。
-
创建两个 topic,一个 6 partition,一个 60 partition。使用相同的 producer 配置发送相同数量的消息,对比吞吐量。60 partition 的吞吐量是否是 6 partition 的 10 倍?
-
使用 JMX 工具(JConsole 或 jmx_exporter + Prometheus)观察一个 producer 持续发送时 broker 的 BytesInPerSec、BytesOutPerSec 和 UnderReplicatedPartitions 三个指标。replication factor = 3 时,BytesOutPerSec 大约是 BytesInPerSec 的几倍?
-
把 compression.type 分别设为 none、lz4、zstd,发送 1KB 的 JSON 文本消息各 100 万条,记录吞吐量、CPU 使用率和磁盘写入量。哪种压缩算法的 throughput/CPU 比最优?
系列导航
参考资料
- Apache Kafka Documentation — Producer Configs / Consumer Configs: https://kafka.apache.org/documentation/#producerconfigs
- Jay Kreps. The Log: What every software engineer should know about real-time data’s unifying abstraction: https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying
- Kafka Performance Tuning — Confluent Documentation: https://docs.confluent.io/platform/current/kafka/performance.html
- Neha Narkhede, Gwen Shapira, Todd Palino. Kafka: The Definitive Guide. O’Reilly, Chapter 3 (Kafka Producers) and Chapter 4 (Kafka Consumers).
