sql 性能测试工具的设计
By Siu 2022/3/17
前言
当前的团队的工作比较多的会在关注和执行 sql 性能相关的测试,对于标准的测试模型,如 TPC 范围内的工具有比较好的实践方式,对于自定义和具体的场景目前团队还没有去总结一个比较好的”工具方案“,这里主要围绕这个问题去做一些分享。
先看看有哪些现有工具?
- Tidb bench :C ;集成在 TiDB 中,用于 TPC-C/H 的测试,不适用于其它数据库
- BenchMarkSQL 5.0: Java;适用于 TPC-C ,主流的 RDBMS 都支持,Mysql/PostgreSQL/Oracle 等
- Sysbench:C;适用于 TPC-C ,主流的 RDBMS 都支持,Mysql/PostgreSQL/Oracle 等
- mysqlslap:C;用于msyql 的性能测试
- sqlbench:go;支持 PostgreSQL 的性能测试
- 其它的开源的库针对非传统数据库/组件:这些通常是依据标准模型 TPC-H/DS、SSB 等的实现,这部分较复杂,可以到官方和社区去找一些方案和工具(ClickHouse、Flink、Trino)
简单总结:
- 以上这些工具都是比较优秀的,大多是开源的;整体我都使用过,值得去深入了解各自的场景和特点;
- BenchMarkSQL、Sysbench 比较合适标准模型的基准测试(mysqlslap 也比较适合,但不主流);
- mysqlslap、sqlbench:适用于自定义的 sql 场景的测试;
这些工具的“设计“
"设计”,主要讨论这些工具的内部设计大体是怎么样的,哪些可以借鉴和指导我们去设计/开发我们的自定义 sql 性能测试工具/脚本。
当然这些我总结的”范式“可能不是真正的标准,但是已经经过了具体的借鉴和实践。
编程范式
当然设计会受语言的编程范式影响,受语言特性影响,但这里忽略这部分,不做讨论。
实际实现的时候要考虑这部分。
工具设计的拆解
分析比较了 BenchMarkSQL 5.0、sqlbench、mysqlslap 等的功能和代码,等到如下总结:
- 环境/全局配置:通过 CMD 参数或配置文件加载到程序
- 环境:系统、依赖工具、数据库信息
- 全局:工具运行时的参数,线程数、执行数等
- sql 任务配置:定义 sql 执行的单位
- 其它:主要是功能,造数、执行日志(参数、环境、上下文,IO,网络等)、执行结果/绘图、终端动态展示运行等
用一个命令行描述:
sh myApp --config=./config/env.conf --sql=./sql/* --func xxx
特别说下 BenchMarkSQL TPC-C 测试流程:
- 配置数据库信息、测试的全局信息
- 启动造数据程序:元数据、数据
- 执行测试:实时输出测试指标和日志、归档日志/结果等到测试目录
- 执行绘图程序,输出图表测试结果
- 清理数据
设计一个简单的 sql 性能测试程序
这里以 mysqlslap + shell 去设计一个 sql 性能测试脚本。
比较好的路线是基于一些标准库用某个语言去实现,这样可定制的功能比较好控制。
选型
- 语言 shell:读写文件、option、函数
- 库/工具:mysqlslap
设计
.
├── config
│ ├── conf # 主配置:环境、全局
│ └── jobs # sql 配置
├── output # 测试输出
│ ├── test-1 # 测试1输出
│ └── test-2 # 测试2输出
└── run.sh # 入口:加载配置/日志、归档测试结果、执行:获取jobs/执行mysqlslap、option(暂未实现)
配置部分:
conf 文件:
任何格式,按照需求去设计
# 一些全局参数
db_schema='ssb'
db_user='root'
db_port='9030'
...
jobs/ :
任何格式,按照需求去设计,JSON、YML、TOML 都行
# test 1: build-in functions
test_name='build-in'
query_sql="select length(c_address) from ssb.customer;"
pre_query="set global enable_vectorized_engine=true;set global batch_size=1024;"
...
实现
逻辑描述:
启动
=> 加载配置/optioin传入
=> 加载jobs
=> LOOP:构建job > 执行job > 记录job日志&结果
=> 归档&展示测试结果
待实现:从 option 传入配置
运行
[root@test-fe-1 test]# sh run.sh
=====================================================================================================================================
test-fe-1 2022-03-16 21:29:17.249 执行测试:build-in
test-fe-1 2022-03-16 21:29:17.256 执行预处理:set global enable_vectorized_engine=true;set global batch_size=1024;
test-fe-1 2022-03-16 21:29:17.264 执行测试 SQL:select length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:29:25.877 build-in 执行完成: build-in,mixed,1.698,1.298,1.872,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:29:25.883 执行测试:n-udf-f
test-fe-1 2022-03-16 21:29:25.890 执行预处理:set global enable_vectorized_engine=false;
test-fe-1 2022-03-16 21:29:25.895 执行测试 SQL:select ssb.get_string_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:29:34.986 n-udf-f 执行完成: n-udf-f,mixed,1.798,1.740,1.879,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:29:34.995 执行测试:rudf2-t-1024
test-fe-1 2022-03-16 21:29:35.001 执行预处理:set global enable_vectorized_engine=true;set global batch_size=1024;
test-fe-1 2022-03-16 21:29:35.008 执行测试 SQL:select ssb.str_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:29:44.929 rudf2-t-1024 执行完成: rudf2-t-1024,mixed,1.961,1.867,2.058,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:29:44.936 执行测试:rudf3-t-2048
test-fe-1 2022-03-16 21:29:44.942 执行预处理:set global enable_vectorized_engine=true;set global batch_size=2048;
test-fe-1 2022-03-16 21:29:44.948 执行测试 SQL:select ssb.str_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:29:53.113 rudf3-t-2048 执行完成: rudf3-t-2048,mixed,1.612,1.494,1.782,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:29:53.119 执行测试:rudf4-t-4096
test-fe-1 2022-03-16 21:29:53.124 执行预处理:set global enable_vectorized_engine=true;set global batch_size=4096;
test-fe-1 2022-03-16 21:29:53.129 执行测试 SQL:select ssb.str_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:30:00.478 rudf4-t-4096 执行完成: rudf4-t-4096,mixed,1.447,1.396,1.518,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:30:00.485 执行测试:rudf2-5-8192
test-fe-1 2022-03-16 21:30:00.492 执行预处理:set global enable_vectorized_engine=true;set global batch_size=8192;
test-fe-1 2022-03-16 21:30:00.497 执行测试 SQL:select ssb.str_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:30:07.553 rudf2-5-8192 执行完成: rudf2-5-8192,mixed,1.388,1.352,1.488,10,1
=====================================================================================================================================
test-fe-1 2022-03-16 21:30:07.561 执行测试:rudf6-t-16384
test-fe-1 2022-03-16 21:30:07.567 执行预处理:set global enable_vectorized_engine=true;set global batch_size=16384;
test-fe-1 2022-03-16 21:30:07.574 执行测试 SQL:select ssb.str_length(c_address) from ssb.customer;
test-fe-1 2022-03-16 21:30:11.685 rudf6-t-16384 执行完成: rudf6-t-16384
##########################################################################
全局参数:
client_num=10
queries_num=10
测试结果: test-20220316212917243/result.csv
test_name mode avg min max client_num queries_per_client
build-in mixed 1.698 1.298 1.872 10 1
n-udf-f mixed 1.798 1.740 1.879 10 1
rudf2-5-8192 mixed 1.388 1.352 1.488 10 1
rudf2-t-1024 mixed 1.961 1.867 2.058 10 1
rudf3-t-2048 mixed 1.612 1.494 1.782 10 1
rudf4-t-4096 mixed 1.447 1.396 1.518 10 1
rudf6-t-16384
#########################################################################
ref
附录
实现的脚本
#!/bin/bash
#
# @CreationTime
# 2022/3/15 下午16:45:20
# @Function
#
# @Usage
# @author Siu
CURRENT_PATH=$(readlink -f "$(dirname "$0")")
## region 全局参数:当有配置文件覆盖时这里的参数无效
db_ip=$(hostname -I | awk '{gsub(/^\s+|\s+$/, "");print}')
# 总查询的次数 = min(client_queries_limit,client_num * run_times)
client_num=10
run_times=5
# 官方文档说明:Limit each client to approximately this number of queries,实际限制每个 client,而是限制总查询数
client_queries_limit=10
db_schema='ssb'
db_user='root'
db_port='9030'
# 配置文件
conf_file="${CURRENT_PATH}"/config/conf
# jobs
jobs_path="${CURRENT_PATH}"/config/jobs
cmd_input=""
## endregion
## 记录日志
logFmt() {
date_str=$(date "+%Y-%m-%d %H:%M:%S.%3N")
echo "$(hostname -s)" "${date_str}" "$1"
# shellcheck disable=SC2086
echo "$(hostname -s)" "${date_str}" $1 >>"${archive_dir}"/run.log
}
log() {
echo "$1"
# shellcheck disable=SC2086
echo $1 >>"${archive_dir}"/run.log
}
runMss() {
mysqlslap -u ${db_user} -P ${db_port} -h ${db_ip} \
--concurrency=${client_num} --iterations=${run_times} --number-of-queries=${client_queries_limit} --create-schema=${db_schema} \
--query=./"${archive_dir}"/"$1".sql \
--pre-query=./"${archive_dir}"/p_"$1".sql \
--csv=./"${archive_dir}"/"$1".csv
tmp=$(cat ./"${archive_dir}"/"$1".csv)
tmp1=$1${tmp}
echo "$tmp1" >./"${archive_dir}"/"$1".csv
res=$(cat ./"${archive_dir}"/"$1".csv)
}
runJob() {
test_name=$1
query_sql=$2
pre_query=$3
echo "${query_sql}" >./"${archive_dir}"/"${test_name}".sql
echo "${pre_query}" >./"${archive_dir}"/p_"${test_name}".sql
log "====================================================================================================================================="
logFmt "执行测试:${test_name} "
logFmt "执行预处理:${pre_query}"
logFmt "执行测试 SQL:${query_sql}"
runMss "${test_name}"
logFmt "$1 执行完成: ${res}"
}
runJobs() {
if [ ! -d "${jobs_path}" ]; then
logFmt "jobs 路径不存在:$jobs_path"
help
exit 1
else
for file in "${jobs_path}"/*; do
if test -f $file; then
#log "加载:$file"
# shellcheck disable=SC1090
. "$file"
test_name=$(basename "$file")
runJob "${test_name}" "${query_sql}" "${pre_query}"
fi
if test -d "$file"; then
logFmt "dir:$file"
fi
done
fi
}
archiveRes() {
# 归档测试结果
echo 'test_name,mode,avg,min,max,client_num,queries_per_client' >"${archive_dir}"/0.csv
cat "${archive_dir}"/*.csv >"${archive_dir}"/result.csv
rm -rf "${archive_dir}"/0.csv
log "##########################################################################"
log "测试结果: ${archive_dir}/result.csv "
# shellcheck disable=SC2002
resFmt=$(cat "${archive_dir}"/result.csv | column -t -s,)
log "${resFmt}"
log "#########################################################################"
}
main() {
log "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ sql性能测试工具 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
logFmt "Options 参数:${cmd_input}"
loadConf
showArgs
date_str=$(date "+%Y%m%d%H%M%S%3N")
archive_dir=./output/test-"${date_str}"
# 创建归档目录
mkdir -p "${archive_dir}"
runJobs
archiveRes
}
showInfo() {
echo """
================================================
# sql 性能测试工具 #
# 版本: 1.0.0 #
# 作者: Siu #
# Support By: mysqlslap #
================================================
"""
help
}
loadConf() {
# shellcheck source=src/
# 加载配置全局文件
if [ ! -f "${conf_file}" ]; then
logFmt "配置文件不存在将使用默认配置或命令行输入参数:${conf_file}"
else
. "${conf_file}"
# shellcheck disable=SC2027
logFmt "加载配置文件: ${conf_file}"
fi
}
showArgs() {
log "###############################################################################"
log "测试参数:"
log "db_ip=${db_ip}"
log "db_port=${db_port}"
log "db_schema=${db_schema}"
log "db_user=${db_user}"
log "client_num=${client_num}"
log "queries_limit=${client_queries_limit}"
log "###############################################################################"
}
help() {
echo """
Usage: ./run.sh -f ./myconfig/conf.file
./run.sh -j ./jobs
./run.sh -h 192.168.1.1
./run.sh -p 9001
./run.sh -u admin
./run.sh -P P@ssw0rd
Options:
-f 配置文件路径,默认:./config/conf
-j sql 任务路径,默认:./config/jobs
-H 数据库IP,默认:本机 IP
-p 数据库端口,默认:9030
-s 数据库Schema,默认:ssb
-u 数据库用户,默认:root
-P 数据库密码,默认:空(当前未加入Option)
-c 测试并发数,默认:10
-q 总查询次数,默认:10
-h 帮助信息
-v 工具版本信息
"""
}
#echo original parameters=[$@]
# https://www.jianshu.com/p/6393259f0a13
#-o或--options选项后面是可接受的短选项,如ab:c::,表示可接受的短选项为-a -b -c,
#其中-a选项不接参数,-b选项后必须接参数,-c选项的参数为可选的
#-l或--long选项后面是可接受的长选项,用逗号分开,冒号的意义同短选项。
#-n选项后接选项解析错误时提示的脚本名字
#ARGS=$(getopt -o ab:c:: --long along,blong:,clong:: -n "$0" -- "$@")
ARGS=$(getopt -o vhf:j:H:p:u:c:q: -n "$0" -- "$@")
if [ $? != 0 ]; then
logFmt "参数错误,退出..."
help
exit 1
fi
#echo ARGS=[$ARGS]
#将规范化后的命令行参数分配至位置参数($1,$2,...)
eval set -- "${ARGS}"
cmd_input=$(echo formatted parameters=[$@])
while true; do
case "$1" in
-v)
showInfo
exit 0
shift
;;
-h)
help
exit 0
shift
;;
-f)
conf_file=$2
shift 2
;;
-j)
jobs_path=$2
shift 2
;;
-H)
db_ip=$2
shift 2
;;
-p)
db_port=$2
shift 2
;;
-s)
db_schema=$2
shift 2
;;
-u)
db_user=$2
shift 2
;;
-c)
client_num=$2
shift 2
;;
-q)
#echo "option q:$2"
client_queries_limit=$2
shift 2
;;
--)
main
shift
break
;;
*)
help
exit 1
;;
esac
done