强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

MySQL 完全指南 / 第 20 章:性能测试

第 20 章:性能测试

不做性能测试就上线,等于裸奔上高速。本章详解 MySQL 压测方法论和工具。


20.1 压测方法论

20.1.1 压测目标

指标说明目标
QPS每秒查询数根据业务需求定
TPS每秒事务数根据业务需求定
响应时间P99 < 100ms用户体验
并发数系统能承受的并发连接预期峰值的 1.5-2 倍
错误率< 0.1%稳定性

20.1.2 压测步骤

1. 明确目标:QPS/TPS/响应时间
         ↓
2. 准备环境:与生产环境一致的硬件和配置
         ↓
3. 准备数据:与生产数据量级一致
         ↓
4. 预热:先跑一段时间让 Buffer Pool 预热
         ↓
5. 梯度压测:逐步增加并发,观察性能拐点
         ↓
6. 持续压测:在目标并发下持续运行 30 分钟以上
         ↓
7. 分析结果:找出瓶颈和优化点

⚠️ 压测环境要求

  • 硬件配置与生产一致(或等比例缩小)
  • 数据量与生产一致(或等比例缩小)
  • 不要在生产环境压测!
  • 压测期间监控系统资源

20.2 sysbench

20.2.1 安装

# Ubuntu/Debian
sudo apt install sysbench

# CentOS/RHEL
sudo yum install sysbench

# Docker
docker pull severalnines/sysbench

20.2.2 内置测试用例

# ===================== OLTP 读写混合测试 =====================
# 准备数据(10 张表,每张 10 万行)
sysbench oltp_read_write \
  --mysql-host=127.0.0.1 \
  --mysql-port=3306 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --tables=10 \
  --table-size=100000 \
  prepare

# 运行测试(32 线程,300 秒)
sysbench oltp_read_write \
  --mysql-host=127.0.0.1 \
  --mysql-port=3306 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --tables=10 \
  --table-size=100000 \
  --threads=32 \
  --time=300 \
  --report-interval=10 \
  run

# 清理测试数据
sysbench oltp_read_write \
  --mysql-host=127.0.0.1 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --tables=10 \
  cleanup

# ===================== 只读测试 =====================
sysbench oltp_read_only \
  --mysql-host=127.0.0.1 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --threads=64 \
  --time=300 \
  run

# ===================== 写入测试 =====================
sysbench oltp_write_only \
  --mysql-host=127.0.0.1 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --threads=32 \
  --time=300 \
  run

# ===================== 点查测试 =====================
sysbench oltp_point_select \
  --mysql-host=127.0.0.1 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --threads=128 \
  --time=300 \
  run

20.2.3 自定义 Lua 脚本

-- custom_oltp.lua
pathtest = string.match("(.-)([^/\\]+%.lua)$", debug.getinfo(1, "S").source:sub(2))

function prepare()
    local drv = sysbench.sql.driver()
    local con = drv:connect()
    
    con:query([[
        CREATE TABLE IF NOT EXISTS orders (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            total_amount DECIMAL(12,2) NOT NULL,
            status TINYINT NOT NULL DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_user_id (user_id),
            INDEX idx_status (status)
        ) ENGINE=InnoDB
    ]])
    
    for i = 1, sysbench.opt.table_size do
        con:query(string.format(
            "INSERT INTO orders (user_id, total_amount, status) VALUES (%d, %.2f, %d)",
            sb_rand(1, 100000), sb_rand(10, 10000), sb_rand(0, 5)
        ))
    end
end

function event()
    local user_id = sb_rand(1, 100000)
    
    -- 读操作(70%)
    if sb_rand(1, 100) <= 70 then
        db_query(string.format(
            "SELECT * FROM orders WHERE user_id = %d ORDER BY created_at DESC LIMIT 10",
            user_id
        ))
    -- 写操作(30%)
    else
        db_query(string.format(
            "INSERT INTO orders (user_id, total_amount, status) VALUES (%d, %.2f, 0)",
            user_id, sb_rand(10, 1000)
        ))
    end
end
sysbench custom_oltp \
  --mysql-host=127.0.0.1 \
  --mysql-user=root \
  --mysql-password='MyPass' \
  --mysql-db=sbtest \
  --threads=32 \
  --time=300 \
  --table-size=100000 \
  run

20.2.4 输出结果解读

SQL statistics:
    queries performed:
        read:                            567890     -- 读查询总数
        write:                           123456     -- 写查询总数
        other:                           45678      -- 其他查询
        total:                           737024     -- 总查询数
    transactions:                        56789 (1893.02 per sec.)  -- TPS
    queries:                             737024 (24567.33 per sec.) -- QPS
    ignored errors:                      0      (0.00 per sec.)
    reconnects:                          0      (0.00 per sec.)

