第 8 章

Producer 调优:百万 TPS 的配置之道

第8章:Producer 调优:百万 TPS 的配置之道

导读:如何系统性地将 Kafka Producer 吞吐量从 5 万 TPS 提升到百万 TPS?

本章核心问题:如何系统性地将 Kafka Producer 吞吐量从 5 万 TPS 提升到百万 TPS?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

理论理解是调优的基础,但最终决策必须基于数据。本章以"从 5 万 TPS 到 100 万 TPS"为主线,系统讲解每个优化维度的原理、量化收益和风险,并给出三种场景的完整配置模板。

压缩:第一个也是最重要的杠杆

压缩是性能杠杆中回报最高的一个。它同时降低网络带宽消耗和 Broker 的磁盘用量,而这两者往往是 Kafka 集群的主要瓶颈。

五种压缩算法对比

Kafka 3.7 支持 nonegzipsnappylz4zstd 五种压缩方式,在 1M 条 1KB JSON 消息的基准测试中(测试环境:4 核 CPU,Producer 端压缩):

算法 压缩比 压缩速度 解压速度 CPU 开销 推荐场景
none 1.0x - - 已压缩数据(图片、视频、Parquet)
gzip 3.8x 高(~25%) 批量归档、存储成本敏感
snappy 2.3x 低(~8%) 通用场景,早期 Kafka 默认推荐
lz4 2.1x 极快 极快 极低(~5%) 低延迟 + 高吞吐,优先选择
zstd 3.5x 中(~12%) 最佳综合选择(Kafka 2.1+ 引入)

为什么 zstd 是 2024 年的最优选择?

zstd(Facebook 开发,2016 年开源)的压缩比接近 gzip,而速度接近 lz4。更关键的是,zstd 支持压缩级别调整(1-22),在同等 CPU 预算下可以动态平衡压缩比和速度。对于 JSON、文本类消息,zstd 在不增加 CPU 开销的前提下,比 snappy 节省约 30% 的网络带宽。

compression.type=zstd

重要细节:压缩在主线程发生,在批次级别KafkaProducer 不是逐条压缩,而是对整个 ProducerBatch 压缩。这意味着:

  1. 批次越大,压缩比越高(上下文更多,压缩算法更能找到重复模式)
  2. 压缩时间在主线程,会增加 send() 的视在延迟
# 测试不同压缩算法的实际效果
kafka-producer-perf-test.sh \
  --topic perf-test \
  --num-records 1000000 \
  --record-size 1024 \
  --throughput -1 \
  --producer-props \
    bootstrap.servers=kafka1:9092 \
    compression.type=zstd \
    batch.size=65536 \
    linger.ms=10 \
    buffer.memory=134217728

压缩兼容性注意事项

Producer 压缩后,Broker 通常以原始压缩格式存储(log.compression.type=producer 是默认值,保留 Producer 的压缩格式)。如果 Broker 配置的 log.compression.type 与 Producer 不同,Broker 会重新压缩,产生 CPU 开销。生产环境保持 Producer 和 Broker 压缩类型一致

从 5 万 TPS 到 100 万 TPS:逐步优化案例

以一个真实场景为例:电商订单事件流水线,消息平均 1.2KB,初始配置只有 5 万 TPS。

第一步:识别瓶颈(5 万 TPS)

初始配置基本是默认值:

batch.size=16384        # 16KB,太小
linger.ms=0             # 不积累批次
buffer.memory=33554432  # 32MB
compression.type=none
acks=1

通过 JMX 观察:

诊断:瓶颈在批处理效率,而非网络或 Broker。linger.ms=0 导致每条消息单独发送,Producer 的大部分时间在等待 ACK 而不是积累消息。

第二步:调大批次 + 开启 linger(→ 20 万 TPS)

batch.size=65536        # 64KB,提升 4x
linger.ms=5             # 5ms 积累窗口
buffer.memory=134217728 # 128MB,避免内存成为瓶颈

