');}

JMeter压测入门

Author Avatar
Rico 5月 21, 2019

读完这篇文字,你将看到

  • 自定义变量模拟多用户

  • JMeter命令行使用

  • Redis压测工具redis-benchmark

文档

官网 http://jmeter.apache.org/

用户手册 http://jmeter.apache.org/usermanual/get-started.html

JMeter入门

设置好这个http请求默认值之后,接下来的http请求,这些配置就不需要填写了

如何查看结果呢?

聚合报告很概括的,如果想看详细的 ,选择图形结果,结果树,表格等

右击可以清除某次压测结果

由于我这台MBP是固态硬盘,且性能较高,16G的内存,吞吐量未经优化有400以上,所以我将程序部署到了一台老旧的3G内存,主频1.5GHZ,,机械硬盘的装有Ubuntu系统,且里面已经跑了一个gitlab服务的华硕笔记本上,这台笔记本2010年买的,已经8年了,硬件参数如下

压测数据如下

1000的并发,吞吐率只有24

分析代码,代码如下

  1. @Autowired
  2. GoodsService goodsService;
  3. @RequestMapping("/to_list")
  4. public String toList(Model model, SecondKillUser user){
  5. model.addAttribute("user",user);
  6. //查询商品列表
  7. ArrayList<GoodsVo> goods=goodsService.listGoodsVo();
  8. model.addAttribute("goodsList",goods);
  9. return "goods_list";
  10. }

由于我们没用cookie发送,所以瓶颈不在redis上,应该在数据库上

我在没运行压测之前运行top命令和压测中运行top命令

压测中负载达到了10

把Java应用和数据库分开,下图是该Ubuntu服务器单独承担mysql的压力的截图

JMeter自定义变量

上面我们压测的请求没有带任何参数

我们新建一个Controller

  1. @Controller
  2. @RequestMapping("/user")
  3. public class UserController {
  4. @RequestMapping("/info")
  5. @ResponseBody
  6. public Result<SecondKillUser> info(Model model, SecondKillUser user) {
  7. return Result.success(user);
  8. }
  9. }

我们这个用户可以通过参数token或者cookie token 来从redis中获取

JMeter支持Cookie参数的

我们为了方便直接在参数里面将用户的md5 传入

上面的商品列表不需要压测就禁用

我们将Token从浏览器中复制出来

这个的QPS比上一个请求的要高很多,因为请求的是redis,缓存

于此同时redis和jdbc的配置可以改高点儿

  1. # 连接超时时间
  2. redis.timeout=3
  3. redis.password=root
  4. # 连接池的配置
  5. # 最大连接数
  6. redis.poolMaxTotal=10
  7. # 最大空闲
  8. redis.poolMaxIdle=10
  9. # 最大等待
  10. redis.poolMaxWait=3
  11. spring.datasource.maxActive=2
  12. spring.datasource.initialSize=1
  13. spring.datasource.minIdle=1

改为

  1. # 连接超时时间
  2. redis.timeout=10
  3. redis.password=root
  4. # 连接池的配置
  5. # 最大连接数
  6. redis.poolMaxTotal=1000
  7. # 最大空闲
  8. redis.poolMaxIdle=500
  9. # 最大等待
  10. redis.poolMaxWait=500
  11. #JDBC
  12. spring.datasource.maxActive=1000
  13. spring.datasource.initialSize=100
  14. #最大空闲
  15. spring.datasource.minIdle=500

如果我想模拟多用户请求怎么办呢?

我们建立一个txt文档jmeter_config.txt

  1. 13712341234,4997c8aff5054353bbc4acb611f2c38f
  2. ...可以多行 意思就是多个用户参数

Recycle on EOF是什么意思呢? 就是假如测1000个并发,但是只有100个token(用户参数),是否循环使用参数

Stop thread on EOF 到末尾是否停止

Variable Name相当于就是给2列起了名字 ,一个叫userid,一个叫usertoken

这样参数就得改成如下

命令行压测

如果在服务器上进行压测,但是服务器没有图形界面,就采用命令行压测

1在windows/MacOS/Linux上录好jmx

2命令行:sh jmeter.sh -n -t xxx.jmx -l result.jtl -n就是不使用图形界面的意思 -t是指定脚本,-l是输出结果

3把result.jtl导入到JMeter

Redis压测

1 redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 100个并发连接,100000个请求

2 redis-benchmark -h 127.0.0.1 -p 6379 -q -d 100 存取大小为100字节的数据包

redis本身提供了压测工具

ps -ef | grep redis

将jmeter软件和导出的jmx配置上传的纯命令行的Linux系统上,执行如下命令

测试期间 cpu负载也上来了

压测结束后将生成的result.jtl导入到带有图形界面的JMeter中

jmx文件下载good_list.jmx

结果文件下载goodslist_result.jtl

秒杀压测

接下来我们批量建立1000个用户的Token ,生成csv文件,使用JMeter中的CSV Data set config元件,来对秒杀接口进行压测

