数据库性能测试实战——慢 SQL 压测、连接池压测、MySQL 性能基准
数据库性能测试实战——慢 SQL 压测、连接池压测、MySQL 性能基准
适读人群:后端开发工程师、DBA、数据库性能优化工程师 | 阅读时长:约15分钟 | 核心价值:掌握针对数据库层的专项压测方法,学会从压测结果定位慢SQL、连接池瓶颈和MySQL配置问题
数据库成为瓶颈的那个夜晚
2021年某个业务快速增长的季度,我们的日均订单量从100万增长到500万。系统其他层面的扩容都很顺利,但数据库成了越来越明显的瓶颈。
订单查询接口的P99从120ms涨到了890ms,用户投诉"查不到刚刚下的单"。
当时我排查的第一反应是"加Redis缓存",但DBA老王拦住了我:
"先看慢查询日志,别急着加缓存。缓存是你不得已的最后手段,不是排查问题的第一步。"
他打开MySQL慢查询日志,五秒钟就发现了问题:有一条SQL的执行时间超过了3秒:
SELECT * FROM orders
WHERE user_id = ? AND status IN (1,2,3)
ORDER BY create_time DESC
LIMIT 20用EXPLAIN一看,走的是全表扫描,没有走到(user_id, status)的联合索引。
原来是前两周有人改了这个字段的类型,user_id在表里是bigint,但代码里传参变成了varchar,MySQL因为类型不匹配做了隐式转换,导致索引失效。
这一个索引问题,拖垮了整个订单查询链路。
数据库压测的目标
数据库压测不是简单地"对DB发大量查询",而是有明确的测试目标:
- 慢SQL压测:找出在高并发下哪些SQL成为瓶颈
- 连接池压测:找到合理的连接池大小配置
- MySQL性能基准:测试当前硬件配置下MySQL的最大读写能力
- 锁竞争测试:测试高并发写入时的锁等待情况
工具准备
sysbench——MySQL基准测试标准工具
# 安装(macOS)
brew install sysbench
# 安装(Ubuntu)
curl -s https://packagecloud.io/install/repositories/akopytov/sysbench/script.deb.sh | sudo bash
sudo apt-get install sysbench
# 验证
sysbench --versionmysqlslap——MySQL自带压测工具
# 直接用,MySQL自带,不需要安装
mysqlslap --helpMySQL 基准测试
sysbench OLTP基准测试
这是业界标准的MySQL性能基准测试流程:
# 步骤1:准备测试数据
sysbench \
--db-driver=mysql \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=your_password \
--mysql-db=sbtest \
--table_size=1000000 \ # 每张表100万行
--tables=10 \ # 10张表
--threads=1 \
oltp_read_write prepare
# 步骤2:只读测试(模拟读多写少场景)
sysbench \
--db-driver=mysql \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=your_password \
--mysql-db=sbtest \
--table_size=1000000 \
--tables=10 \
--threads=64 \ # 64并发
--time=300 \ # 跑5分钟
--report-interval=10 \ # 每10秒输出一次
oltp_read_only run
# 步骤3:读写混合测试(更接近真实场景)
sysbench \
--db-driver=mysql \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=your_password \
--mysql-db=sbtest \
--table_size=1000000 \
--tables=10 \
--threads=64 \
--time=300 \
--report-interval=10 \
--oltp-read-write-ratio=5 \ # 读写比5:1
oltp_read_write run
# 步骤4:清理测试数据
sysbench ... oltp_read_write cleanupsysbench 结果解读
SQL statistics:
queries performed:
read: 2809056
write: 802588
other: 401294
total: 4012938
transactions: 200647 (668.68 per sec.) ← 这是TPS
queries: 4012938 (13377.5 per sec.) ← QPS
ignored errors: 0 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
Throughput:
events/s (eps): 668.6820
time elapsed: 300.0731s
total number of events: 200647
Latency (ms):
min: 4.43
avg: 95.68
max: 3242.13
95th percentile: 186.54 ← P95
sum: 19200000.00基准数字参考(不同硬件差异很大,仅供参考):
| 场景 | 普通SSD(HDD时代) | NVMe SSD | 云数据库(RDS) |
|---|---|---|---|
| 只读TPS(64并发) | ~2000 | ~8000 | ~5000-15000 |
| 读写TPS(64并发) | ~500 | ~2000 | ~1000-5000 |
| P95(只读) | ~30ms | ~8ms | ~10-30ms |
慢 SQL 压测
开启慢查询日志
-- 开启慢查询日志(生产环境需要谨慎,有性能开销)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1; -- 超过100ms记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录不用索引的查询压测期间捕获慢SQL
# 压测脚本同时开启慢查询日志监控
import subprocess
import time
def monitor_slow_queries(slow_log_path: str, interval: int = 10):
"""每10秒统计一次新增的慢查询"""
last_size = 0
while True:
current_size = os.path.getsize(slow_log_path)
if current_size > last_size:
# 读取新增内容
with open(slow_log_path, 'r') as f:
f.seek(last_size)
new_content = f.read()
# 统计慢查询数量
query_count = new_content.count('# Query_time')
print(f"[{time.strftime('%H:%M:%S')}] 新增慢查询: {query_count} 条")
last_size = current_size
time.sleep(interval)pt-query-digest 分析慢查询
# 安装percona-toolkit
sudo apt-get install percona-toolkit
# 分析慢查询日志,输出Top 10最慢的查询
pt-query-digest /var/log/mysql/slow.log \
--limit 10 \
--output report \
> slow_query_report.txt
# 输出示例:
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============= ====== ====== ===== ===========
# 1 0xDEADBEEF... 120.3456 28.0% 1234 0.0975 0.12 SELECT orders
# 2 0xCAFEBABE... 89.2341 20.8% 567 0.1574 0.34 SELECT inventory连接池压测
测试不同连接池大小对性能的影响
# connection_pool_benchmark.py
import concurrent.futures
import time
import mysql.connector
from mysql.connector import pooling
def run_query_with_pool(pool, query_count: int) -> dict:
"""在给定连接池下执行指定数量的查询"""
errors = 0
latencies = []
def single_query():
try:
conn = pool.get_connection()
cursor = conn.cursor()
start = time.time()
cursor.execute(
"SELECT * FROM orders WHERE user_id = %s ORDER BY create_time DESC LIMIT 20",
(random.randint(10001, 110000),)
)
cursor.fetchall()
latency = (time.time() - start) * 1000
cursor.close()
conn.close()
return latency, True
except Exception as e:
return 0, False
with concurrent.futures.ThreadPoolExecutor(max_workers=200) as executor:
futures = [executor.submit(single_query) for _ in range(query_count)]
for future in concurrent.futures.as_completed(futures):
latency, success = future.result()
if success:
latencies.append(latency)
else:
errors += 1
if latencies:
latencies.sort()
p99_index = int(len(latencies) * 0.99)
return {
'p50': latencies[len(latencies)//2],
'p99': latencies[p99_index],
'error_rate': errors / query_count * 100,
'tps': query_count / (sum(latencies) / 1000 / len(latencies) / 200)
}
# 测试不同连接池大小
import random
for pool_size in [5, 10, 20, 50, 100, 200]:
pool = pooling.MySQLConnectionPool(
pool_name=f"pool_{pool_size}",
pool_size=pool_size,
host="127.0.0.1",
database="testdb",
user="root",
password="your_password"
)
result = run_query_with_pool(pool, 10000)
print(f"Pool size={pool_size:3d}: P50={result['p50']:.1f}ms, "
f"P99={result['p99']:.1f}ms, "
f"Error={result['error_rate']:.2f}%")典型的测试结果:
| 连接池大小 | P50 | P99 | 错误率 |
|---|---|---|---|
| 5 | 45ms | 8900ms | 12% |
| 10 | 42ms | 2100ms | 2% |
| 20 | 40ms | 380ms | 0% |
| 50 | 38ms | 245ms | 0% |
| 100 | 37ms | 230ms | 0% |
| 200 | 95ms | 450ms | 0% |
从数据可以看出:连接池从5增加到20时,P99从8900ms降到380ms。从20增加到100改善不大。200时反而有轻微下降,因为MySQL本身处理200个并发连接也有开销。
最优连接池大小大约在20-50之间,具体值要根据业务查询的复杂度和MySQL服务器的配置来调整。
MySQL 关键配置调优
压测过程中,同时监控这几个MySQL关键指标:
-- 实时监控
SHOW STATUS LIKE 'Threads_running'; -- 当前活跃线程数
SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数
SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests'; -- Buffer Pool读请求
SHOW STATUS LIKE 'Innodb_buffer_pool_reads'; -- Buffer Pool未命中(物理读)
-- Buffer Pool命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)
-- 低于95%说明Buffer Pool太小innodb_buffer_pool_size
最重要的MySQL配置参数。建议设为服务器内存的60-80%:
# my.cnf
[mysqld]
innodb_buffer_pool_size = 12G # 16G内存的服务器,设12G
innodb_buffer_pool_instances = 8 # 分8个实例减少锁竞争踩坑实录
坑1:压测数据分布不均匀导致热点行锁竞争
现象: 库存扣减接口在50并发时P99就超过了5000ms,数据库CPU只有20%。
原因: 压测脚本里所有请求都对同一个商品(productId=1001)扣减库存。MySQL的行锁导致所有请求排队,P99等于并发数×单次操作时间。
解法: 压测脚本里使用随机的productId,模拟真实的分散流量:
product_id = random.randint(1001, 11000) # 10000个不同商品坑2:sysbench基准测试没有Warm Up导致基准偏低
现象: sysbench跑出来的TPS比预期低很多,而且前30秒数字特别低。
原因: MySQL的Buffer Pool是冷的,测试开始时全是物理IO。没有预热阶段,前30秒的IO密集操作拉低了整体均值。
解法: 加上--warmup-time=60参数,先预热60秒再开始统计:
sysbench oltp_read_only \
--warmup-time=60 \
--time=300 \
... run坑3:压测后忘记关闭慢查询日志导致生产性能下降
现象: 某次在预发环境开启了慢查询日志做压测,压测完忘记关闭。预发环境继续正常使用,慢查询日志文件几天后增长到了50GB,磁盘写满,导致MySQL服务崩溃。
原因: 慢查询日志会有一定的磁盘写入开销,而且日志文件会持续增长。
解法: 压测结束后立即关闭:
SET GLOBAL slow_query_log = 'OFF';并且在脚本里加上清理步骤,确保无论测试成功还是失败都会执行关闭操作。
总结
数据库压测三件套:sysbench测MySQL基线能力,慢查询日志+pt-query-digest定位慢SQL,连接池压测找最优配置。
记住:数据库性能问题的前三大原因是慢SQL(索引问题)、连接池配置不合理、Buffer Pool太小。这三个问题覆盖了80%的数据库性能故障场景。