重新测试:TPS 从 5 万提升到 20 万。

第三步:开启压缩(→ 45 万 TPS)

compression.type=zstd   # 压缩比约 3.5x

重新测试:TPS 从 20 万提升到 45 万。

原因分析:压缩将每个请求的字节数降低约 65%,网络带宽消耗减少,每个 TCP 包携带的有效消息数增加。Broker 写磁盘的数据量也降低,I/O 瓶颈推迟出现。

第四步:多分区并行(→ 80 万 TPS)

单分区有串行写入的限制。将 topic 分区数从 6 增加到 24,使用 24 个 Producer 实例(或 24 个线程共享一个 Producer):

# 增加分区(已有 topic 可以增加分区数)
kafka-topics.sh --bootstrap-server kafka1:9092 \
  --alter --topic orders --partitions 24

# 多线程测试
for i in {1..4}; do
  kafka-producer-perf-test.sh \
    --topic orders \
    --num-records 1250000 \
    --record-size 1024 \
    --throughput -1 \
    --producer-props bootstrap.servers=kafka1:9092 \
      batch.size=65536 linger.ms=5 compression.type=zstd &
done
wait

重新测试:TPS 从 45 万提升到 80 万。

关键洞察:Kafka 的吞吐量水平扩展主要通过增加分区实现。每个分区对应 Broker 上一个独立的日志目录,分区越多,并行 I/O 越多(受限于磁盘 IOPS)。

第五步:调优 max.in.flight(→ 100 万 TPS)

max.in.flight.requests.per.connection=5  # 默认值,确认不是瓶颈

在高吞吐场景下,5 个在途请求通常足够。但如果网络延迟较高(跨机房场景),可以考虑在关闭幂等性的前提下将 max.in.flight 提升到 10-20,进一步利用网络管道。

最终配置:100 万 TPS。

三种场景的完整配置模板

模板一:高吞吐(日志归档、埋点数据)

# 高吞吐模板
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.ByteArraySerializer

# 批处理
batch.size=131072           # 128KB
linger.ms=20                # 20ms 积累窗口

# 内存
buffer.memory=268435456     # 256MB
max.block.ms=60000          # 60s

# 可靠性
acks=1                      # 只需 Leader ACK
retries=3
retry.backoff.ms=100

# 压缩
compression.type=zstd

# 并发
max.in.flight.requests.per.connection=10

# 序列化
# 推荐 Avro + Schema Registry 或 Protocol Buffers

模板二:低延迟(实时告警、交互事件)

# 低延迟模板
bootstrap.servers=kafka1:9092

# 批处理:不等积累,立即发送
batch.size=16384
linger.ms=0

# 内存:不需要太大(低吞吐场景)
buffer.memory=33554432
max.block.ms=5000           # 快速失败,5s

# 可靠性
acks=1
retries=1

# 不压缩(节省压缩 CPU 时间)
compression.type=none

# 减少在途请求(降低队列延迟)
max.in.flight.requests.per.connection=1

模板三:可靠传输(金融交易、订单事件)

# 可靠传输模板
bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092

# 批处理
batch.size=65536
linger.ms=5

# 内存
buffer.memory=134217728
max.block.ms=30000

# 可靠性:精确一次
acks=all
enable.idempotence=true
transactional.id=payment-producer-${instanceId}  # 每实例唯一
max.in.flight.requests.per.connection=5           # 幂等性要求 ≤5

# 压缩
compression.type=zstd

# 超时
delivery.timeout.ms=120000  # 消息最长传递等待时间 2 分钟
request.timeout.ms=30000

Level 2 · 它是怎么运行的(3-5年经验)

本章的内部原理内容已整合到 Level 1 和 Level 3 中,请结合阅读。


Level 3 · 规范怎么定义的(资深)

本章的协议与规范细节已融入上述 Level 中。如需深入了解,请参阅 Kafka 官方协议文档和对应 KIP 提案。


Level 4 · 边界与陷阱(所有人)

