我用Super Smack做过几次MySQL性能测试。虽然可以用,但这种用c写的程序存在安装困难的问题:在阿里云的centos上可以装的上,在aws上未必就装的上;配置起来也很难受,那些个smack文件,太古怪。
后来我发现,其实用JMeter的jdbc request就可以对mysql性能测试。我用的模式是:
2. 将test plan文件上传到无干扰无界面的linux测试机上,以non-gui方式在测试机上执行测试,再收集结果。
JMeter支持server模式:用本地的jmeter控制测试机上的jmeter, 直接在本机上启动、停止、查看结果,用起来非常顺手。但它有个问题:在测试过程中,测试机上的jmeter要不停地从本地机器获取测试数据,并返回采样数据到本地机器。这些数据流动会增加测试机上JMeter的负担,使qps偏低。
所以我还是选择了在测试机上使用non-gui模式这种方案。下面给个完整的例子,例示jmeter jdbc request的使用。
例示:用JMeter对MySQL进行压力测试
测试目标
使用普通varchar作为主键,比起使用自增数字类型作为主键,到底会慢多少?这个性能损失是不是可以忽略不计?
测试数据准备
建两张差不多的表,一个使用数字user_id作主键,一个直接使用user_name
drop table if exists user_id_pk; drop table if exists user_name_pk; create table user_id_pk ( user_id bigint unsigned not null auto_increment , user_name varchar(50) not null description varchar(200) default null created_when datetime not null primary key(user_id), unique key idx_user_name (user_name) ) ;
create table user_name_pk ( user_name varchar(50) not null , description varchar(200) default null, created_when datetime not null, primary key(user_name) ) ;
生成两个数据文件,一个用于测试插入性能,一个用于在插入完后测试查询性能。 两个文件都是一百万行。
import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.math.RandomUtils; /** * 请设置-Xmx3072m */ public class GenCsvDataForUserTable { public static void main(String[] args) throws IOException { File rootDir = new File("/home/ec2-user/kentbench/usertable"); File forInsertFile = new File(rootDir, "for-insert.csv"); File forQueryFile = new File(rootDir, "for-query.csv"); rootDir.mkdirs(); forInsertFile.delete(); forQueryFile.delete(); int TOTAL_RECORDS = 1000000; int saveTheshhold = TOTAL_RECORDS / 10; List<String> forInsertLines = new ArrayList<String>(); List<String> forQueryLines = new ArrayList<String>(); for (int i = 1; i <= TOTAL_RECORDS; i++) { String userId = String.valueOf(i); String userName = UUID.randomUUID().toString(); String description = RandomStringUtils .randomAlphanumeric(RandomUtils.nextInt(200) + 1); String line = userId + "," + userName + "," + description; forInsertLines.add(line); // 同时也用于查询 forQueryLines.add(line); // 累积到一定量就存一下 if (i % saveTheshhold == 0) { System.out.println("done with No." + i); FileUtils.writeLines(forInsertFile, forInsertLines, true); // 用于查询的数据先弄散,再保存 Collections.shuffle(forQueryLines); FileUtils.writeLines(forQueryFile, forQueryLines, true); forInsertLines = new ArrayList<String>(); forQueryLines = new ArrayList<String>(); } } System.out.println("done with for-insert.csv. please check " + forInsertFile); System.out.println("done with for-query.csv. please check " + forQueryFile); } }
Test Plan配置
一个plan用来往两个表里插入数据,另一个用来从两个表里查询出数据。
以“插入”为例:
线程池配置
数据库JDBC配置
数据文件配置
这里会用到上面生成的数据文件。另外还为每列指定一个变量名,这些变量名在下面中会用到
JDBC Request配置 – 插入到以user_id为主键的表
“parameter values”对应的就是数据文件配置中定义的变量名
JDBC Request配置 – 插入到以user_name为主键的表
测试的执行
点一下JMeter中的“播放”按钮即可执行测试,看下基本的效果 (本地使用了gui模式, 为了查看执行效果,应该添加一个Listener如Summary Report. 此处不表,请自行google)
我们可以将这个test plan存储为一个*.jmx文件,再把它上传到一个无界面无干扰的测试环境中执行。但在执行这一步之前,应该禁掉一个jdbc request, 只保留另一个。否则JMeter会同时执行两个jdbc request, 这会干扰测试结果.
这里解释了为什么不应该同时执行。
(这个测完后再互换一下,然后再执行一次测试。)
在测试机上执行:
$jmeter -n -t my.jmx
执行完后查看控制台输出或者jmeter.log, 可以看到类似于这样的数据:
其中s = 2863.4/s就是吞吐量(tps), Avg:34是平均RT (ms)
补充说明
用于查询的Test Plan的配置办法与上述的用于插入的Plan的配法差不多,不过你一定要加上一个Assertion配置,以确保某条select语句符合我们的预期,确实查到了数据。
这个意思是每次查询返回一条记录。”_#”代表返回的记录数. firstColumn代表返回记录的第1个字段,这个变量名要在jdbc request中配置。
(对于这种记录数的Assertion, 是否通过第1个字段来判断根本无所谓,第2个字段,第N个字段都可以)
测试结果
场景: 逐渐插入数据,直到累积到100万条
rt | qps | |
用user_id当主键的表 | 34 | 2863.4 |
用user_name当主键的表 | 41 | 2346.8 |
下表比上表 | 慢20% | 低18% |
场景: 在100万条数据的表中查询100万次
rt | qps | |
用user_id当主键的表 | 16 | 5803.9 |
用user_name当主键的表 | 18 | 5270.7 |
下表比上表 | 慢12.5 | 低9% |
(机器:aws ec2 c3.large, cpu 2核, 内存3.75g, ssd硬盘,操作系统Amazon Linux AMI release, mysql 5.5.40 innodb)
结论:使用varchar作为主键,比使用id作为主键性能要差10%-20%,而且插入的性能差别比查询的性能差别更大。插入场景差别更大应该是因为varchar随机插入时会导致page split所致。
这个经历比起用Super Smack还是愉快的多的!