目录
[1. 主流工具 / 方案舍弃原因](#1. 主流工具 / 方案舍弃原因)
[2. 最终方案:自定义压测脚本 bench.sh](#2. 最终方案:自定义压测脚本 bench.sh)
[1. 建库建表](#1. 建库建表)
[2. 生成测试数据](#2. 生成测试数据)
[3. 导出为压测专用备份文件供脚本使用](#3. 导出为压测专用备份文件供脚本使用)
[五、ProxySQL 配置](#五、ProxySQL 配置)
[1. 主机组配置](#1. 主机组配置)
[2. 路由规则](#2. 路由规则)
[六、测试脚本 bench.sh 思路、设计与实现](#六、测试脚本 bench.sh 思路、设计与实现)
[1. 核心前提](#1. 核心前提)
[2. 设计目标](#2. 设计目标)
[3. 整体架构(三进程模型)](#3. 整体架构(三进程模型))
[4. 执行流程](#4. 执行流程)
[5. 关键设计](#5. 关键设计)
[6. 完整测试脚本 bench.sh](#6. 完整测试脚本 bench.sh)
[1. 实测数据汇总表](#1. 实测数据汇总表)
[2. 测试结果说明](#2. 测试结果说明)
[(2)主库 CPU 负载](#(2)主库 CPU 负载)
[(3)主库磁盘 IO](#(3)主库磁盘 IO)
在公开权威测试中,仅 Percona 发布过 ProxySQL 2.x 系列在 TPCC 混合负载下的性能损耗数据:相比直接连接 MySQL,ProxySQL 会带来约 2%~6% 的吞吐量下降与 0.3~0.5ms 的 95% 延迟增加。(https://www.percona.com/blog/proxysql-2-6-performance-improvements-and-benchmarks/,原文虽下架,但数据已被业界广泛引用、存档,是目前唯一可信的 ProxySQL 纯开销数据。)
|--------|--------------|------------------|----------|
| 并发 | 直连 MySQL | ProxySQL 2.6 | 延迟增加 |
| 8 | 7.3 ms | 7.8 ms | +0.5 ms |
| 16 | 8.1 ms | 8.6 ms | +0.5 ms |
| 32 | 9.2 ms | 9.6 ms | +0.4 ms |
| 64 | 11.5 ms | 11.9 ms | +0.4 ms |
| 128 | 15.8 ms | 16.1 ms | +0.3 ms |
|--------|--------------|------------------|----------|
| 并发 | 直连 MySQL | ProxySQL 2.6 | 性能损耗 |
| 8 | 1,017 | 951 | -6.5% |
| 16 | 1,821 | 1,711 | -6.0% |
| 32 | 3,158 | 3,011 | -4.7% |
| 64 | 5,011 | 4,872 | -2.8% |
| 128 | 6,418 | 6,299 | -1.9% |
目前不存在权威机构发布的、基于 ProxySQL 实现主从读写分离架构并与直连主库进行对比的官方基准测试数据。读写分离带来的整体性能提升依赖业务读写比例、主从资源瓶颈等环境因素,需通过实测获得量化结果。
一、测试目的
模拟线上 80% 读、20% 写短事务业务场景,对比直连 MySQL 主库与 ProxySQL 2.7.3 读写分离架构的吞吐量(QPS)、响应时延、主库 CPU 平均使用率、主库磁盘 IO % util。量化读写分离的降压与性能提升效果,为生产环境落地提供真实、可复现、可追溯的测试依据。
二、压测工具与方案选型说明
1. 主流工具 / 方案舍弃原因
- Sysbench:默认采用长事务 / 多语句事务模型,与 tpcc-mysql 一致。由于 ProxySQL 机制限制,事务内所有语句不做读写分离,全部强制走主库,导致读写分离完全失效,测试结果无效。
- tpcc-mysql:典型长事务、复杂交易模型,事务内混合读写,ProxySQL 同样不做读写分离,全部路由主库,与本次短单语句、读多写少场景不匹配。
- 存储过程:ProxySQL 无法解析存储过程内部 SQL,所有语句强制走主库,读写分离失效。
- Shell 短连接逐行执行:每条 SQL 新建 / 断开连接,无法复用连接,性能失真,不贴合生产连接池模型。
- mysqlslap:无法自定义真随机读写比例、无法每次重建基准数据、长连接不稳定,不满足本次测试要求。
2. 最终方案:自定义压测脚本 bench.sh
- 短单语句自动提交:确保 ProxySQL 读写分离 100% 生效。
- 固定总请求量:所有用例总 SQL 数量完全一致,结果公平可对比。
- 长连接复用:每个并发仅建立一次连接,批量执行 SQL,符合生产环境的连接池特性。
- 本机压测:脚本运行在 MySQL 主库所在主机,排除网络波动干扰。
- 双进程独立监控:压测 + CPU/IO 监控完全隔离,数据真实。
- 环境初始化:每次自动重建库、重新导入相同的测试数据、分析表,使得每轮测试起跑线完全一致。
三、测试环境
- MySQL 版本:8.0.22
- 架构:一主(10.252.1.162:18251)两从(10.252.1.163:18251、10.252.1.168:18251),半同步复制,GTID + AUTO_POSITION
- ProxySQL 版本:2.7.3
- ProxySQL 配置:一写三读(hostgroup 1 写 | hostgroup 2 读),读写分离 + 读负载均衡
- 压测执行位置:MySQL 主库本机
- 测试表:user_order,100 万行数据,主键 + 索引齐全。
四、测试数据初始化(主库执行)
1. 建库建表
sql
DROP DATABASE IF EXISTS test_bench;
CREATE DATABASE test_bench;
USE test_bench;
CREATE TABLE user_order (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id int NOT NULL,
order_no varchar(64) NOT NULL,
status tinyint NOT NULL,
create_time datetime NOT NULL,
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id)
) ENGINE=InnoDB;
2. 生成测试数据
sql
USE test_bench;
DROP PROCEDURE IF EXISTS init_data;
DELIMITER //
CREATE PROCEDURE init_data()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 1000000 DO
INSERT INTO user_order(id, user_id, order_no, status, create_time)
VALUES(
i,
FLOOR(RAND() * 1000000),
CONCAT('ORDER_', i),
FLOOR(RAND() * 5),
DATE_SUB(NOW(), INTERVAL FLOOR(RAND()*365) DAY)
);
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
CALL init_data();
3. 导出为压测专用备份文件供脚本使用
bash
mysqldump -uwxy -p123456 -h10.252.1.162 -P18251 \
--set-gtid-purged=OFF \
--single-transaction \
test_bench user_order > user_order_100w.sql
生成文件:user_order_100w.sql,与 bench.sh 同目录。
五、ProxySQL 配置
1. 主机组配置
bash
+--------------+--------------+-------+--------+--------+-----------------+---------------------+
| hostgroup_id | hostname | port | status | weight | max_connections | max_replication_lag |
+--------------+--------------+-------+--------+--------+-----------------+---------------------+
| 1 | 10.252.1.162 | 18251 | ONLINE | 1 | 1000 | 0 |
| 2 | 10.252.1.162 | 18251 | ONLINE | 1 | 1000 | 0 |
| 2 | 10.252.1.163 | 18251 | ONLINE | 1 | 1000 | 0 |
| 2 | 10.252.1.168 | 18251 | ONLINE | 1 | 1000 | 0 |
+--------------+--------------+-------+--------+--------+-----------------+---------------------+
2. 路由规则
bash
+---------+--------+-----------------------------+-----------------------+
| rule_id | active | match_digest | destination_hostgroup |
+---------+--------+-----------------------------+-----------------------+
| 1 | 1 | ^INSERT | 1 |
| 2 | 1 | ^UPDATE | 1 |
| 3 | 1 | ^DELETE | 1 |
| 4 | 1 | ^REPLACE | 1 |
| 5 | 1 | ^MERGE | 1 |
| 10 | 1 | ^CREATE | 1 |
| 11 | 1 | ^ALTER | 1 |
| 12 | 1 | ^DROP | 1 |
| 13 | 1 | ^TRUNCATE | 1 |
| 14 | 1 | ^RENAME | 1 |
| 15 | 1 | ^GRANT | 1 |
| 16 | 1 | ^REVOKE | 1 |
| 20 | 1 | ^SELECT.*FOR UPDATE | 1 |
| 21 | 1 | ^SELECT.*LOCK IN SHARE MODE | 1 |
| 100 | 1 | ^SELECT | 2 |
+---------+--------+-----------------------------+-----------------------+
六、测试脚本 bench.sh 思路、设计与实现
1. 核心前提
本测试脚本直接在 MySQL 主库所在主机上运行。
- 压测客户端 = 主库本机
- CPU/IO 监控数据 = 主库真实资源使用率
- 无网络损耗、无跨机干扰,数据最真实、最严谨
2. 设计目标
- 模拟生产真实短事务:单语句自动提交,保证 ProxySQL 读写分离生效。
- 固定总请求量:总 SQL 数 = 并发数 × 2500,确保对比公平。
- 长连接复用:每个并发只建立一次连接,执行一批 SQL,避免连接抖动。
- 双进程独立监控:CPU、IO 监控与压测逻辑完全隔离,不干扰压测。
- 环境绝对干净:每次压测自动重建库、导入 100 万数据、ANALYZE。
- 自动统计输出:自动计算平均 CPU、平均 IO、峰值、耗时。
3. 整体架构(三进程模型)
- 主进程:参数解析 → 环境初始化 → 启动监控 → 并发压测 → 结果计算。
- CPU 监控进程:0.5s 高频采样,记录 us+sy 总使用率。
- IO 监控进程:1s 采样,采集磁盘 dm-0 的 %util 指标。
4. 执行流程
- 接收参数:master(直连)/ proxysql、并发数。
- 初始化环境:DROP/CREATE DATABASE → 导入 100 万数据 → ANALYZE。
- 启动 CPU、IO 独立后台监控进程。
- 开启 N 个并发任务:每个任务先生成 2500 条随机 SQL;通过单个长连接一次性批量执行。
- 等待所有并发完成。
- 停止监控 → 计算平均值 → 输出结果 → 清理临时文件。
5. 关键设计
- 本机压测:消除网络延迟,只对比数据库与代理本身性能。
- 批量 SQL + 长连接:完全贴合生产 JDBC / 连接池使用模型。
- 独立监控:监控不抢占压测资源,数据准确。
- 无侵入、无残留:执行完自动清理,可重复、可批量运行。
6. 完整测试脚本 bench.sh
bash
#!/bin/bash
# 最终稳定版 - 双进程监控,确保IO数据准确
# 用法:./bench.sh master 10
# 用法:./bench.sh proxysql 10
if [ $# -ne 2 ]; then
echo "用法:$0 <master|proxysql> <并发数>"
exit 1
fi
MODE="$1"
CONCUR="$2"
USER="wxy"
PASS="123456"
DB="test_bench"
DUMP_FILE="./user_order_100w.sql"
LOOP=2500
if [ "$MODE" = "master" ]; then
HOST="10.252.1.162"
PORT="18251"
else
HOST="127.0.0.1"
PORT="6033"
fi
echo "====================================="
echo "初始化:重建库并导入100w数据"
echo "====================================="
mysql -h$HOST -P$PORT -u$USER -p$PASS -e "DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB;" >/dev/null 2>&1
mysql -h$HOST -P$PORT -u$USER -p$PASS $DB < $DUMP_FILE >/dev/null 2>&1
mysql -h$HOST -P$PORT -u$USER -p$PASS -D $DB -e "ANALYZE TABLE user_order;" >/dev/null 2>&1
echo "压测模式: $MODE 并发: $CONCUR"
echo "比例: SELECT80% INSERT10% UPDATE9% DELETE1%"
echo "====================================="
# 创建临时文件
CPU_FILE=$(mktemp)
IO_FILE=$(mktemp)
# ============================================
# CPU监控进程(高频采样,0.5秒间隔)
# ============================================
cpu_monitor() {
while true; do
cpu_line=$(top -bn1 | grep "Cpu(s)" | head -1)
us=$(echo "$cpu_line" | awk '{print $2}' | cut -d'%' -f1)
sy=$(echo "$cpu_line" | awk '{print $4}' | cut -d'%' -f1)
if [[ "$us" =~ ^[0-9.]+$ ]] && [[ "$sy" =~ ^[0-9.]+$ ]]; then
cpu_usage=$(echo "$us + $sy" | bc)
echo "$cpu_usage" >> $CPU_FILE
fi
sleep 0.5
done
}
# ============================================
# IO监控进程(低频准确采样,2秒间隔)
# ============================================
io_monitor() {
while true; do
# 使用2秒采样周期,取最后一次结果(最准确)
io_util=$(iostat -x 1 2 | grep "dm-0" | tail -1 | awk '{print $NF}')
if [[ "$io_util" =~ ^[0-9.]+$ ]]; then
echo "$io_util" >> $IO_FILE
fi
sleep 1
done
}
# 启动两个独立的监控进程
cpu_monitor &
CPU_PID=$!
io_monitor &
IO_PID=$!
echo "监控已启动 (CPU-PID: $CPU_PID, IO-PID: $IO_PID)"
sleep 2
# ============================================
# 开始压测
# ============================================
START=$(date +%s.%N)
# 记录所有子进程PID
declare -a pids=()
# 启动并发任务
for ((i=0; i<CONCUR; i++)); do
(
sql_batch=""
for ((k=0; k<LOOP; k++)); do
rnd=$((RANDOM % 100))
if [ $rnd -lt 80 ]; then
sql_batch+="SELECT id,user_id,order_no,status,create_time FROM user_order WHERE id > FLOOR(RAND()*1000000) LIMIT 1;"$'\n'
elif [ $rnd -lt 90 ]; then
nid=$((1000000 + RANDOM%1000000))
sql_batch+="INSERT IGNORE INTO user_order(id,user_id,order_no,status,create_time) VALUES($nid,$nid,CONCAT('ORD_',$nid),1,NOW());"$'\n'
elif [ $rnd -lt 99 ]; then
uid=$((1 + RANDOM%1000000))
sql_batch+="UPDATE user_order SET status=status+1 WHERE id=$uid;"$'\n'
else
did=$((1 + RANDOM%1000000))
sql_batch+="DELETE FROM user_order WHERE id=$did;"$'\n'
fi
done
echo "$sql_batch" | mysql -h$HOST -P$PORT -u$USER -p$PASS -D $DB -s -N 2>/dev/null 1>/dev/null
) &
pids+=($!)
done
# 等待所有压测进程完成
echo "等待 $CONCUR 个并发任务完成..."
for pid in "${pids[@]}"; do
wait $pid 2>/dev/null
done
END=$(date +%s.%N)
cost=$(echo "scale=3; $END - $START" | bc)
# ============================================
# 停止监控进程
# ============================================
kill $CPU_PID $IO_PID 2>/dev/null
sleep 1
# ============================================
# 计算平均值
# ============================================
avg_cpu=$(awk '{sum+=$1; count++} END {if(count>0) printf "%.2f", sum/count}' $CPU_FILE 2>/dev/null)
avg_io=$(awk '{sum+=$1; count++} END {if(count>0) printf "%.2f", sum/count}' $IO_FILE 2>/dev/null)
cpu_samples=$(wc -l < $CPU_FILE 2>/dev/null | tr -d ' ')
io_samples=$(wc -l < $IO_FILE 2>/dev/null | tr -d ' ')
# 可选:输出最大值和最小值用于参考
max_cpu=$(awk 'BEGIN{max=0}{if($1>max) max=$1}END{printf "%.2f", max}' $CPU_FILE 2>/dev/null)
max_io=$(awk 'BEGIN{max=0}{if($1>max) max=$1}END{printf "%.2f", max}' $IO_FILE 2>/dev/null)
# 清理临时文件
rm -f $CPU_FILE $IO_FILE
# ============================================
# 输出结果
# ============================================
echo -e "\n✅ 测试完成"
echo "====================================="
echo "模式:$MODE"
echo "并发数:$CONCUR"
echo "总耗时:$cost 秒"
echo "====================================="
echo "CPU统计:"
echo " 采样次数:$cpu_samples 次"
echo " 平均值:${avg_cpu}%"
echo " 峰值:${max_cpu}%"
echo "====================================="
echo "IO统计 (/data 磁盘):"
echo " 采样次数:$io_samples 次"
echo " 平均值:${avg_io}%"
echo " 峰值:${max_io}%"
echo "====================================="
七、测试执行命令
bash
./bench.sh master 10
./bench.sh proxysql 10
./bench.sh master 20
./bench.sh proxysql 20
./bench.sh master 40
./bench.sh proxysql 40
./bench.sh master 80
./bench.sh proxysql 80
./bench.sh master 160
./bench.sh proxysql 160
八、实测结果说明
1. 实测数据汇总表
| 并发 | 连接方式 | 总耗时 (s) | 总请求数 | QPS (约) | 主库 CPU 平均使用率 | 主库 IO % util 平均值 | 性能提升 |
|---|---|---|---|---|---|---|---|
| 10 | 直连 master | 2.90 | 25000 | 8620 | 56.84% | 4.50% | --- |
| 10 | ProxySQL | 1.55 | 25000 | 16120 | 30.00% | 4.85% | 1.87 倍 |
| 20 | 直连 master | 5.56 | 50000 | 8990 | 72.90% | 3.33% | --- |
| 20 | ProxySQL | 2.80 | 50000 | 17850 | 35.66% | 3.85% | 1.99 倍 |
| 40 | 直连 master | 11.35 | 100000 | 8810 | 76.10% | 3.07% | --- |
| 40 | ProxySQL | 5.74 | 100000 | 17420 | 39.54% | 1.85% | 1.98 倍 |
| 80 | 直连 master | 22.75 | 200000 | 8790 | 83.10% | 2.22% | --- |
| 80 | ProxySQL | 11.33 | 200000 | 17650 | 45.51% | 1.54% | 2.01 倍 |
| 160 | 直连 master | 44.29 | 400000 | 9030 | 87.06% | 1.72% | --- |
| 160 | ProxySQL | 22.06 | 400000 | 18130 | 46.62% | 2.29% | 2.01 倍 |
2. 测试结果说明
(1)性能提升
- 直连主库:QPS 上限约 9000,CPU 成为瓶颈
- ProxySQL 架构:QPS 稳定 1.7~1.8 万
- 整体性能提升 ≈ 100%(2 倍)
(2)主库 CPU 负载
- 直连主库:CPU 随并发飙升,最高达 87.06%
- ProxySQL:CPU 始终平稳,最高仅 46.62%
- 读写分离后,主库 CPU 平均降低 45%~50%
(3)主库磁盘 IO
- 所有场景 IO % util 均处于极低水平。
- ProxySQL 未增加主库磁盘压力。
- 读 IO 已完全转移至读库集群。
(4)稳定性与扩展性
- 并发越高,ProxySQL 优势越明显。
- 主库资源消耗大幅下降,系统稳定性显著提升。
九、最终总结
- 测试脚本运行于主库本机,无网络干扰,所有 CPU/IO 数据为主库真实指标。
- 自定义短事务脚本是唯一能让 ProxySQL 读写分离完整生效的压测方案。
- 建库 → 建表 → 数据初始化 → 自动导入,全套可复现。
- ProxySQL 可使系统性能提升 100%,主库 CPU 降低近 50%。
- 整套方案可复现、可核对。