一、为什么要用JMeter JDBC协议做数据库压测
很多做开发或者运维的朋友,可能都遇到过这么个糟心事儿:上线前功能测了几十遍都没问题,一到用户多的晚上,系统突然卡得要死,查日志半天找不到前端的问题,最后定位到是数据库扛不住了。比如之前我帮一个做电商的朋友排查过,他们搞大促前,前端页面测了都流畅,结果大促当天用户一刷新商品列表,页面直接转圈圈,最后发现是商品表的查询请求太集中,数据库CPU直接拉满到100%,连正常的订单数据都写不进去。
这时候如果提前做了数据库的压测,就能在上线前发现问题,不会等到出事故才补救。而JMeter的JDBC协议,就是专门用来测数据库性能的工具——它能直接模拟应用程序给数据库发请求,比如查数据、写数据,然后看数据库在不同压力下的表现,比如会不会慢、会不会报错、资源会不会炸。
可能有人会问:我为什么不直接用JMeter的HTTP协议测?比如模拟用户点页面,看数据库的表现?其实HTTP协议测的是整个系统的性能,包括前端、后端、网络、数据库,最后定位到数据库出问题,你不知道是数据库本身慢,还是后端的代码写得烂(比如没加索引、查了多余的字段)。而JDBC协议是直接测数据库的,排除了中间所有环节的干扰,能精准找到数据库的瓶颈。
二、测试前的准备工作
在开始压测之前,得先把环境和工具都搭好,不然跑一半出问题,排查起来更麻烦。这里分几步说,每一步都有具体的要求。
2.1 准备压测环境
首先得有一个和生产环境几乎一模一样的数据库,包括硬件配置(CPU、内存、硬盘)、软件版本(比如MySQL是5.7还是8.0)、数据量(比如生产环境的商品表有100万条,测试环境不能只有1万条)。如果测试环境比生产环境差,压出来的结果就不准,比如测试环境能扛1000个请求,生产环境可能连500都扛不住;如果数据量太少,比如查一个小表,数据库能秒返回,但生产环境的大表可能要几秒,压测就没意义了。
然后是JMeter的配置,得把JMeter的内存调大一点,不然压测的时候JMeter自己先卡了,测出来的结果也不对。比如默认的JMeter内存可能只有1G,压测的时候要开几百个线程,就会内存溢出。调内存的方法是找到JMeter安装目录下的jmeter.bat(Windows)或者jmeter.sh(Linux),修改里面的JVM参数。比如Windows下的jmeter.bat,找到这一行:
set JVM_ARGS=-Xms1g -Xmx1g
把它改成:
set JVM_ARGS=-Xms4g -Xmx4g
意思是最小内存4G,最大内存4G,根据你自己的电脑配置来,一般压测的时候电脑至少要留2G内存给系统用,比如你的电脑有16G内存,就可以设成-Xms8g -Xmx8g。
2.2 准备JDBC驱动
JMeter要和数据库连接,得有对应的驱动包,比如MySQL的驱动是mysql-connector-java,Oracle的是ojdbc8,PostgreSQL的是postgresql。这个驱动包的版本要和你用的数据库版本匹配,比如MySQL 8.0用mysql-connector-java 8.0.x,MySQL 5.7用5.1.x,不然会连接失败。
驱动包的放法很简单:把下载好的jar包,放到JMeter安装目录下的lib文件夹里,然后重启JMeter,这样JMeter就能识别到驱动了。
2.3 准备压测数据
压测用的SQL不能是固定的,比如你要测商品表的查询,不能每次都查id=1的商品,这样数据库会把这个数据放到缓存里,第二次查就快了,测不出真实的性能。得用随机的或者动态的参数,比如随机生成一个商品id,或者从一个CSV文件里读id,这样每次查的都是不同的商品,更贴近真实场景。
比如我们要测的是电商的商品表查询,表结构大概是这样的:
CREATE TABLE goods (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品id',
name VARCHAR(100) NOT NULL COMMENT '商品名称',
price DECIMAL(10,2) NOT NULL COMMENT '商品价格',
stock INT NOT NULL COMMENT '库存',
create_time DATETIME NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
我们可以提前往测试环境的这个表里插100万条数据,然后把id的范围(比如1到1000000)存到一个CSV文件里,压测的时候每次从这个文件里取一个id,作为查询的参数。
三、具体的测试方案设计
准备工作做完了,接下来就是核心的测试方案设计,这里分几个部分,每个部分都有具体的步骤和示例。
3.1 确定压测的核心指标
在压测之前,得先知道我们要测什么,也就是核心指标,这些指标是判断数据库性能好不好的标准。常见的核心指标有这几个:
- 响应时间:就是从发送请求到收到响应的时间,比如查一个商品的时间,这个时间越短越好,一般要求大部分请求(比如95%)的响应时间不超过1秒。
- TPS:每秒处理的请求数,比如每秒能查多少个商品,这个数值越高,说明数据库的性能越好。
- 错误率:压测过程中报错的请求占总请求的比例,比如连接超时、查询失败,这个比例越低越好,最好是0。
- 资源使用率:压测过程中数据库的CPU、内存、磁盘IO、网络的使用率,比如CPU不能超过80%,内存不能超过90%,磁盘IO不能超过90%,不然就会出现瓶颈。
3.2 设计压测场景
压测场景就是模拟真实的用户行为,比如大促的时候,用户会频繁查商品、下订单、改订单,这些行为对应的数据库操作就是查商品表、写订单表、改订单表。我们要把这些操作组合起来,设计成压测场景。
比如我们设计一个电商大促的压测场景,包含三个操作:查商品、写订单、改订单,每个操作的比例是70%(查商品)、20%(写订单)、10%(改订单),因为大促的时候查商品的人最多,写订单的次之,改订单的最少。
接下来我们就用JMeter来实现这个场景,技术栈是JMeter 5.4.3 + MySQL 8.0,所有的操作都用这个技术栈。
首先,打开JMeter,新建一个测试计划,然后添加一个线程组,线程组是用来模拟用户的,比如我们要模拟100个用户同时操作,线程数就设成100。
然后,添加一个JDBC连接配置,用来连接数据库。右键线程组→添加→配置元件→JDBC Connection Configuration,然后配置里面的参数:
- Variable Name:随便起个名字,比如mysql_conn,这个名字要和后面的JDBC请求里的变量名一致。
- Database URL:数据库的地址,比如jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8,这里的test_db是数据库名,serverTimezone=GMT%2B8是时区,必须加,不然会报错。
- JDBC Driver class:数据库的驱动类,MySQL 8.0是com.mysql.cj.jdbc.Driver,MySQL 5.7是com.mysql.jdbc.Driver。
- Username:数据库的用户名,比如root。
- Password:数据库的密码,比如123456。
配置好之后,我们可以先测试一下连接,右键这个配置元件→添加→测试取样器→JDBC Request,然后写一个简单的查询语句,比如SELECT 1;,然后运行,看能不能成功。
接下来,我们要添加参数化的配置,比如从CSV文件里读商品id。右键线程组→添加→配置元件→CSV Data Set Config,配置里面的参数:
- Filename:CSV文件的路径,比如D:/jmeter_data/goods_id.csv,这个文件里只有一列,内容是1到1000000的数字,每行一个。
- Variable Names:变量名,比如goods_id。
- Delimiter:分隔符,因为CSV里只有一列,所以可以不填。
- Allow quoted data?:False。
- Recycle on EOF?:True,意思是读完所有的id之后,从头开始读。
- Stop thread on EOF?:False。
- Sharing mode:All threads,意思是所有的线程共享这个CSV文件,每个线程取不同的id。
然后,我们添加三个JDBC请求,分别对应查商品、写订单、改订单:
第一个是查商品的JDBC请求:右键线程组→添加→取样器→JDBC Request,配置里面的参数:
- Variable Name:mysql_conn,和之前的JDBC连接配置的变量名一致。
- Query Type:Select Statement,因为是查询语句。
- Query:写查询的SQL,比如:
SELECT * FROM goods WHERE id = ?;
这里的?是占位符,代表参数。
- Parameter Values:填参数,也就是我们之前定义的变量名${goods_id}。
- Parameter Types:VARCHAR,因为id是整数,这里可以填VARCHAR或者INT。
第二个是写订单的JDBC请求:同样添加一个JDBC Request,配置参数:
- Variable Name:mysql_conn。
- Query Type:Update Statement,因为是写数据。
- Query:写插入订单的SQL,比如:
INSERT INTO orders (goods_id, user_id, price, create_time) VALUES (?, ?, ?, NOW());
这里有三个占位符,分别是商品id、用户id、价格。
- Parameter Values:${goods_id}, ${__Random(1,100000,user_id)}, ${__Random(10,1000,price)},这里的__Random是JMeter的函数,用来生成随机数,比如生成1到100000之间的随机用户id,10到1000之间的随机价格。
- Parameter Types:VARCHAR,VARCHAR,VARCHAR。
第三个是改订单的JDBC请求:再添加一个JDBC Request,配置参数:
- Variable Name:mysql_conn。
- Query Type:Update Statement。
- Query:写修改订单的SQL,比如:
UPDATE orders SET status = 2 WHERE id = ?;
这里的?是订单id,我们可以从之前的写订单请求里获取订单id,所以需要添加一个变量。右键写订单的JDBC请求→添加→后置处理器→JDBC Request,然后在这个后置处理器里配置:
- Variable Name:mysql_conn。
- Query Type:Select Statement。
- Query:SELECT LAST_INSERT_ID() AS order_id;,这个SQL是获取刚刚插入的订单的id。
- Result Variable Name:order_id,这样就会把订单id存到变量order_id里。 然后改订单的JDBC请求的Parameter Values填${order_id}。
接下来,我们要给这三个请求设置比例,也就是70%的查商品,20%的写订单,10%的改订单。右键线程组→添加→逻辑控制器→随机控制器,然后把三个JDBC请求放到随机控制器下面,每个请求的权重分别设为70、20、10,这样就会按照这个比例执行。
3.3 分阶段执行压测
压测不能一下子就开1000个线程,得循序渐进,分阶段来,比如先开10个线程,然后20、50、100、200、500,每个阶段跑5分钟,看数据库的表现,直到找到瓶颈。
每个阶段的步骤是:
- 调整线程组的线程数,比如10个。
- 运行压测脚本,跑5分钟。
- 查看压测结果,比如响应时间、TPS、错误率,同时查看数据库的资源使用率(比如用top命令看CPU,free命令看内存,iostat命令看磁盘IO)。
- 如果这个阶段的所有指标都符合要求,比如响应时间不超过1秒,错误率为0,CPU不超过80%,就进入下一个阶段;如果有指标不符合,就说明这个压力下数据库已经有瓶颈了,停下来分析。
比如我们分阶段压测:
- 第一阶段:10个线程,跑5分钟,结果:平均响应时间100ms,TPS 50,错误率0,CPU 10%,内存20%,没问题。
- 第二阶段:20个线程,跑5分钟,结果:平均响应时间150ms,TPS 100,错误率0,CPU 20%,内存25%,没问题。
- 第三阶段:50个线程,跑5分钟,结果:平均响应时间300ms,TPS 200,错误率0,CPU 40%,内存30%,没问题。
- 第四阶段:100个线程,跑5分钟,结果:平均响应时间800ms,TPS 300,错误率0,CPU 70%,内存40%,没问题。
- 第五阶段:200个线程,跑5分钟,结果:平均响应时间2s,TPS 400,错误率0,CPU 90%,内存50%,这时候响应时间超过了1秒,CPU也快到瓶颈了,说明数据库的瓶颈在CPU。
3.4 分析瓶颈原因
找到瓶颈之后,就要分析原因,比如上面的例子里,瓶颈在CPU,可能的原因有几个:
- 没有加索引:比如查商品的时候,id是主键,已经加了索引,那可能是查订单的时候,没有给订单的商品id加索引,导致每次查都要全表扫描,消耗大量的CPU。
- 慢查询太多:比如有一些复杂的SQL,比如多表关联查询,没有优化,导致执行时间长,CPU使用率高。
- 数据库的配置问题:比如MySQL的innodb_buffer_pool_size设置得太小,导致缓存不够,频繁的从磁盘读数据,消耗CPU。
- 硬件问题:比如CPU的性能不够,或者磁盘的IO不够,导致CPU等待数据。
怎么分析呢?首先看慢查询日志,MySQL的慢查询日志会记录执行时间超过1秒的SQL,我们可以打开慢查询日志,看有没有对应的SQL。比如打开慢查询日志的配置:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
然后压测的时候,看慢查询日志里的SQL,比如有没有一个查订单的SQL,每次执行都要2秒,然后看这个SQL的执行计划:
EXPLAIN SELECT * FROM orders WHERE goods_id = 123;
如果执行计划里的type列是ALL,说明是全表扫描,没有加索引,这时候就需要给goods_id加索引:
ALTER TABLE orders ADD INDEX idx_goods_id (goods_id);
加完索引之后,再重新压测,看CPU的使用率会不会降下来,响应时间会不会变短。
四、测试的应用场景、优缺点和注意事项
4.1 应用场景
这个测试方案的应用场景主要有几个:
- 数据库上线前的性能验证:比如新上线一个业务,或者对数据库做了升级(比如版本升级、硬件升级),上线前用这个方案测一下,确保数据库能扛住预期的压力。
- 瓶颈排查:比如线上系统出现性能问题,怀疑是数据库的问题,用这个方案直接测数据库,排除中间环节的干扰,快速定位瓶颈。
- 优化效果验证:比如对数据库做了优化(比如加索引、优化SQL、调整配置),用这个方案测一下,看优化后的性能有没有提升。
- 容量规划:比如要预估未来一年的用户增长,需要多少数据库资源,用这个方案测不同压力下的数据库表现,估算需要的CPU、内存、磁盘等资源。
4.2 技术优缺点
这个测试方案的优点很明显:
- 精准:直接测数据库,排除了前端、后端、网络等中间环节的干扰,能精准找到数据库的瓶颈。
- 灵活:可以模拟各种数据库操作,比如查、写、改、删,还可以设置不同的操作比例,贴近真实场景。
- 成本低:JMeter是免费的,只需要准备测试环境和驱动包,不需要额外的成本。
- 结果直观:能输出响应时间、TPS、错误率等指标,还能结合数据库的资源使用率,快速判断瓶颈。
当然也有缺点:
- 测试环境要求高:需要和生产环境几乎一模一样的数据库,包括硬件、软件、数据量,不然测试结果不准,而搭建这样的测试环境成本比较高。
- 不能完全模拟真实场景:虽然可以设置操作比例,但真实场景的用户行为更复杂,比如用户的操作顺序、思考时间、并发模式等,很难完全模拟。
- 对测试人员的要求高:需要熟悉JMeter的使用,熟悉数据库的配置和优化,不然可能会出现测试脚本写错,导致结果不准的情况。
4.3 注意事项
做这个测试的时候,有几个注意事项要特别注意:
- 不能在生产环境压测:压测会给数据库带来很大的压力,可能会影响生产环境的正常运行,所以一定要在测试环境做。
- 压测前要备份数据:如果压测的时候有写操作,可能会修改测试环境的数据,所以压测前要备份数据,压测后恢复。
- 要控制压测的时间和压力:不要一下子开太多线程,或者跑太长时间,避免测试环境的数据库宕机。
- 要考虑数据库的缓存:比如第一次查数据的时候,数据库会把数据放到缓存里,第二次查就快了,所以压测的时候要等缓存预热之后再看结果,或者用不同的参数,避免缓存的影响。
- 要结合多个指标分析:不能只看一个指标,比如TPS高,但响应时间也很高,或者错误率很高,都说明性能不好,要结合响应时间、TPS、错误率、资源使用率等多个指标一起分析。
五、文章总结
总的来说,用JMeter JDBC协议排查数据库性能瓶颈的测试方案,是一个精准、灵活、成本低的方法,适合各种数据库相关的性能测试场景。只要按照正确的步骤来,先准备好测试环境和工具,然后设计符合真实场景的压测脚本,分阶段执行压测,最后分析结果,就能快速找到数据库的瓶颈,提前优化,避免线上出现性能问题。
当然,这个方案也不是万能的,它也有自己的局限性,比如对测试环境的要求高,不能完全模拟真实场景,所以在实际使用的时候,要结合自己的业务情况,灵活调整,比如如果是复杂的分布式系统,可能还需要结合其他的压测工具,比如Locust、k6,或者结合全链路压测的方法,来更全面的评估系统的性能。
最后要提醒大家的是,性能测试是一个需要反复迭代的过程,不是一次就能完成的,比如第一次压测发现了瓶颈,优化之后要再压测,直到所有的指标都符合要求,这样才能确保上线后的系统稳定运行。
评论
围绕“JMeter JDBC协议压测:排查数据库性能瓶颈时的测试方案设计”参与讨论