ShardingSphere开启预编译参数insert慢定位
ShardingSphere开启预编译参数insert慢定位
问题背景
用户反馈在使用 ShardingSphere-JDBC 进行批量插入时性能较慢,插入 500 条数据耗时超过 20 秒。
而相同配置下,ShardingSphere-Proxy 执行相同操作则表现正常。
环境信息:
- MySQL 驱动版本:8.0.22
- MySQL Server 版本:5.7.38
- ShardingSphere 版本:5.x
问题现象
初步现象
相同配置下:
- Proxy 执行:正常(约 1-2 秒)
- JDBC 执行:很慢(500 条数据超过 20 秒)
通过添加日志确认:慢在 MySQL 驱动执行 SQL 到驱动返回结果这段过程。
疑问点
疑问 1:数据源参数配置相同,为什么 Proxy 不慢而 JDBC 慢?
疑问 2:useServerPrepStmts 和 cachePrepStmts 这两个参数对批量插入的影响是什么?
问题定位
1. 排查思路
1 | |
2. ShardingSphere-JDBC 添加日志分析
在 JDBCExecutorCallback 中添加执行耗时日志,记录调用 MySQL JDBC 驱动执行 SQL 前后耗时:
1 | |
发现 MySQL 驱动执行 SQL 较慢,初步排除 ShardingSphere 问题。
3. 抓包分析
使用 tcpdump 抓取 MySQL 驱动到 MySQL Server 通信包:
1 | |
使用 Wireshark 分析,发现关键问题:
| 包序号 | 包类型 | 描述 | 耗时 |
|---|---|---|---|
| 658 | COM_STMT_PREPARE | 预编译语句 | - |
| 17104 | COM_STMT_EXECUTE | 执行语句(设置 12,000 个参数) | - |
| 23415 | Response | 响应包 | MySQL 服务端响应较慢,需要几十秒 |
关键结论:慢在 COM_STMT_EXECUTE 阶段的参数设置过程,服务端响应异常缓慢。怀疑是 MySQL Server 端问题。
4. 完整测试矩阵验证
测试 4 种驱动版本 × 2 种 MySQL 版本 × 2 种参数组合:
| 驱动版本 | Server 版本 | useServerPrepStmts | 耗时 (ms) | 状态 |
|---|---|---|---|---|
| 5.1.49 | 5.7.38 | true | 27,182 | ⚠️ SLOW (21x) |
| 5.1.49 | 5.7.38 | false | 1,303 | ✓ 正常 |
| 5.1.49 | 8.0.32 | true | 1,750 | ✓ 正常 |
| 5.1.49 | 8.0.32 | false | 2,300 | ✓ 正常 |
| 8.0.22 | 5.7.38 | true | 29,291 | ⚠️ SLOW (23x) |
| 8.0.22 | 5.7.38 | false | 1,257 | ✓ 正常 |
| 8.0.22 | 8.0.32 | true | 1,763 | ✓ 正常 |
| 8.0.22 | 8.0.32 | false | 2,745 | ✓ 正常 |
| 8.0.33 | 5.7.38 | true | 23,553 | ⚠️ SLOW (15x) |
| 8.0.33 | 5.7.38 | false | 1,563 | ✓ 正常 |
| 8.0.33 | 8.0.32 | true | 1,711 | ✓ 正常 |
| 8.0.33 | 8.0.32 | false | 3,661 | ✓ 正常 |
| 8.3.0 | 5.7.38 | true | 25,844 | ⚠️ SLOW (9x) |
| 8.3.0 | 5.7.38 | false | 2,846 | ✓ 正常 |
| 8.3.0 | 8.0.32 | true | 1,752 | ✓ 正常 |
| 8.3.0 | 8.0.32 | false | 3,679 | ✓ 正常 |
核心发现:
问题根源:MySQL 5.7.38 服务端预编译性能瓶颈
- 所有测试的驱动版本在 MySQL 5.7.38 + useServerPrepStmts=true 时都出现性能问题
- 性能差距:15-23 倍(25 秒 vs 1-2 秒)
MySQL 8.0.32 无此问题
- 所有驱动版本在 MySQL 8.0.32 上表现正常
useServerPrepStmts=false 是有效的临时解决方案
- 在 MySQL 5.7.38 上,设置
useServerPrepStmts=false后所有版本都恢复正常
- 在 MySQL 5.7.38 上,设置
5. 为什么 Proxy 不慢而 JDBC 慢?
通过分析连接参数发现:
Proxy 场景:
- 应用连接 Proxy 的 URL 没有指定
useServerPrepStmts=true,此时 Proxy 默认按照普通 Statement 执行(不走预编译) - Proxy → MySQL 的 URL 里的 useServerPrepStmts 参数失效(因为使用的普通 statement)
JDBC 场景:
- 应用使用 prepared statement
- 实际 ShardingSphere-JDBC 在存储单元的 HikariCP 连接池 dataSourceProperties 会自动添加
useServerPrepStmts=true和cachePrepStmts=true - ShardingSphere-JDBC 通过上面预编译参数通过 MySQL 驱动连接 MySQL Server,触发了预编译性能问题
如何让 Proxy 复现同样的 JDBC 慢问题:
如果应用连接 Proxy 时显式指定 useServerPrepStmts=true,Proxy 也会创建 PreparedStatement 并在 MySQL URL 里添加该参数,同样会触发 MySQL bug 执行慢。
根因分析
MySQL Bug #73056
Bug 链接:https://bugs.mysql.com/bug.php?id=73056
问题描述:当 binlog、general log 或 slow query log 启用时,MySQL 5.7 在处理 prepared statement 时会生成完整的 SQL 用于日志记录,而生成过程使用了极低效的 String::replace() 算法。
性能影响:50 倍慢(实际测试 15-23 倍,完全吻合)
调用栈分析
1 | |
问题根源:insert_params_with_log() 函数使用 String::replace() 逐个替换参数,每次替换都会:
- 重新分配字符串缓冲区
- 复制整个字符串
- 复制参数值
对于 12,000 个参数(500 条 × 24 字段),这意味着 12,000 次内存重分配和字符串复制!
本地测试验证
使用原生 MySQL JDBC 驱动直接测试,复现问题:
1 | |
必须开启 --slow_query_log=1 参数才会出现该问题,这与 Bug #73056 的描述完全一致。
Perf 性能分析
为了进一步验证根因,使用 perf 工具对 Docker 容器中的 MySQL 5.7.38 进行 CPU 性能分析。
1. Perf 抓取方法
1.1 获取容器 PID
1 | |
1.2 使用 perf 抓取性能数据
1 | |
在抓取期间执行慢的 SQL(触发预编译 bug)。
1.3 生成报告
1 | |
2. 实际性能分析结果
2.1 抓取信息
| 项目 | 值 |
|---|---|
| 容器名称 | mysqlv5-3306-temp |
| 目标进程 | mysqld |
| 抓取时长 | 60 秒 |
| 总采样数 | 463K 样本 |
| 总事件数 | ~4615 亿 CPU 周期 |
2.2 核心性能瓶颈
🔴 瓶颈 1: 内存拷贝 (27.61%)
1 | |
调用栈:
1 | |
问题:Prepared Statement 参数插入时频繁进行内存重新分配和拷贝。
🔴 瓶颈 2: 内存页管理开销 (~40% 内核占用)
| 函数 | 占比 | 说明 |
|---|---|---|
clear_page_erms |
8.41% | 清零内存页 |
do_anonymous_page |
3.47% | 匿名页分配 |
__mod_memcg_lruvec_state |
3.42% | 内存 cgroup 状态修改 |
__handle_mm_fault |
2.69% | 处理内存缺页错误 |
rmqueue |
1.75% | 页分配器队列操作 |
handle_mm_fault |
1.53% | 内存故障处理 |
问题:频繁的内存分配导致大量页错误和 cgroup 开销。
🔴 瓶颈 3: 大量页错误 (Page Fault)
exc_page_fault(1.64%)do_user_addr_fault(1.71%)handle_pte_fault(0.77%)
2.3 性能分析结论
通过实际 perf 分析验证:
String::mem_realloc()被频繁调用,每次都需要memcpy大量数据(27.61% CPU 消耗)- 频繁的内存分配导致内核页错误处理开销巨大(约 40% 内核占用)
- 内存 cgroup 统计修改 也占用了 3.42% 的 CPU
这与 Bug #73056 的描述完全吻合:MySQL 5.7 在处理批量 INSERT 时的 Prepared Statement 参数处理存在严重的性能问题。
3. 火焰图分析
生成的火焰图清晰显示了性能瓶颈:
- 最宽的栈:
String::replace和__memcpy_ssse3 - 调用路径:
dispatch_command → mysqld_stmt_execute → Prepared_statement::set_parameters → String::replace - 颜色深度:内核函数(内存管理)占比极高
火焰图 X 轴表示样本数量,Y 轴表示调用栈深度。String::replace 及其相关的内存拷贝函数占据了火焰图的绝大部分宽度,直观展示了性能瓶颈所在。
解决方案
| 方案 | 操作 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 方案 1 | useServerPrepStmts=false |
立即生效 | 无法使用服务端预编译优化 | ⭐⭐⭐⭐ |
| 方案 2 | 升级到 MySQL 8.0.x | 彻底解决,可使用预编译优化 | 需要数据库升级 | ⭐⭐⭐⭐⭐ |
| 方案 3 | 关闭 slow_query_log 等日志 | 可能生效 | 影响审计和恢复 | ⭐⭐ |
方案 1:关闭服务端预编译(推荐临时方案)
修改 JDBC URL:
1 | |
效果:立即解决性能问题,insert 耗时从 20+ 秒降至 1-3 秒。
说明:
useServerPrepStmts=false时,PreparedStatement 和普通 Statement 一样,在客户端拼接 SQL 语句- 不会触发 MySQL 5.7 的服务端预编译 bug
方案 2:升级 MySQL(推荐长期方案)
升级到 MySQL 8.0.x 后,可以继续使用 useServerPrepStmts=true:
1 | |
MySQL 8.0 对此进行了优化:
- 修复了 Bug #73056
- 重写了
insert_params_with_log()算法 - 预分配缓冲区,避免重复 realloc
- 反向处理参数,避免重复复制
总结
问题定位完成 ✅
| 层级 | 问题 | 状态 |
|---|---|---|
| 应用层 | ShardingSphere JDBC | ✅ 排除 |
| 驱动层 | MySQL Connector/J | ✅ 排除 |
| 网络层 | TCP 传输 | ✅ 排除 |
| 协议层 | Prepared Statement 协议 | ✅ 确认 |
| MySQL 层 | Bug #73056 | ✅ 确认 |
技术细节
1 | |
