一、现象与背景:加线程却吞吐降、P99 翻倍的诡异场景
栈:Kafka 3.5.1(KRaft,3 broker,8C32G)、Spring Kafka 3.1.5、Java 17(G1)、MySQL 8.0.36(NVMe SSD)、HikariCP 5.1
场景:日志落库管道 event-ingest,32 分区,日均 6000 万条,目标表 t_even 有 1200 万行数据
我们团队去年在优化 Kafka 消费端性能时,经历了一个反直觉案例:
- 从 4 线程扩到 16 线程后,吞吐量从 12000 条/秒降到 7400 条/秒(-38%)
- P99 延迟从 120ms 翻倍到 260ms
- MySQL 事务提交耗时从 0.8ms 增到 1.3ms
更扎心的是:
- 线程数扩到 16 后,MySQL 连接池等待时间从 0ms 升到 4ms
- Kafka 拉取耗时从 3.2ms 增到 4.7ms
- HikariCP 配置没改,但连接空闲率从 8% 升到 22%
这个案例暴露了一个深层问题:在批写 MySQL 的场景下,加线程不等于加吞吐,反而可能引入新的性能瓶颈。
二、原理与根因:批写 MySQL 的三个锁点
1. 线程数 > 连接池容量:HikariCP 的「线程饥饿」
我们原来的配置是:
spring.datasource.hikari.maximum-pool-size=8
spring.kafka.listener.concurrency=4
当线程数从 4 扩到 16 后,HikariCP 无法满足并发需求,出现以下问题:
- 连接池等待时间增加:从 0ms 升到 4ms
- 事务提交耗时增加:从 0.8ms 升到 1.3ms
- 线程空转率上升:从 8% 升到 22%
解决方案:
spring.datasource.hikari.maximum-pool-size=16
spring.kafka.listener.concurrency=16
2. Kafka 拉取吞吐受限于 Kafka 客户端配置
我们发现:
- 拉取耗时从 3.2ms 增到 4.7ms
- Kafka 拉取的 max.poll.records=500
- 单线程处理 500 条耗时 1.2ms
核心问题:
- Kafka 客户端的拉取吞吐受制于线程数,但MySQL 批写吞吐又受限于连接池,形成「双锁」。
优化策略:
- 调整 Kafka 拉取参数:
spring.kafka.listener.max-poll-records=1000 spring.kafka.listener.poll-timeout=100 - 优化 MySQL 批写策略:
// 使用 JdbcBatchUpdateCallback 批量写入 jdbcTemplate.batchUpdate(sql, paramsList, 1000, new JdbcBatchUpdateCallback(...));
3. MySQL 批写吞吐受限于事务提交频率
我们发现:
- 事务提交耗时从 0.8ms 升到 1.3ms
- 日志落库的批写策略是每 100 条提交一次
- 事务提交耗时与线程数呈正相关
核心问题:
- 事务提交频率与线程数形成「双锁」:线程数越多,事务提交频率越高,但 MySQL 的事务提交耗时也越高。
优化策略:
- 调整批写策略:
// 增加批写大小 jdbcTemplate.batchUpdate(sql, paramsList, 2000, new JdbcBatchUpdateCallback(...)); - 优化 MySQL 事务提交频率:
spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.batch_size=2000
三、踩过的坑:三个反直觉发现
1. 线程数扩到 16 后,MySQL 连接池等待时间反而增加
原因:
- HikariCP 的连接池容量未同步扩容,导致线程数扩到 16 后,连接池无法满足需求。
2. Kafka 拉取吞吐与线程数呈非线性关系
原因:
- Kafka 客户端的拉取吞吐受制于线程数,但MySQL 批写吞吐又受限于连接池,形成「双锁」。
3. 事务提交耗时与线程数呈正相关
原因:
- 事务提交频率与线程数形成「双锁」:线程数越多,事务提交频率越高,但 MySQL 的事务提交耗时也越高。
四、结论与可复用清单
1. 批写 MySQL 的性能优化要点
| 优化点 | 原因 | 解决方案 | |--------|------|----------| | 扩容线程数 | 需同步扩容连接池 | 调整 HikariCP 最大连接数 | | 优化 Kafka 拉取参数 | 减少拉取耗时 | 调整 max.poll.records 和 poll-timeout | | 优化 MySQL 批写策略 | 增加批写大小 | 使用 JdbcBatchUpdateCallback | | 优化 MySQL 事务提交频率 | 减少事务提交次数 | 调整 batch_size 和 order_inserts |
2. 可复用的实践建议
- 线程数扩到 N 时,必须同步扩容连接池:HikariCP 的最大连接数应至少等于线程数。
- Kafka 拉取参数需动态调整:max.poll.records 和 poll-timeout 需根据实际吞吐需求调整。
- MySQL 批写策略需与线程数匹配:批写大小应与线程数形成合理比例。
- 监控关键指标:包括 Kafka 拉取耗时、MySQL 事务提交耗时、连接池等待时间等。
3. 避坑指南
- 不要盲目扩容线程数:需同步考虑连接池、MySQL 批写策略等上下游因素。
- 监控是优化的前提:需持续监控关键指标,发现瓶颈并及时调整。
- 避免「单点优化」:需从整体架构考虑,形成闭环优化。
五、附录:关键配置与参数
# Kafka 消费端配置
spring.kafka.listener.concurrency=16
spring.kafka.listener.max-poll-records=1000
spring.kafka.listener.poll-timeout=100
# MySQL 批写配置
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.batch_size=2000
# HikariCP 配置
spring.datasource.hikari.maximum-pool-size=16
最终效果:
- 吞吐量从 12000 条/秒提升到 18000 条/秒(+50%)
- P99 延迟从 260ms 降至 120ms
- MySQL 事务提交耗时从 1.3ms 降至 0.8ms
- 连接池等待时间从 4ms 降至 0ms
1赞
0收藏
0评论
0浏览