一、JMeter 如何通过自定义Sample来压测RPC服务
RPC(Remote Procedure Call)俗称远程过程调用,是常用的一种高效的服务调用方式,也是性能压测时经常遇到的一种服务调用形式。常见的RPC有GRPC、Thrift、Dubbo等。这里以GRPC为例介绍在JMeter中如何添加自定义的Sample来压测GRPC服务,JMeter中提供的Sample如下图所示,从中可以看到并没有我们需要压测GRPC的Sampler。
本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》
但是从图中可以看到,JMeter中提供了Java 请求Sample,因此我们可以编写一个自定义的Java请求的Sample来实现GRPC调用,由于需要自定义,自然就需要新建一个Java语言的Maven项目,在项目中引入如下jar包依赖,jar包的版本需要跟压测时的JMeter工具版本保持一致。由于笔者用的JMeter工具的版本是3.0,所以如下依赖包选择的也是3.0版本。由于本节需要一些Java语言和Maven项目管理的基础,所以对于这块不熟悉的读者可以预先阅读一些关于这块的基础书籍。
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_java</artifactId>
<version>3.0</version>
</dependency>
项目中除了需要增加JMeter的依赖外,还需要增加GRPC的依赖,Maven项目完整的pom内容如下所示。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>jmeter.tools</groupId> <artifactId>jmeter-grpc</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <properties> <grpc.version>1.27.0</grpc.version> </properties> <dependencies> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty</artifactId> <version>${grpc.version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>${grpc.version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>${grpc.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.jmeter/ApacheJMeter_java --> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_java</artifactId> <version>3.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.jmeter/ApacheJMeter_core --> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_core</artifactId> <version>3.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${java.version}</source> <target>${java.version}</target> <skip>true</skip> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.8</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}</outputDirectory> <overWriteReleases>true</overWriteReleases> <overWriteSnapshots>true</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> <useSubDirectoryPerType>true</useSubDirectoryPerType> <includeArtifactIds> guava </includeArtifactIds> <silent>true</silent> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> <defaultGoal>compile</defaultGoal> </build> </project>
编写一个自定义的Java请求Sample,只需要实现JMeter提供的JavaSamplerClient接口即可,如下所示。
本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》
import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.JavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; public class ExampleSample implements JavaSamplerClient { @Override public void setupTest(JavaSamplerContext javaSamplerContext) { //初始化方法,对数据进行初始化,该方法只会执行一次 } @Override public SampleResult runTest(JavaSamplerContext javaSamplerContext) { //Sample的请求的具体实现 return null; } @Override public void teardownTest(JavaSamplerContext javaSamplerContext) { //数据或者资源销毁接口,一般用于压测停止时,需要做的动作。 } @Override public Arguments getDefaultParameters() { //参数设置方法,一般用于设置传递参数 return null; } }
JMeter提供的JavaSamplerClient接口需要实现的四个方法,如下表所示。
表: JavaSamplerClient接口需要实现的四个方法说明
方法 |
描述 |
setupTest(JavaSamplerContext javaSamplerContext) |
初始化方法。一般用于对数据进行初始化。性能压测时该方法只会被执行一次,方法体里面的内容可以为空 |
runTest(JavaSamplerContext javaSamplerContext) |
Sample请求的具体实现。比如调用GRPC服务就需要在该方法中编写调用GRPC服务的代码 |
teardownTest(JavaSamplerContext javaSamplerContext) |
用于数据或者资源销毁的方法。一般用于压测停止时,需要执行的数据或者资源的释放动作。性能压测时该方法也只会被执行一次,方法体里面的内容同样可以为空 |
getDefaultParameters() |
参数设置方法。一般用于设置传递的参数 |
GRPC示例:以传入用户名和密码进行用户注册的GRPC服务作为示例,该GRPC接口请求输入和响应输出都是JSON的文本形式,GRPC服务的proto文件内容如下(proto是GRPC提供的接口协议定义标准文档):
syntax = "proto3"; package com.zyq.example.cas.management.grpc; message RequestData { string text = 1; } message ResponseData { string text = 1; } service StreamService { //rpc服务的方法 rpc SimpleFun(RequestData) returns (ResponseData){} }
服务接口详细说明如下表示。
表: 服务接口详细说明
参数 |
说明 |
RequestData |
定义了文本类型的参数用于GRPC服务的请求入参使用,比如传入JSON: {"userAccount":"zyq","password":"mima"} |
ResponseData |
定义了文本类型的参数用于请求响应使用,用于存储GRPC服务调用后响应的文本内容 |
StreamService |
定义了一个GRPC服务,并且服务里面包含了SimpleFun这个方法,方法中请求传入RequestData,调用完成后返回ResponseData |
本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》
请求调用过程如下图所示。
服务器的配置信息如下表所示。
表: 服务器的配置说明
服务器类型 |
配置说明 |
应用服务器(GRPC) |
内存:2G CPU:4核 部署软件:GRPC Java应用服务、JDK1.8 操作系统:CentOS7 |
数据库服务器 |
内存:2G CPU:2核 部署软件:MySQL 操作系统:CentOS7 本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》 |
笔者这里自己实现的GRPC服务的Sample具体示例代码如下:
import com.cf.cas.management.grpc.Example; import com.cf.cas.management.grpc.StreamServiceGrpc; import com.google.gson.Gson; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.JavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; import java.util.HashMap; import java.util.Map; /** * Created by zyq on 2020/3/4. */ public class GrpcJmeter implements JavaSamplerClient { private String userAccount; private String password; private String address; private Integer port; @Override public void setupTest(JavaSamplerContext javaSamplerContext) { } @Override public SampleResult runTest(JavaSamplerContext javaSamplerContext) { SampleResult results = new SampleResult(); userAccount = javaSamplerContext.getParameter("userAccount"); // 获取在JMeter中设置的参数值 password = javaSamplerContext.getParameter("password"); // 获取在JMeter中设置的参数值 address = javaSamplerContext.getParameter("address"); // 获取在JMeter中设置的参数值 port =Integer.valueOf(javaSamplerContext.getParameter("port")) ; // 获取在JMeter中设置的参数值 results.sampleStart();// JMeter 开始统计响应时间标记 ManagedChannel channel=null; try { //grpc调用的具体实现 channel = ManagedChannelBuilder.forAddress(address, port).usePlaintext().build(); StreamServiceGrpc.StreamServiceBlockingStub stub = StreamServiceGrpc.newBlockingStub(channel); Map<String,Object> map = new HashMap<>(); map.put("userAccount",userAccount); map.put("password",password); Gson gson = new Gson(); Example.RequestData requestData = Example.RequestData.newBuilder().setText(gson.toJson(map)).build(); Example.ResponseData responseData = stub.simpleFun(requestData); //设置请求的数据,这里设置后,在JMeter的察看结果树中才可显示 results.setRequestHeaders(gson.toJson(map)); if(null!=responseData && null!=responseData.getText() && responseData.getText().contains("success")){ results.setSuccessful(true); } else { results.setSuccessful(false); } //设置响应的数据,这里设置后,在JMeter的察看结果树中才可显示 results.setResponseMessage(responseData.getText()); results.setResponseData(responseData.getText(),"UTF-8"); } catch (Exception e) { results.setSuccessful(false); e.printStackTrace(); } finally { if(null!=channel){ channel.shutdown(); } results.sampleEnd();// JMeter 结束统计响应时间标记 } return results; } @Override public void teardownTest(JavaSamplerContext javaSamplerContext) { } @Override public Arguments getDefaultParameters() { Arguments params = new Arguments(); params.addArgument("userAccount", "zyq");//设置参数,并赋予默认值 params.addArgument("password", "111");//设置参数,并赋予默认值 params.addArgument("address", "127.0.0.1");//设置参数,并赋予默认值 params.addArgument("port", "8883");//设置参数,并赋予默认值 return params; } }
本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》
示例编写完成后,执行Maven项目打包命令mvn assembly:assembly,即可生成性能压测时需要放入JMeter中的jar包,如下图所示。
将生成的jmeter-grpc-1.0-SNAPSHOT.jar放入JMeter工具的apache-jmeter-3.0\apache-jmeter-3.0\lib\ext目录下,如下图所示,JMeter的ext目录专门用于存放扩展的JMeter自定义jar包。
放入后打开JMeter工具,在添加Java请求Sample后,即可看到我们自己编写的自定义GRPC服务Sample了,如下图所示。
在JMeter工具中执行请求调用后,即可在察看结果树这个JMeter元件中看到请求调用的结果,如下所示。
由此可见,JMeter支持的功能其实非常强大,理论上只要Java语言可以调用的服务都可以使用JMeter来做性能压测。
二、JMeter对GRPC服务的性能压测分析与调优
在添加完GRPC服务的Sample后,我们在上图的基础上,增加Summary Report、聚合报告、图形结果、响应断言、计数器这几个JMeter元件,以辅助我们做性能压测。其中计数器是本次用来辅助做参数化的,如下图所示,在图中userAccount和password这两个参数都用到了计数器产生的counter变量来构造数据,由于计数器是递增的,所以保证了构造出来的数据不会重复。
JMeter的性能压测脚本准备完成后,采用10个并发用户开始进行压测,如下图所示。
未完待续........(中间省略的部分请查看原书)
使用jvisualvm工具,查看jvm进程的线程运行情况如下图所示。可以看到由于是10个并发用户,所以GRPC服务端的默认执行线程也是10个,但是从图中可以看到这些线程大部分时间都不是处于真正的运行状态,而是处于监视状态,由此怀疑服务端应用程序多线程并发处理时可能遇到了同步锁争抢。
未完待续........(中间省略的部分请查看原书)
从代码中可以看到,这段代码使用同步锁来保证插入到数据中的用户账号不会重复,每次插入前都需要先查询数据库中是否存在该账号,如果不存在才插入,同步锁是用来保证并发调用时线程安全的,确保数据库中不会出现重复的脏数据。
针对上述情况,分析总结如下:
- 代码中虽然使用了同步锁保证了线程安全,使数据库中不出现重复的脏数据,但是却影响了多线程并发时的性能。而且此种线程安全只能适用单个应用服务器节点的部署情况,如果是分布式的多个节点部署方案,则此种同步锁无法奏效,此时一般需要借助分布式同步锁,比如借助Redis、Zookeeper来实现分布式同步锁。但是使用这种分布式同步锁,其并发性能一般也很低效。
- 除了使用同步锁来保证数据不重复插入这种方式外,还可以使用数据库的唯一索引来保证数据库的数据唯一。比如针对本示例中的情况,可以对数据库表中的用户账号字段建立唯一索引,确保不重复插入,虽然使用唯一索引后,数据库肯定会有性能消耗,但是在数据量不是非常大的时候,这种方式性能效果应该更佳,而且由于需要根据用户账号查询,所以在查询时,也是需要索引来提高查询效率。
- 针对数据库中用户表中的数据量非常大的情况,还可以采用分表的方案。比如可以针对用户账号基于某种算法做分表处理,确保同一个用户账号采用算法计算时每次都是进入同一个表中,这样还是可以对每张分表中的用户账号字段建立唯一索引来提高性能。
本文作者:张永清, 转载请注明: https://www.cnblogs.com/laoqing/p/16339979.html 来源于博客园 ,本文摘选自《软件性能测试分析与调优实践之路》
备注:作者的原创文章,转载须注明出处。原创文章归作者所有,欢迎转载,但是保留版权。对于转载了博主的原创文章,不标注出处的,作者将依法追究版权,请尊重作者的成果。
关于软件性能分析调优,可以加微信号yq597365581或者微信号hqh345932,进入专业的性能分析调优群进行交流沟通。