├── README.md └── mysql_oltp_test.sh /README.md: -------------------------------------------------------------------------------- 1 | 本文是作者工作中需要对[atlas](https://github.com/Qihoo360/Atlas)(360开源的mysql中间件,可实现读写分离、分表、多从库负载均衡)以及后期对[proxysql](https://github.com/sysown/proxysql/)(另一款高效并很有特色的数据库代理软件)进行测试时所设计和采用的一套脚本。由于对中间件测试,要测试对比的维度较多,所以尽量将涉及到的因素都纳入脚本中以实现自动化的压测和分析过程。总体思路如下: 2 | ``` 3 | 准备测试数据(这步在脚本之外人工完成) --> 4 | 运行脚本测试(压测线程数在脚本里定义,压测次数默认为3) --> 5 | 脚本将测试输出格式化后写入数据库 --> 6 | 脚本加 analyse 参数 --> 将压测分析结果打印到标准输出 7 | 脚本加 chart 参数 --> 对压测结果中的所有测试场景进行绘图 8 | 脚本加 chart '测试场景'... --> 只对给定的'测试场景进行绘图' 9 | ``` 10 | 11 | **脚本依赖于`sysbench-0.5`和`gnuplot`** 12 | 13 | 下面来看下脚本使用说明及用例截图 14 | 15 | **帮助信息:** 16 | ``` 17 | [ljk@demo ~]$ sh shells/mysql_oltp_test.sh -h 18 | 19 | Usage: shells/mysql_oltp_test.sh test (test_scenario test_type mysql_host mysql_port mysql_user mysql_password) 20 | shells/mysql_oltp_test.sh analyse 21 | shells/mysql_oltp_test.sh chart [scenario]... 22 | 23 | ---------- 24 | 测试: 子命令test 25 | test_scenario: 自定义的测试场景名 26 | test_type: read-only 或 read-write, 表示测试模式 27 | 其余4参数表示待测试MySQL连接相关信息,密码若包含特殊字符,将其置于单引号内 28 | ---------- 29 | 分析: 子命令analyse 30 | 展示分析结果 31 | ---------- 32 | 画图: 子命令chart 33 | 会在/tmp/下生成request_per_second.png total_time.png 95_pct_time.png 三张图 34 | chart (对分析结果中的所有测试场景画图) 35 | chart scenario ... (对指定的测试场景画图,场景名依据先前自定义的名称) 36 | 37 | ``` 38 | 39 | **进行测试:** 40 | ``` 41 | [ljk@demo ~]$ sh shells/mysql_oltp_test.sh test localhost_read_only read-only 127.0.0.1 3306 user 'passwd' 42 | 43 | --------------- 44 | 创建测测试结果表test.sysbench_test 45 | --------------- 46 | 47 | --------------- 48 | 场景:localhost_read_only 模式:read-only 49 | --------------- 50 | 8线程 第1次运行... 51 | 24线程 第1次运行... 52 | 48线程 第1次运行... 53 | 64线程 第1次运行... 54 | 96线程 第1次运行... 55 | 128线程 第1次运行... 56 | 160线程 第1次运行... 57 | 196线程 第1次运行... 58 | 256线程 第1次运行... 59 | ``` 60 | 61 | **查看分析结果:** 62 | ``` 63 | [ljk@demo ~]$ sh shells/mysql_oltp_test.sh analyse 64 | Warning: Using a password on the command line interface can be insecure. 65 | scenario server_name test_type sb_threads server_load request_total request_read request_write request_per_second total_time 95_pct_time 66 | single-mysql-remote 192.168.1.3 read-only 8 1.48 280000 280000 0 14269.88 19.78 17.32 67 | single-mysql-remote 192.168.1.3 read-only 24 1.55 280016 280016 0 23933.22 11.81 111.12 68 | single-mysql-remote 192.168.1.3 read-only 48 1.83 280022 280022 0 26868.16 10.47 225.34 69 | single-mysql-remote 192.168.1.3 read-only 64 2.02 280016 280016 0 28269.55 9.95 231.25 70 | single-mysql-remote 192.168.1.3 read-only 96 1.88 280012 280012 0 29160.26 9.62 409.48 71 | single-mysql-remote 192.168.1.3 read-only 128 1.92 280016 280016 0 31032.74 9.04 443.33 72 | single-mysql-remote 192.168.1.3 read-only 160 1.90 280012 280012 0 30906.38 9.07 616.22 73 | single-mysql-remote 192.168.1.3 read-only 196 2.19 280022 280022 0 31525.47 8.89 647.59 74 | single-mysql-remote 192.168.1.3 read-only 256 2.35 280009 280009 0 31965.53 8.77 844.62 75 | single-mysql-remote 192.168.1.3 read-only 512 2.48 280012 280012 0 31291.09 9.15 1362.92 76 | proxysql-1.3.0e 192.168.1.4 read-only 8 0.96 280005 280005 0 10092.85 27.79 25.06 77 | proxysql-1.3.0e 192.168.1.4 read-only 24 1.00 280009 280009 0 19884.42 14.10 159.36 78 | proxysql-1.3.0e 192.168.1.4 read-only 48 1.30 280014 280014 0 23239.66 12.07 232.69 79 | proxysql-1.3.0e 192.168.1.4 read-only 64 1.28 280023 280023 0 25746.88 10.89 238.74 80 | proxysql-1.3.0e 192.168.1.4 read-only 96 1.25 280014 280014 0 28304.34 9.90 434.78 81 | proxysql-1.3.0e 192.168.1.4 read-only 128 1.52 280014 280014 0 27669.46 10.22 481.75 82 | proxysql-1.3.0e 192.168.1.4 read-only 160 1.68 280028 280028 0 30020.32 9.34 606.40 83 | proxysql-1.3.0e 192.168.1.4 read-only 196 1.69 280028 280028 0 30620.70 9.15 682.21 84 | proxysql-1.3.0e 192.168.1.4 read-only 256 1.76 280033 280033 0 31710.98 8.83 826.42 85 | proxysql-1.3.0e 192.168.1.4 read-only 512 1.65 280028 280028 0 32398.24 8.65 1289.20 86 | ``` 87 | 88 | **对结果进行画图:** 89 | ``` 90 | #对存在于表中的所有场景进行横向对比画图 91 | [ljk@demo ~]$ sh shells/mysql_oltp_test.sh chart 92 | 对特定的场景进行(横向对比)画图 93 | [ljk@demo ~]$ sh shells/mysql_oltp_test.sh chart single-mysql-remote proxysql-use-ps 94 | ``` 95 | 96 | **效果图展示:** 97 | ![image](http://s2.51cto.com/wyfs02/M02/88/EC/wKioL1gA-33y64FqAAAfUIYcEh0352.png) 98 | 99 | 接下来我们来了解一下sysbench-0.5对MySQL进行测试的方法及原理 100 | 101 | sysbench-0.5,对于数据库的测试较0.4版本有较大不同,之前有内建的--test=oltp方法,现在改成了外部的lua脚本形式,这样更灵活,也方便用户构建自己的测试模型。 102 | 103 | 这些相关的lua脚本位于”/usr/share/doc/sysbench/tests/db/“ 目录,其内脚本如下图所示 104 | ![image](http://s2.51cto.com/wyfs02/M01/7C/BD/wKiom1bW8M-yS4DpAAAoCWAEXJI478.png) 105 | 106 | 我们需要了解我们最有可能用到的三个脚本:common.lua(公共脚本)、oltp.lua(oltp测试主脚本)和parallel_prepare.lua(并行准备数据)。common.lua中定义了一些选项的默认值(故而,这些选项的值既可以通过命令行指定也可直接修改该脚本里对应值来更改). 107 | 108 | 简单说一下oltp.lua脚本的逻辑: 109 | ``` 110 | 默认通过显式的使用begin和commit语句将如下模式的sql组合在一起形成一个事务(只读测试的话则没有写请求) 111 | 112 | 关于oltp '读写'和'只读'测试的语句及其出现比例如下 113 | 114 | 10条 SELECT c FROM sbtest6 WHERE id=5047; 115 | 1条 SELECT c FROM sbtest16 WHERE id BETWEEN 5050 AND 5050+99; 116 | 1条 SELECT SUM(K) FROM sbtest7 WHERE id BETWEEN 5039 AND 5039+99; 117 | 1条 SELECT c FROM sbtest7 WHERE id BETWEEN 4987 AND 4987+99 ORDER BY c; 118 | 1条 SELECT DISTINCT c FROM sbtest7 WHERE id BETWEEN 13 AND 13+99 ORDER BY c; 119 | 1条 UPDATE sbtest1 SET k=k+1 WHERE id=1234; 120 | 1条 UPDATE sbtest2 SET c='78864443858-59732318638' where id=2345; 121 | 1条 DELETE FROM sbtest11 WHERE id=4958; 122 | 1条 INSERT 语句; 123 | 124 | 然后将此事务循环执行10000次。也就是只读测试共14w请求,混合测试18w请求。 125 | 若觉得数量不够,可以修改common.lua中的设置 126 | 127 | function set_vars() 128 | oltp_table_size = oltp_table_size or 10000 129 | oltp_range_size = oltp_range_size or 100 130 | oltp_tables_count = oltp_tables_count or 1 131 | oltp_point_selects = oltp_point_selects or 20 (原来10) 132 | oltp_simple_ranges = oltp_simple_ranges or 2 (原来1) 133 | oltp_sum_ranges = oltp_sum_ranges or 2 (原来1) 134 | oltp_order_ranges = oltp_order_ranges or 2 (原来1) 135 | oltp_distinct_ranges = oltp_distinct_ranges or 2 (原来1) 136 | oltp_index_updates = oltp_index_updates or 1 137 | 138 | oltp_non_index_updates = oltp_non_index_updates or 1 139 | 140 | 这样总的测试请求量会变成28w 141 | ``` 142 | 以上是通过lua脚本里总结出来的,各位也可查看下这些lua脚本,来更好的理解测试的逻辑过程。 143 | 144 | 一般来说,对MySQL做压测会基于两种需求: 145 | 146 | - 一种是通过压测来大致评估MySQL实例的最大能力,这种适合给定时长来测; 147 | - 另一种就是来对比某些改动前后的性能变化(如版本升级、参数调整等),这种适合给定请求数来测。 148 | 149 | 以作者的小经验来看,后者要更多一些,所以我的测试模式也是趋向于后者的。 150 | 151 | 前提功课做好了,接下来一起看一下本例的测试过程 152 | 153 | **准备数据:** 154 | 155 | (**手动创建**)在被测的mysql上执行如下命令(以8线程并发创建16张50w数据的表) 156 | ``` 157 | sysbench --test=/usr/share/doc/sysbench/tests/db/parallel_prepare.lua \ 158 | --mysql-table-engine=innodb --oltp-table-size=500000 --mysql-user=user \ 159 | --mysql-password='passwd' --mysql-port=3306 --mysql-host=192.168.1.33 \ 160 | --oltp-tables-count=16 --num-threads=8 run 161 | ``` 162 | 还有另外一种方式,用oltp.lua脚本以串行方式准备数据 163 | ``` 164 | sysbench --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-table-engine=innodb \ 165 | --oltp-table-size=500000 --mysql-user=user --mysql-password='passwd' \ 166 | --mysql-port=3306 --mysql-host=192.168.1.3 --oltp-tables-count=16 prepare 167 | ``` 168 | 169 | **开始测试:** 170 | ``` 171 | sh mysql_oltp_test.sh test atlas-24-threads read-only 192.168.1.44 3306 user passwd 172 | ``` 173 | 174 | **清空测试数据:** 175 | ``` 176 | sysbench --test=/usr/share/doc/sysbench/tests/db/parallel_prepare.lua \ 177 | --mysql-user=user --mysql-password='passwd' --mysql-port=3306 \ 178 | --mysql-host=192.168.1.3 --oltp-tables-count=16 --num-threads=8 cleanup 179 | ``` 180 | -------------------------------------------------------------------------------- /mysql_oltp_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #by ljk 20161003 3 | 4 | #通过sysbench测试mysql相关性能,并将关键数据存储于‘test.sysbenc_test’表中 5 | 6 | #----------自定义部分---------- 7 | #定义记录测试结果的mysql连接相关参数,本例我在测试机上记录测试结果 8 | m_user='test' 9 | m_passwd='test' 10 | m_port='3306' 11 | m_host='127.0.0.1' 12 | #测试结果存储于哪个库 13 | m_db='test' 14 | #测试结果存储于哪个表 15 | m_table='sysbench_test' 16 | 17 | #sysbench lua脚本目录 18 | lua_dir=/usr/share/doc/sysbench/tests/db 19 | 20 | #画图维度(关注的三个指标,也就是会画三张图) 21 | target="request_per_second total_time 95_pct_time" 22 | 23 | #定义错误日志文件 24 | log=/tmp/mysql_oltp.log 25 | #定义分析结果文件 26 | data=/tmp/mysql_oltp.dat 27 | 28 | #定义测试线程 29 | threds_num='8 24 48 64 96 128 160 196 256' 30 | #每种场景的测试次数,分析时取平均值 31 | times=3 32 | #----------自定义部分结束---------- 33 | 34 | #测试函数 35 | sb_test() { 36 | 37 | #定义测试方式相关变量 38 | tables_count=16 #测试表的数量 39 | if [ "$3" == "read-only" ];then read_only='on';else read_only='off';fi #根据脚本参数确定是否read-only 40 | 41 | #创建记录测试信息的表 42 | echo -e "\n---------------\n创建测测试结果表$m_db.$m_table\n---------------" 43 | return=$(mysql -u$m_user -p$m_passwd -P$m_port -h$m_host <&1 44 | CREATE TABLE IF NOT EXISTS $m_db.$m_table ( 45 | scenario varchar(30) NOT NULL DEFAULT '' COMMENT '测试场景', 46 | server_name varchar(15) NOT NULL COMMENT '被测DB name', 47 | test_type varchar(15) NOT NULL COMMENT 'read-only,read-write,insert等', 48 | sb_threads int(11) NOT NULL DEFAULT '0' COMMENT 'sysbench 测试线程', 49 | server_load decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '以当前线程测试完后立刻记录一分钟负载值', 50 | request_total int(11) NOT NULL DEFAULT '0', 51 | request_read int(11) NOT NULL DEFAULT '0', 52 | request_write int(11) NOT NULL DEFAULT '0', 53 | request_per_second decimal(12,2) NOT NULL DEFAULT '0.00', 54 | total_time decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '单位秒', 55 | 95_pct_time decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '单位毫秒' 56 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 57 | EOF 58 | ) 59 | if [ $? -ne 0 ];then 60 | echo $return|sed 's/[Ww]arning:.*password on the command.*insecure\.//' 61 | #echo "create table $m_db.$m_table failed" 62 | exit -1 63 | fi 64 | 65 | #开始测试,每种条件测$times次,分析时取平均值 66 | echo -e "\n---------------\n场景:$2 模式:$3\n---------------" 67 | for i in `seq $times`;do 68 | for sb_threds in $threds_num;do #按照指定的sysbench线程测试 69 | printf " %-10s %s\n" $sb_threds线程 第$i次运行... 70 | 71 | #result 作为每次最小测试单元的结果,根据sysbench测试结果各参数的出现顺序 72 | #以request_read、request_write、request_total、request_per_second、total_time、95_pct_time为顺序插入表中 73 | #下条命令中,egerp之后的操作是为了对sysbench的输出做筛选和格式化,以便插入数据库 74 | sysbench --test=$lua_dir/oltp.lua --mysql-user=$6 --mysql-password=$7 --mysql-port=$5 --mysql-host=$4 \ 75 | --num-threads=$sb_threds run --oltp-skip-trx=on --oltp-read-only=$read_only &> $log 76 | if [ $? -ne 0 ];then 77 | echo -e "\nSysbench error! For more information see $log" 78 | exit -1 79 | fi 80 | result=$(cat $log | egrep "read:|write:|read/write.*:|total:|total\ time:|approx\..*95.*:" | sed -r -e "s/[0-9]+ \(//g" -e "s/\ per sec\.\)//g" -e "s/m?s$//g"| awk '{printf("%s ",$NF)}' | sed "s/\ /,/g" | sed "s/,$//g") 81 | 82 | #测试完成后立刻记录系统一分钟负载值,可近似认为测试过程中proxy的负载抽样 83 | load=$(ssh -p22 $4 "uptime|awk -F: '{print \$NF}'|awk -F, '{print \$1}'" 2>/dev/null) 84 | if [ -s $load ];then 85 | load=0.00 86 | fi 87 | #本次测试结果写入数据库 88 | return=$(mysql -u$m_user -p$m_passwd -P$m_port -h$m_host <&1 89 | INSERT INTO $m_db.$m_table (scenario,server_name,test_type,sb_threads,server_load,request_read, 90 | request_write,request_total,request_per_second,total_time,95_pct_time) 91 | VALUES ('$2','$4','$3','$sb_threds','$load',$result); 92 | EOF 93 | ) 94 | 95 | if [ $? -ne 0 ];then 96 | echo -e "\n----------$sb_threds线程测试,第$i次插入数据库时失败----------" 97 | #错误输出中排除mysql安全提示 98 | echo $return|sed 's/[Ww]arning:.*password on the command.*insecure\.//' 99 | #echo "INSERT VALUES ('$2','$4','$3',$sb_threds,$load,$result)" 100 | exit -2 101 | fi 102 | sleep 60 #让库歇一会,也让一分钟负载能够恢复到测试前的值 103 | done 104 | 105 | done 106 | } 107 | 108 | #结果分析函数 109 | sb_analyse() { 110 | #2>&1|grep部分为避免安全提示,使用该技巧就无法获取SQL执行的返回码了 111 | mysql -u$m_user -p$m_passwd -h$m_host -P$m_port <&1|grep -v 'password on the command line' 112 | SELECT 113 | scenario, 114 | server_name, 115 | test_type, 116 | sb_threads, 117 | convert(avg(server_load),decimal(12,2)) as server_load, 118 | convert(avg(request_total),decimal(12,0)) as request_total, 119 | convert(avg(request_read),decimal(12,0)) as request_read, 120 | convert(avg(request_write),decimal(12,0)) as request_write, 121 | convert(avg(request_per_second),decimal(12,2)) as request_per_second, 122 | convert(avg(total_time),decimal(12,2)) as total_time, 123 | convert(avg(95_pct_time),decimal(12,2)) as 95_pct_time 124 | FROM $m_db.$m_table group by scenario,server_name,test_type,sb_threads 125 | EOF 126 | } 127 | 128 | #画图函数 129 | sb_chart() { 130 | sb_analyse >$data 131 | 132 | for chart_type in $target;do 133 | col_num=0 #该行及下面这个for循环用于取得三个指标在数据中的列号 134 | for col_name in `cat $data |awk 'NR<2 {print}'`;do 135 | let col_num++ 136 | if [ $col_name == $chart_type ];then break;fi 137 | done 138 | 139 | if [ $chart_type == "request_per_second" ];then #根据图表特点为不同的chart_type设置gunplot不同的key position 140 | key_pos="bottom right" 141 | unit="" 142 | elif [ $chart_type == "total_time" ];then 143 | key_pos="top right" 144 | unit="(s)" 145 | elif [ $chart_type == "95_pct_time" ];then 146 | key_pos="top left" 147 | unit="(ms)" 148 | fi 149 | 150 | plot_cmd="set term png size 800,600;set output '/tmp/$chart_type.png';set title '$chart_type $unit';set grid;set key $key_pos;plot " 151 | 152 | if [ $# -eq 0 ];then 153 | #对分析结果中所有场景进行画图 154 | for scenario in `mysql -u$m_user -p$m_passwd -h$m_host -P$m_port -s -e "select distinct(scenario) from $m_db.$m_table" 2>/dev/null`;do 155 | sb_analyse | awk -v scenario=$scenario '$1 == scenario {print}' > /tmp/"$scenario.dat" 156 | plot_cmd=${plot_cmd}"'/tmp/"$scenario.dat"' using $col_num:xtic(4) title '$scenario' with linespoints lw 2," 157 | done 158 | plot_cmd=$(echo $plot_cmd | sed 's/,$//g') 159 | echo $plot_cmd | gnuplot 160 | else 161 | #只绘制指定的场景 162 | for scenario in $*;do 163 | sb_analyse | awk -v scenario=$scenario '$1 == scenario {print}' > /tmp/"$scenario.dat" 164 | plot_cmd=${plot_cmd}"'/tmp/"$scenario.dat"' using $col_num:xtic(4) title '$scenario' with linespoints lw 2," 165 | done 166 | plot_cmd=$(echo $plot_cmd | sed 's/,$//g') 167 | echo "$plot_cmd" | gnuplot 168 | fi 169 | done 170 | } 171 | 172 | #脚本使用说明/参数判断 173 | if [ $# -eq 1 ] && [ $1 == "-h" -o $1 == "--help" ];then 174 | echo -e "\nUsage: $0 test (test_scenario) (test_type) (mysql_host) (mysql_port) (mysql_user) (mysql_password)\n $0 analyse\n $0 chart [scenario]...\n" 175 | echo ---------- 176 | echo -e "测试: 子命令test" 177 | echo -e " test_scenario: 自定义的测试场景名" 178 | echo -e " test_type: read-only 或 read-write, 表示测试模式" 179 | echo -e " 其余4参数表示待测试MySQL连接相关信息,密码若包含特殊字符,将其置于单引号内" 180 | echo -e "----------" 181 | echo -e "分析: 子命令analyse" 182 | echo -e "----------" 183 | echo -e "画图: 子命令chart" 184 | echo -e " 会在/tmp/下生成request_per_second.png total_time.png 95_pct_time.png 三张图" 185 | echo -e " chart (对分析结果中的所有测试场景画图)" 186 | echo -e " chart scenario ... (对指定的测试场景画图,场景名依据先前自定义的名称)\n" 187 | exit -1 188 | elif [ "$1" == "test" -a $# -eq 7 ];then 189 | sb_test $1 $2 $3 $4 $5 $6 $7 190 | elif [ "$1" == "analyse" -a $# -eq 1 ];then 191 | sb_analyse 192 | elif [ "$1" == "chart" ];then 193 | #chart函数可不接参数,也可接任意个'测试场景'作为参数 194 | arg=($*) 195 | arg_len=${#arg[@]} 196 | sb_chart ${arg[@]:1:$arg_len-1} 197 | else 198 | echo -e "\nUsage: $0 test (test_scenario) (test_type) (mysql_host) (mysql_port) (mysql_user) (mysql_password)\n $0 analyse\n $0 chart [scenario]...\n" 199 | fi 200 | --------------------------------------------------------------------------------