数据库性能压测工具sysbench介绍
Tags: MySQLPGsysbench工具工具介绍性能压测
简介
sysbench是一个开源的、基于LuaJIT(LuaJIT 是 Lua 的即时编译器,可将代码直接翻译成机器码,性能比原生 lua 要高) 的、可自定义脚本的多线程基准测试工具,也是目前用得最多的 MySQL 性能压测工具。
基于 sysbench,我们可以对比 MySQL 在不同版本、不同硬件配置、不同参数(操作系统和数据库)下的性能差异。
面会从 sysbench 的基本用法出发,逐渐延伸到 sysbench 的一些高级玩法,譬如如何阅读自带的测试脚本、如何自定义测试项等。除此之外,使用 sysbench 对 CPU 进行测试,网上很多资料都语焉不详,甚至是错误的,所以这次也会从源码的角度分析 CPU 测试的实现逻辑及 --cpu-max-prime 选项的具体含义。
安装 sysbench
yum直接安装:
1 | yum install -y sysbench |
下面是 sysbench 源码包的安装步骤。
1 2 3 4 5 6 7 8 9 | yum -y install make automake libtool pkgconfig libaio-devel openssl-devel mysql-devel cd /usr/src/ wget https://github.com/akopytov/sysbench/archive/refs/tags/1.0.20.tar.gz tar xvf 1.0.20.tar.gz cd sysbench-1.0.20/ ./autogen.sh ./configure make -j make install |
安装完成后,压测脚本默认会安装在 /usr/local/share/sysbench
目录下。
我们看看该目录的内容。
1 2 3 4 | # ls /usr/local/share/sysbench/ bulk_insert.lua oltp_insert.lua oltp_read_write.lua oltp_write_only.lua tests oltp_common.lua oltp_point_select.lua oltp_update_index.lua select_random_points.lua oltp_delete.lua oltp_read_only.lua oltp_update_non_index.lua select_random_ranges.lua |
除了oltp_common.lua
是个公共模块,其它每个 lua 脚本都对应一个测试场景。
sysbench 用法讲解
sysbench 命令语法如下:
1 | sysbench [options]... [testname] [command] |
命令中的testname
是测试项名称。sysbench 支持的测试项包括:
- *.lua:数据库性能基准测试。
- fileio:磁盘 IO 基准测试。
- cpu:CPU 性能基准测试。
- memory:内存访问基准测试。
- threads:基于线程的调度程序基准测试。
- mutex:POSIX 互斥量基准测试。
command
是 sysbench 要执行的命令,支持的选项有:prepare
,prewarm
,run
,cleanup
,help
。注意,不是所有的测试项都支持这些选项。
options
是配置项。sysbench 中的配置项主要包括以下两部分:
- 通用配置项。这部分配置项可通过
sysbench --help
查看。例如,
1 2 3 4 5 6 7 | ## sysbench --help ... General options: --threads=N number of threads to use [1] --events=N limit for total number of events [0] --time=N limit for total execution time in seconds [10] ... |
- 测试项相关的配置项。各个测试项支持的配置项可通过
sysbench testname help
查看。例如,
1 2 3 4 5 6 7 8 9 10 | ## sysbench memory help sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2) memory options: --memory-block-size=SIZE size of memory block for test [1K] --memory-total-size=SIZE total size of data to transfer [100G] --memory-scope=STRING memory access scope {global,local} [global] --memory-hugetlb[=on|off] allocate memory from HugeTLB pool [off] --memory-oper=STRING type of memory operations {read, write, none} [write] --memory-access-mode=STRING memory access mode {seq,rnd} [seq] |
对 MySQL 进行基准测试的基本步骤
下面以oltp_read_write
为例,看看使用 sysbench 对 MySQL 进行基准测试的四个标准步骤:
prepare
生成压测数据。默认情况下,sysbench 是通过 INSERT INTO 命令来导入测试数据的。如果是使用 LOAD DATA LOCAL INFILE 命令来导入,sysbench 导数速度能提升30%,具体可参考:使用 LOAD DATA LOCAL INFILE,sysbench 导数速度提升30%
1 | sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=30 prepare |
命令中各个选项的具体含义如下:
- oltp_read_write:测试项,对应的是
/usr/local/share/sysbench/oltp_read_write.lua
。这里也可指定脚本的绝对路径名。 - --mysql-host、--mysql-port、--mysql-user、--mysql-password:分别代表 MySQL 实例的主机名、端口、用户名和密码。
- --mysql-db:库名。不指定则默认为
sbtest
。 - --tables :表的数量,默认为 1。
- --table-size :单表的大小,默认为 10000。
- --threads :并发线程数,默认为 1。注意,导入时,单表只能使用一个线程。
- prepare:执行准备工作。
oltp_read_write 用来压测 OLTP 场景。在 sysbench 1.0 之前, 该场景是通过 oltp.lua 这个脚本来测试的。不过该脚本在 sysbench 1.0 之后就被废弃了,但为了跟之前的版本兼容,该脚本放到了 /usr/local/share/sysbench/tests/include/oltp_legacy/
目录下。
鉴于 oltp_read_write.lua 和 oltp.lua 两者的压测内容完全一致。从 sysbench 1.0 开始,压测 OLTP 场景建议直接使用 oltp_read_write。
prewarm
预热。主要是将磁盘中的数据加载到内存中。
1 | sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=30 prewarm |
除了需要将命令设置为 prewarm
,其它配置与 prepare
中一样。
run
压测。
1 | sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
其中,
- --time :压测时间。不指定则默认为 10 秒。除了 --time,也可通过 --events 限制需要执行的 event 的数量。
- --report-interval=10 :每 10 秒输出一次测试结果,默认为 0,不输出。
cleanup
清理数据。
1 | sysbench oltp_read_write --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 cleanup |
这里只需指定 --tables ,sysbench 会串行执行 DROP TABLE IF EXISTS sbtest
操作。
如何分析 MySQL 基准测试结果
下面我们分析下 oltp_read_write 场景下的压测结果。注:右滑可以看到每个指标的具体含义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | Threads started! [ 10s ] thds: 64 tps: 5028.08 qps: 100641.26 (r/w/o: 70457.59/20121.51/10062.16) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00 -- thds 是并发线程数。tps 是每秒事务数。qps 是每秒操作数,等于 r(读操作)加上 w(写操作)加上 o(其他操作,主要包括 BEGIN 和 COMMIT)。lat 是延迟,(ms,95%) 是 95% 的查询时间小于或等于该值,单位毫秒。err/s 是每秒错误数。reconn/s 是每秒重试的次数。 [ 20s ] thds: 64 tps: 5108.93 qps: 102192.09 (r/w/o: 71533.28/20440.64/10218.17) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00 [ 30s ] thds: 64 tps: 5126.50 qps: 102505.50 (r/w/o: 71756.30/20496.60/10252.60) lat (ms,95%): 17.32 err/s: 0.00 reconn/s: 0.00 [ 40s ] thds: 64 tps: 5144.50 qps: 102907.20 (r/w/o: 72034.07/20583.72/10289.41) lat (ms,95%): 17.01 err/s: 0.00 reconn/s: 0.00 [ 50s ] thds: 64 tps: 5137.29 qps: 102739.80 (r/w/o: 71916.99/20548.64/10274.17) lat (ms,95%): 17.01 err/s: 0.00 reconn/s: 0.00 [ 60s ] thds: 64 tps: 4995.38 qps: 99896.35 (r/w/o: 69925.98/19979.61/9990.75) lat (ms,95%): 17.95 err/s: 0.00 reconn/s: 0.00 SQL statistics: queries performed: read: 4276622 ## 读操作的数量 write: 1221892 ## 写操作的数量 other: 610946 ## 其它操作的数量 total: 6109460 ## 总的操作数量,total = read + write + other transactions: 305473 (5088.63 per sec.) ## 总的事务数(每秒事务数) queries: 6109460 (101772.64 per sec.) ## 总的操作数(每秒操作数) ignored errors: 0 (0.00 per sec.) ## 忽略的错误数(每秒忽略的错误数) reconnects: 0 (0.00 per sec.) ## 重试次数(每秒重试的次数) General statistics: total time: 60.0301s ## 总的执行时间 total number of events: 305473 ## 执行的 event 的数量 ## 在 oltp_read_write 中,一个 event 其实就是一个事务 Latency (ms): min: 5.81 ## 最小耗时 avg: 12.57 ## 平均耗时 max: 228.87 ## 最大耗时 95th percentile: 17.32 ## 95% event 的执行耗时 sum: 3840044.28 ## 总耗时 Threads fairness: events (avg/stddev): 4773.0156/30.77 ## 平均每个线程执行 event 的数量 ## stddev 是标准差,值越小,代表结果越稳定。 execution time (avg/stddev): 60.0007/0.01 ## 平均每个线程的执行时间 |
输出中,重点关注三个指标:
- 每秒事务数,即我们常说的 TPS。
- 每秒操作数,即我们常说的 QPS。
- 95% event 的执行耗时。
TPS 和 QPS 反映了系统的吞吐量,越大越好。执行耗时代表了事务的执行时长,越小越好。在一定范围内,并发线程数指定得越大,TPS 和 QPS 也会越高。
使用 sysbench 对服务器进行测试
除了数据库基准测试,sysbench 还能对服务器的性能进行测试。服务器资源一般包括四大类:CPU、内存、IO和网络。sysbench 可对CPU、内存和磁盘IO进行测试。下面我们具体来看看。
cpu
CPU 性能测试。支持的选项只有一个,即--cpu-max-prime
。
CPU 测试的命令如下:
1 | # sysbench cpu --cpu-max-prime=20000 --threads=32 run |
输出中,重点关注events per second
。值越大,代表 CPU 的计算性能越强。
1 2 | CPU speed: events per second: 25058.08 |
下面是 CPU 测试相关的代码,可以看到,sysbench 是通过计算--cpu-max-prime
范围内的质数来衡量 CPU 的计算能力的。
质数(prime number)又称素数,指的是大于 1,且只能被 1 和自身整除的自然数。在代码实现时,对于自然数 n,一般会用 2 到根号 n 之间的整数去除,如果都无法整除,则意味着 n 是个质数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int cpu_execute_event(sb_event_t *r, int thread_id) { unsigned long long c; unsigned long long l; double t; unsigned long long n=0; (void)thread_id; /* unused */ (void)r; /* unused */ // max_prime 即命令行中指定的 --cpu-max-prime for(c=3; c < max_prime; c++) { t = sqrt((double)c); for(l = 2; l <= t; l++) if (c % l == 0) break; if (l > t ) n++; } return 0; } |
memory
内存测试,支持的选项有:
- --memory-block-size:内存块的大小,默认为 1KB。测试时建议设置为 1MB。
- --memory-total-size:要传输的数据的总大小。默认为 100GB。
- --memory-scope:内存访问范围,可指定 global、local,默认为 global。
- --memory-hugetlb:是否从 HugeTLB 池中分配内存,默认为 off。
- --memory-oper:内存操作类型,可指定 read、write、none,默认为 write。
- --memory-access-mode:内存访问模式,可指定 seq(顺序访问)、rnd(随机访问),默认为 seq。
内存测试的命令如下:
1 | # sysbench --test=memory --memory-block-size=1M --memory-total-size=100G --num-threads=1 run |
输出中,重点关注以下部分:
1 | 102400.00 MiB transferred (23335.96 MiB/sec) |
23335.96 MiB/sec 即数据在内存中的顺序写入速率。
fileio
磁盘 IO 测试。支持的选项有:
- --file-num:需要创建的文件数,默认为128。
- --file-block-size:数据块的大小,默认为16384,即16KB。
- --file-total-size:需要创建的文件总大小,默认为2GB。
- --file-test-mode:测试模式,可指定 seqwr(顺序写)、seqrewr(顺序重写)、seqrd(顺序读)、rndrd(随机读)、rndwr(随机写)、rndrw(随机读写)。
- --file-io-mode:文件的操作模式,可指定 sync(同步 IO)、async(异步 IO)、mmap,默认为 sync。
- --file-async-backlog:每个线程异步 IO 队列的长度,默认为 128。
- --file-extra-flags:打开文件时指定的标志,可指定 sync、dsync、direct,默认为空,没指定。
- --file-fsync-freq:指定持久化操作的频率,默认为 100,即每执行 100 个 IO 请求,则会进行一次持久化操作。
- --file-fsync-all:在每次写入操作后执行持久化操作,默认为 off。
- --file-fsync-end:在测试结束时执行持久化操作,默认为 on。
- --file-fsync-mode:持久化操作的模式,可指定 fsync、fdatasync,默认为 fsync。fdatasync 和 fsync类似,只不过 fdatasync 只会更新数据,而 fsync 还会同步更新文件的属性。
- --file-merged-requests:允许合并的最多 IO 请求数,默认为0,不合并。
- --file-rw-ratio:混合测试中的读写比例,默认为1.5。
磁盘 IO 测试主要分为以下三步:
1 2 3 4 5 6 7 8 | -- 准备测试文件 sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw prepare -- 测试 sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw run -- 删除测试文件 sysbench fileio --file-num=1 --file-total-size=10G --file-test-mode=rndrw cleanup |
输出中,重点关注以下两部分:
1 2 3 4 5 6 7 8 | File operations: reads/s: 4978.26 writes/s: 3318.84 fsyncs/s: 83.07 Throughput: read, MiB/s: 77.79 written, MiB/s: 51.86 |
其中,reads/s 加上 writes/s 即我们常说的 IOPS。read, MiB/s 加上 written, MiB/s 即我们常说的吞吐量。
MySQL 常见测试场景及对应的 SQL 语句
接下来会列举 MySQL 常见的测试场景及各个场景对应的 SQL 语句。
为了让大家清晰的知道 SQL 语句的含义,首先我们看看测试表的表结构。
除了 bulk_insert 会创建单独的测试表,其它场景都会使用下面的表结构。
1 2 3 4 5 6 7 8 9 10 11 12 | mysql> show create table sbtest.sbtest1\G *************************** 1. row *************************** Table: sbtest1 Create Table: CREATE TABLE `sbtest1` ( `id` int NOT NULL AUTO_INCREMENT, `k` int NOT NULL DEFAULT '0', `c` char(120) NOT NULL DEFAULT '', `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `k_1` (`k`) ) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec) |
bulk_insert
批量插入测试。
1 | sysbench bulk_insert --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
下面是 bulk_insert 场景下创建的测试表。
1 2 3 4 5 6 7 8 9 | mysql> show create table sbtest.sbtest1\G *************************** 1. row *************************** Table: sbtest1 Create Table: CREATE TABLE `sbtest1` ( `id` int NOT NULL, `k` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.01 sec) |
测试对应的 SQL 语句如下:
1 | INSERT INTO sbtest1 VALUES(?, ?),(?, ?),(?, ?),(?, ?)... |
oltp_delete
删除测试。
1 | sysbench oltp_delete --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
基于主键进行删除。测试对应的 SQL 语句如下:
1 | DELETE FROM sbtest1 WHERE id=? |
oltp_insert
插入测试。
1 | sysbench oltp_insert --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 | INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?) |
oltp_point_select
基于主键进行查询。
1 | sysbench oltp_point_select --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 | SELECT c FROM sbtest1 WHERE id=? |
oltp_read_only
只读测试。
1 | sysbench oltp_read_only --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 2 3 4 5 | SELECT c FROM sbtest1 WHERE id=? ## 默认会执行 10 次,由 --point_selects 选项控制。 SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ? SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c |
oltp_read_write
读写测试。
测试对应的 SQL 语句如下:
1 2 3 4 5 6 7 8 9 | SELECT c FROM sbtest1 WHERE id=? ## 默认会执行 10 次,由 --point_selects 选项控制。 SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ? SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ? ORDER BY c UPDATE sbtest1 SET k=k+1 WHERE id=? UPDATE sbtest1 SET c=? WHERE id=? DELETE FROM sbtest1 WHERE id=? INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?) |
oltp_update_index
基于主键进行更新,更新的是索引字段。
1 | sysbench oltp_update_index --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 | UPDATE sbtest1 SET k=k+1 WHERE id=? |
oltp_update_non_index
基于主键进行更新,更新的是非索引字段。
1 | sysbench oltp_update_non_index --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 | UPDATE sbtest1 SET c=? WHERE id=? |
oltp_write_only
只写测试。
1 | sysbench oltp_write_only --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 2 3 4 | UPDATE sbtest1 SET k=k+1 WHERE id=? UPDATE sbtest1 SET c=? WHERE id=? DELETE FROM sbtest1 WHERE id=? INSERT INTO sbtest1 (id, k, c, pad) VALUES (?, ?, ?, ?) |
select_random_points
基于索引进行随机查询。
1 | sysbench select_random_points --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 2 3 | SELECT id, k, c, pad FROM sbtest1 WHERE k IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
select_random_ranges
基于索引进行随机范围查询。
1 | sysbench select_random_ranges --mysql-host=10.0.0.64 --mysql-port=3306 --mysql-user=admin --mysql-password=Py@123456 --mysql-db=sbtest --tables=30 --table-size=1000000 --threads=64 --time=60 --report-interval=10 run |
测试对应的 SQL 语句如下:
1 2 3 | SELECT count(k) FROM sbtest1 WHERE k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? OR k BETWEEN ? AND ? |
如何自定义 sysbench 测试脚本
下面通过 bulk_insert.lua 和 oltp_point_select.lua 这两个脚本分析下 sysbench 测试脚本的实现逻辑。
首先看看 bulk_insert.lua。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | # cat bulk_insert.lua #!/usr/bin/env sysbench cursize=0 function thread_init() drv = sysbench.sql.driver() con = drv:connect() end function prepare() local i local drv = sysbench.sql.driver() local con = drv:connect() for i = 1, sysbench.opt.threads do print("Creating table 'sbtest" .. i .. "'...") con:query(string.format([[ CREATE TABLE IF NOT EXISTS sbtest%d ( id INTEGER NOT NULL, k INTEGER DEFAULT '0' NOT NULL, PRIMARY KEY (id))]], i)) end end function event() if (cursize == 0) then con:bulk_insert_init("INSERT INTO sbtest" .. thread_id+1 .. " VALUES") end cursize = cursize + 1 con:bulk_insert_next("(" .. cursize .. "," .. cursize .. ")") end function thread_done(thread_9d) con:bulk_insert_done() con:disconnect() end function cleanup() local i local drv = sysbench.sql.driver() local con = drv:connect() for i = 1, sysbench.opt.threads do print("Dropping table 'sbtest" .. i .. "'...") con:query("DROP TABLE IF EXISTS sbtest" .. i ) end end |
下面,我们看看这几个函数的具体作用:
- thread_init():线程初始化时调用。这个函数常用来创建数据库连接。
- prepare():指定 prepare 时调用。这个函数常用来创建测试表,生成测试数据。
- event():指定 run 时调用。这个函数会定义需要测试的 SQL 语句。
- thread_done():线程退出时调用。这个函数常用来关闭 Prepared Statements 和数据库连接。
- cleanup():指定 cleanup 时调用。这个函数常用来删除测试表。
如果我们要自定义测试脚本,只需实现这几个函数即可。
如果我们要基于 sbtest 表自定义测试项,就要分析 oltp*.lua 脚本的实现逻辑。
下面,以 oltp_point_select.lua 脚本为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/usr/bin/env sysbench ... require("oltp_common") function prepare_statements() -- point_selects 是 oltp_point_select 中支持的选项,默认为 10,这里调整为了 1。 sysbench.opt.point_selects=1 prepare_point_selects() end function event() execute_point_selects() end |
与 bulk_insert.lua 不一样的是,oltp_point_select.lua 只简单的定义了两个函数:prepare_statements()
和event()
。实际上,不仅仅是 oltp_point_select.lua,其它 oltp*.lua 脚本也只定义了这两个函数。
虽然只定义了这两个函数,但脚本导入了 oltp_common 模块,所以实际上,脚本中的 prepare_point_selects(),execute_point_selects() 以及 bulk_insert.lua 中的 thread_init(),prepare(),thread_done(),cleanup() 都是在oltp_common.lua
这个公共模块中定义的。
接下来,我们看看 prepare_point_selects() 和 execute_point_selects() 这两个函数的实现逻辑。
首先看看prepare_point_selects()
。
它调用的是prepare_for_each_table()
。prepare_for_each_table()是一个基础函数。所有prepare 相关的函数都会调用prepare_for_each_table(), 只不过不同的 prepare 函数会传入不同的参数名。
prepare_for_each_table()
会填充两张表(Lua 中的表既可用来表示数组,也可用来表示集合):stmt 和 param。其中,stmt 用来存储 Prepared Statements 语句,param 用来存储 Prepared Statements 语句相关的参数类型。
填充完毕后,最后再通过 bind_param 函数将两者绑定在一起。
可以看到,无论是 Prepared Statements 语句还是相关的参数类型,都是在 stmt_defs 定义的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function prepare_point_selects() prepare_for_each_table("point_selects") end function prepare_for_each_table(key) for t = 1, sysbench.opt.tables do -- t 是表的序号,key 是测试项的名字 stmt[t][key] = con:prepare(string.format(stmt_defs[key][1], t)) local nparam = #stmt_defs[key] - 1 if nparam > 0 then param[t][key] = {} end for p = 1, nparam do local btype = stmt_defs[key][p+1] local len if type(btype) == "table" then len = btype[2] btype = btype[1] end if btype == sysbench.sql.type.VARCHAR or btype == sysbench.sql.type.CHAR then param[t][key][p] = stmt[t][key]:bind_create(btype, len) else param[t][key][p] = stmt[t][key]:bind_create(btype) end end if nparam > 0 then stmt[t][key]:bind_param(unpack(param[t][key])) end end end |
接下来,我们看看 stmt_defs 的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | local stmt_defs = { point_selects = { "SELECT c FROM sbtest%u WHERE id=?", t.INT}, simple_ranges = { "SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ?", t.INT, t.INT}, sum_ranges = { "SELECT SUM(k) FROM sbtest%u WHERE id BETWEEN ? AND ?", t.INT, t.INT}, order_ranges = { "SELECT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c", t.INT, t.INT}, distinct_ranges = { "SELECT DISTINCT c FROM sbtest%u WHERE id BETWEEN ? AND ? ORDER BY c", t.INT, t.INT}, index_updates = { "UPDATE sbtest%u SET k=k+1 WHERE id=?", t.INT}, non_index_updates = { "UPDATE sbtest%u SET c=? WHERE id=?", {t.CHAR, 120}, t.INT}, deletes = { "DELETE FROM sbtest%u WHERE id=?", t.INT}, inserts = { "INSERT INTO sbtest%u (id, k, c, pad) VALUES (?, ?, ?, ?)", t.INT, t.INT, {t.CHAR, 120}, {t.CHAR, 60}}, } |
可以看到,stmt_defs 是一张表,里面定义了不同测试项对应的 Prepared Statements 语句和参数类型。
具体到 point_selects 这个测试项,它对应的 Prepared Statements 语句是SELECT c FROM sbtest%u WHERE id=?
,对应的参数类型是t.INT
。
梳理完 prepare_point_selects() 函数的实现逻辑。最后我们看看execute_point_selects()
函数的实现逻辑。
1 2 3 4 5 6 7 8 9 10 | function execute_point_selects() local tnum = get_table_num() local i -- point_selects 对应命令行中的 --point_selects 选项,默认为 10。 for i = 1, sysbench.opt.point_selects do param[tnum].point_selects[1]:set(get_id()) stmt[tnum].point_selects:execute() end end |
逻辑也非常简单,先赋值,最后执行。
所以如果我们要基于 sbtest 表自定义测试项,最关键的一步其实就是在 stmt_defs 中定义 Prepared Statements 语句和相关的参数类型。至于 prepare_xxx 和 execute_xxx 函数,实现起来都非常简单。
总结
基准测试一般会关注三个指标:TPS/QPS、响应耗时和并发量。
只有进行全链路压测,我们才知道系统的瓶颈在哪里。不能想当然的以为,数据库不容易横向扩展,系统瓶颈就一定会出在数据库层。事实上,很多系统在设计之初就引入了缓存,而缓存会分担很大一部分读流量,这种架构下的数据库压力其实并不大。
不能简单的将 sysbench 的测试结果(TPS/QPS) 作为业务系统的吞吐量指标,因为两者的业务模型并不一致。
如果要自定义测试脚本,实现的方式有两种:
- 自己实现测试相关的所有函数,具体实现细节可参考 bulk_insert.lua。
- 基于 sbtest 表自定义测试项。实现过程中最关键的一步是在 stmt_defs 中定义 Prepared Statements 语句和相关的参数类型。
常用SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | -- 若要让用户%us保持在90%,可以使用 stress --cpu 8 --timeout 600 sysbench --threads=100 --events=80000 cpu --cpu-max-prime=8000000 run -- 压测MySQL sysbench /usr/share/sysbench/oltp_common.lua --time=300 \ --mysql-host=172.17.0.2 --mysql-port=3306 --mysql-user=root --mysql-password=lhr --mysql-db=sbtest \ --table-size=1000000 --tables=10 --threads=16 --events=999999999 prepare sysbench /usr/share/sysbench/oltp_read_write.lua --time=300 \ --mysql-host=172.17.0.2 --mysql-port=3306 --mysql-user=root --mysql-password=lhr --mysql-db=sbtest \ --table-size=1000000 --tables=10 --threads=16 --events=999999999 \ --report-interval=10 --db-ps-mode=disable --forced-shutdown=1 run -- 压测PostgreSQL sysbench /usr/share/sysbench/oltp_common.lua --db-driver=pgsql \ --pgsql-host=172.72.6.20 --pgsql-port=5432 \ --pgsql-user=postgres --pgsql-password=lhr --pgsql-db=lhrdb \ --table-size=10000 --tables=10 --threads=80 \ --events=999999999 --time=60 prepare sysbench /usr/share/sysbench/oltp_read_write.lua --db-driver=pgsql \ --pgsql-host=172.72.6.20 --pgsql-port=5432 \ --pgsql-user=postgres --pgsql-password=lhr --pgsql-db=lhrdb \ --table-size=10000 --tables=10 --threads=20 \ --events=999999999 --time=60 --report-interval=10 \ --db-ps-mode=disable --forced-shutdown=1 run |