General statistics:
    total time:                          30.0015s    -- 总耗时
    total number of events:              56789       -- 事件总数

Latency (ms):
         min:                                    0.51  -- 最小延迟
         avg:                                    1.69  -- 平均延迟
         max:                                   45.23  -- 最大延迟
         95th percentile:                        3.55  -- P95 延迟
         sum:                                95892.34  -- 总延迟

20.3 mysqlslap

# 基本用法
mysqlslap -uroot -p \
  --concurrency=50 \
  --iterations=5 \
  --auto-generate-sql \
  --auto-generate-sql-add-autoincrement \
  --auto-generate-sql-load-type=mixed \
  --auto-generate-sql-write-number=1000 \
  --number-of-queries=100000

# 使用自定义 SQL
mysqlslap -uroot -p \
  --concurrency=100 \
  --iterations=3 \
  --query="SELECT * FROM myapp.users WHERE id = FLOOR(RAND() * 100000)" \
  --number-of-queries=100000

# 创建测试表并压测
mysqlslap -uroot -p \
  --concurrency=50 \
  --iterations=3 \
  --create-schema=sbtest \
  --query="INSERT INTO sbtest.test (name, value) VALUES ('test', RAND())" \
  --number-of-queries=50000

20.4 梯度压测脚本

#!/bin/bash
# benchmark_gradient.sh - 梯度压测脚本

HOST="127.0.0.1"
USER="root"
PASS="MyPass"
DB="sbtest"
TABLES=10
TABLE_SIZE=100000
DURATION=300
RESULT_DIR="./benchmark_results"

mkdir -p ${RESULT_DIR}

# 准备数据
sysbench oltp_read_write \
  --mysql-host=${HOST} --mysql-user=${USER} --mysql-password=${PASS} \
  --mysql-db=${DB} --tables=${TABLES} --table-size=${TABLE_SIZE} prepare

# 梯度压测:8, 16, 32, 64, 128, 256 线程
for THREADS in 8 16 32 64 128 256; do
    echo "=========================================="
    echo "Testing with ${THREADS} threads..."
    echo "=========================================="
    
    sysbench oltp_read_write \
      --mysql-host=${HOST} --mysql-user=${USER} --mysql-password=${PASS} \
      --mysql-db=${DB} --tables=${TABLES} --table-size=${TABLE_SIZE} \
      --threads=${THREADS} --time=${DURATION} \
      --report-interval=10 \
      run > ${RESULT_DIR}/result_${THREADS}_threads.txt 2>&1
    
    sleep 10  # 等待系统恢复
done

# 汇总结果
echo ""
echo "============ 结果汇总 ============"
for THREADS in 8 16 32 64 128 256; do
    TPS=$(grep "transactions:" ${RESULT_DIR}/result_${THREADS}_threads.txt | awk '{print $2}' | sed 's/(//')
    QPS=$(grep "queries:" ${RESULT_DIR}/result_${THREADS}_threads.txt | head -1 | awk '{print $2}' | sed 's/(//')
    P95=$(grep "95th percentile" ${RESULT_DIR}/result_${THREADS}_threads.txt | awk '{print $NF}')
    echo "Threads: ${THREADS} | TPS: ${TPS} | QPS: ${QPS} | P95: ${P95}ms"
done

20.5 压测监控

# 压测期间同时监控

# 系统资源
vmstat 1                    # CPU、内存、IO
iostat -x 1                 # 磁盘 I/O
top -p $(pidof mysqld)      # MySQL 进程

# MySQL 状态
watch -n 1 'mysqladmin -uroot -p"MyPass" status extended-status | grep -E "Queries|Threads|Innodb"'

# 慢查询
tail -f /var/log/mysql/slow.log

20.6 性能对比测试

# 对比不同配置的性能
# 例如:对比 innodb_flush_log_at_trx_commit = 1 vs 2

# 配置 1(安全模式)
mysql -uroot -p -e "SET GLOBAL innodb_flush_log_at_trx_commit = 1; SET GLOBAL sync_binlog = 1;"
sysbench oltp_write_only --threads=64 --time=300 run

# 配置 2(性能模式)
mysql -uroot -p -e "SET GLOBAL innodb_flush_log_at_trx_commit = 2; SET GLOBAL sync_binlog = 0;"
sysbench oltp_write_only --threads=64 --time=300 run

业务场景

场景 1:上线前性能验证

1. 用 prod 相同配置搭建测试环境
2. 导入 prod 的数据快照
3. 梯度压测,找到性能拐点
4. 在目标并发下持续压测 30 分钟
5. 监控 CPU、内存、磁盘 I/O、MySQL 状态
6. 确认 P99 响应时间满足 SLA
7. 记录基线数据,后续对比

扩展阅读