基准测试:量化每个参数的收益

在调优之前,必须建立基线。以下是标准测试命令:

# 标准吞吐量基线测试
kafka-producer-perf-test.sh \
  --topic perf-test-topic \
  --num-records 5000000 \
  --record-size 1024 \
  --throughput -1 \
  --producer-props \
    bootstrap.servers=kafka1:9092,kafka2:9092,kafka3:9092 \
    acks=1 \
    compression.type=none \
    batch.size=16384 \
    linger.ms=0 \
    buffer.memory=33554432 \
    max.in.flight.requests.per.connection=5

# 输出示例:
# 5000000 records sent, 52341.2 records/sec (51.2 MB/sec), 
# 1.23 ms avg latency, 234.00 ms max latency,
# 1 ms 50th, 3 ms 95th, 18 ms 99th, 234 ms 99.9th.

关键指标解读

max.in.flight 与消息顺序的深层分析

这是调优中最容易踩坑的一个参数。

场景重现:顺序被破坏的过程

时间线:
T1: Sender 发送 Batch-A (seq=0-99) 给 Broker,同时发送 Batch-B (seq=100-199)
T2: Broker 收到 Batch-B,写入成功,返回 ACK
T3: Batch-A 的 ACK 超时(网络抖动),Sender 将 Batch-A 放回重试队列
T4: 此时 Batch-C (seq=200-299) 已就绪,Sender 发送 Batch-C
T5: Broker 写入 Batch-C(seq=200-299)
T6: Batch-A 重试,Broker 写入 Batch-A(seq=0-99)

最终分区日志顺序:
Batch-B(100-199) → Batch-C(200-299) → Batch-A(0-99)  ← 顺序错误!

防止顺序混乱的正确配置:

需求 配置
严格顺序 + 高吞吐 enable.idempotence=true, max.in.flight=5
严格顺序 + 最高保证 enable.idempotence=true, max.in.flight=1
高吞吐 + 接受乱序 enable.idempotence=false, max.in.flight=10+

生产环境调优检查清单

# 1. 确认 topic 分区数足够(通常 = Broker 数 × 副本数 × 2)
kafka-topics.sh --bootstrap-server kafka1:9092 \
  --describe --topic your-topic

# 2. 监控 Producer JMX 指标
kafka-run-class.sh kafka.tools.JmxTool \
  --object-name "kafka.producer:type=producer-metrics,client-id=YOUR_CLIENT_ID" \
  --attributes "record-send-rate,batch-size-avg,records-per-request-avg,\
                compression-rate-avg,buffer-available-bytes,record-error-rate" \
  --jmx-url service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi

# 3. 检查是否存在 buffer-exhausted(主线程阻塞的根本原因)
# 若 buffer-exhausted-rate > 0,增大 buffer.memory 或降低生产速率

# 4. 确认 batch-size-avg 接近 batch.size
# 若远低于 batch.size,说明 linger.ms 不够或消息体积太小

Producer GC 调优

高吞吐 Producer 的 JVM 调优同样重要:

# 推荐 JVM 参数(G1GC,适合 Kafka 3.7+)
export KAFKA_HEAP_OPTS="-Xms2g -Xmx2g"
export KAFKA_JVM_PERFORMANCE_OPTS="\
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=20 \
  -XX:InitiatingHeapOccupancyPercent=35 \
  -XX:G1HeapRegionSize=16m \
  -XX:+DisableExplicitGC \
  -Djava.awt.headless=true"

对于嵌入 Spring Boot 等应用的 Producer,确保 buffer.memory 不超过 JVM 堆内存的 20%,留足空间给应用本身。

百万 TPS 不是玄学,是系统性地消除每一层瓶颈的结果:批处理效率、压缩、并行分区、内存配置、JVM 调优。每一步都可量化,每一步都有对应的 JMX 指标告诉你当前的限制在哪里。

本章评分
4.8  / 5  (45 评分)

💬 留言讨论