使用这段代码在数据库中生成1000个用户

  1. List<SecondKillUser> users = new ArrayList<SecondKillUser>(count);
  2. //生成用户
  3. for(int i=0;i<count;i++) {
  4. SecondKillUser user = new SecondKillUser();
  5. user.setId(13000000000L+i);
  6. user.setLoginCount(1);
  7. user.setNickname("user"+i);
  8. user.setRegisterDate(new Date());
  9. user.setSalt("1a2b3c");
  10. user.setPassword(MD5Util.inputPass2DBPass("123456", user.getSalt()));
  11. users.add(user);
  12. }//创建的用户除了nickname不一样,其他都一样,比如密码,salt等
  13. System.out.println("create user");
  14. //插入数据库
  15. Connection conn = DBUtil.getConn();
  16. String sql = "insert into secondkill_user(login_count, nickname, register_date, salt, password, id)values(?,?,?,?,?,?)";
  17. PreparedStatement pstmt = conn.prepareStatement(sql);
  18. for(int i=0;i<users.size();i++) {
  19. SecondKillUser user = users.get(i);
  20. pstmt.setInt(1, user.getLoginCount());
  21. pstmt.setString(2, user.getNickname());
  22. pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
  23. pstmt.setString(4, user.getSalt());
  24. pstmt.setString(5, user.getPassword());
  25. pstmt.setLong(6, user.getId());
  26. pstmt.addBatch();
  27. }
  28. pstmt.executeBatch();
  29. pstmt.close();
  30. conn.close();
  31. System.out.println("insert to db");

使用这段代码生成用户id+Token组成的csv文件 注意,上面的数据库所在的服务器和redis所在的服务器都是我们配置文件里面配置的ip,因为redis缓存和数据库用户都是要插入到这台服务器上的

  1. //登录,生成token
  2. //调我们自己的接口,生成userid和token的csv文件
  3. String urlString = "http://localhost:8088/login/do_login";
  4. File file = new File("/Users/xxxxxx/workspace/JavaWorkSpace/secondkill/tokens.txt");
  5. if(file.exists()) {
  6. file.delete();
  7. }
  8. RandomAccessFile raf = new RandomAccessFile(file, "rw");
  9. file.createNewFile();
  10. raf.seek(0);
  11. for(int i=0;i<users.size();i++) {
  12. SecondKillUser user = users.get(i);
  13. URL url = new URL(urlString);
  14. HttpURLConnection co = (HttpURLConnection)url.openConnection();
  15. co.setRequestMethod("POST");
  16. co.setDoOutput(true);
  17. OutputStream out = co.getOutputStream();
  18. String params = "mobile="+user.getId()+"&password="+MD5Util.inputPassMD5("123456");//拼装参数
  19. out.write(params.getBytes());
  20. out.flush();
  21. InputStream inputStream = co.getInputStream();
  22. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  23. byte buff[] = new byte[1024];
  24. int len = 0;
  25. while((len = inputStream.read(buff)) >= 0) {
  26. bout.write(buff, 0 ,len);
  27. }
  28. inputStream.close();
  29. bout.close();
  30. String response = new String(bout.toByteArray());
  31. JSONObject jo = JSON.parseObject(response);
  32. String token = jo.getString("data");//返回的数据是这样的{"code":0,"msg":null,"data":"fbd690a8f574485e974b585daab160e4"},所以要获取data
  33. System.out.println("create token : " + user.getId());
  34. String row = user.getId()+","+token;//userid+token
  35. raf.seek(raf.length());
  36. raf.write(row.getBytes());
  37. raf.write("\r\n".getBytes());
  38. System.out.println("write to file : " + user.getId());
  39. }
  40. raf.close();
  41. System.out.println("over");

同时redis中也有数据了

我们生成token的csv文件其实就是模拟这1000个用户登陆,然后登陆过程会在redis中保存用户token,同时生成csv文件用于压测工具作为参数传参

数据有了,开始配置JMeter

有这2个参数时因为秒杀接口需要goodsId和token

未压测前的库存

压测中的性能

压测后的库存,超卖了122 ,非常严重 ,要是上线了就亏大了.要是并发量不大,这种问题永远不会暴露出来

秒杀订单

吞吐量

这里我JMeter是跑在我的Mac上的,去测我的Linux机器,所以JMeter并没有影响这台机器的性能,结果还是比较准的

我对第二个商品也进行了秒杀压测,不过这次我把应用和mysql+redis分开放了

应用跑在mac上,请求的mysql和redis在linux机器上

压测前库存

压测后库存

吞吐量 比上面那种方式高了

解决卖超

原来减库存的sql是这样的

  1. @Update("update secondkill_goods set stock_count=stock_count-1 where goods_id=#{id}")
  2. public void reduceStock(SecondKillGoods secondKillGoods);

现在改成

  1. update secondkill_goods set stock_count=stock_count-1 where goods_id=#{id} and stock_count>0

而且原来的代码对于1个用户同时发起多个请求进行秒杀,可能会出现同一个用户秒杀到多次的情况

结论:使用数据库的唯一索引,这也是为什么秒杀表要单拉出一个表的原因

重新压测

没有卖超了

吞吐量

This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://hogwartsrico.github.io/2019/05/21/jmeter/