玖叶教程网

前端编程开发入门

性能工具之Jmeter压测Thrift RPC服务

概述

Thrift是一个可互操作和可伸缩服务的框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 等等编程语言间无缝结合的、高效的服务。

Thrift最初由facebook开发,07年四月开放源码,08年5月进入apache孵化器。thrift允许你定义一个简单的定义文件中的数据类型和服务接口(IDL)。以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨编程语言。

其传输数据采用二进制格式,相对于XML和JSON等序列化方式体积更小,对于高并发、大数据量和多语言的环境更有优势。 Thrift它含有三个主要的组件:protocol,transport和server,其中,protocol定义了消息是怎样序列化的,transport定义了消息是怎样在客户端和服务器端之间通信的,server用于从transport接收序列化的消息,根据protocol反序列化之,调用用户定义的消息处理器,并序列化消息处理器的响应,然后再将它们写回transport。

官网地址:thrift.apache.org

基本概念

架构图

堆栈的顶部是从Thrift定义文件生成的代码。Thrift 服务生成的客户端和处理器代码。这些由图中的棕色框表示。红色框为发送的数据结构(内置类型除外)也会生成代码。协议和传输是Thrift运行时库的一部分。因此使用Thrift可以定义服务,并且可以自由更改协议和传输,而无需重新生成代码。 Thrift还包括一个服务器基础结构,用于将协议和传输绑定在一起。有可用的阻塞,非阻塞,单线程和多线程服务器。 堆栈的“底层I / O”部分根据所开发语言而有所不同。对于Java和Python网络I / O,Thrift库利用内置库,而C ++实现使用自己的自定义实现。

数据类型:

基本类型:

  • bool:布尔值,true 或 false,对应 Java 的 boolean
  • byte:8 位有符号整数,对应 Java 的 byte
  • i16:16 位有符号整数,对应 Java 的 short
  • i32:32 位有符号整数,对应 Java 的 int
  • i64:64 位有符号整数,对应 Java 的 long
  • double:64 位浮点数,对应 Java 的 double
  • string:未知编码文本或二进制字符串,对应 Java 的 String

结构体类型:

  • struct:定义公共的对象,类似于 C 语言中的结构体定义,在 Java 中是一个 JavaBean

集合类型:

  • list:对应 Java 的 ArrayList
  • set:对应 Java 的 HashSet
  • map:对应 Java 的 HashMap

异常类型:

  • exception:对应 Java 的 Exception

服务类型:

  • service:对应服务的类

数据传输层Transport

  • TSocket —— 使用阻塞式 I/O 进行传输,是最常见的模式
  • TFramedTransport —— 使用非阻塞方式,按块的大小进行传输,类似于 Java 中的 NIO,若使用 TFramedTransport 传输层,其服务器必须修改为非阻塞的服务类型
  • TNonblockingTransport —— 使用非阻塞方式,用于构建异步客户端

数据传输协议Protocol

Thrift 可以让用户选择客户端与服务端之间传输通信协议的类别,在传输协议上总体划分为文本 (text) 和二进制 (binary) 传输协议,为节约带宽,提高传输效率,一般情况下使用二进制类型的传输协议为多数,有时还会使用基于文本类型的协议,这需要根据项目 / 产品中的实际需求。

常用协议有以下几种:

  • TBinaryProtocol : 二进制格式.
  • TCompactProtocol : 高效率的、密集的二进制压缩格式
  • TJSONProtocol : JSON格式
  • TSimpleJSONProtocol : 提供JSON只写协议, 生成的文件很容易通过脚本语言解析

注意:客户端和服务端的协议要一致。

服务器类型Server

  • TSimpleServer ——单线程服务器端使用标准的阻塞式 I/O,一般用于测试。
  • TThreadPoolServer —— 多线程服务器端使用标准的阻塞式 I/O,预先创建一组线程处理请求。
  • TNonblockingServer —— 多线程服务器端使用非阻塞式 I/O,服务端和客户端需要指定 TFramedTransport 数据传输的方式。
  • THsHaServer —— 半同步半异步的服务端模型,需要指定为: TFramedTransport 数据传输的方式。它使用一个单独的线程来处理网络I/O,一个独立的worker线程池来处理消息。这样,只要有空闲的worker线程,消息就会被立即处理,因此多条消息能被并行处理。
  • TThreadedSelectorServer —— TThreadedSelectorServer允许你用多个线程来处理网络I/O。它维护了两个线程池,一个用来处理网络I/O,另一个用来进行请求的处理。当网络I/O是瓶颈的时候,TThreadedSelectorServer比THsHaServer的表现要好。

实现逻辑

服务端

实现服务处理接口 impl

创建TProcessor 创建TServerTransport 创建TProtocol 创建TServer 启动Server

客户端

创建Transport 创建TProtocol 基于TTransport和TProtocol创建 Client 调用Client的相应方法

ThriftServerDemo实例

新建 Maven 项目,并且添加 thrift 依赖包

 <dependencies>
       <dependency>
           <groupId>org.apache.thrift</groupId>
           <artifactId>libthrift</artifactId>
           <version>0.9.3</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-log4j12</artifactId>
           <version>1.7.12</version>
       </dependency>
       <dependency>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-api</artifactId>
           <version>2.7</version>
       </dependency>
       <dependency>
           <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-core</artifactId>
           <version>2.7</version>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.3</version>
               <configuration>
                   <source>1.8</source>
                   <target>1.8</target>
                   <encoding>utf-8</encoding>
               </configuration>
           </plugin>
       </plugins>
   </build>

编写 IDL 接口并生成接口文件

  1. namespace java thrift.service

  2. // 计算类型 - 仅限整数四则运算
  3. enum ComputeType {
  4. ADD = 0;
  5. SUB = 1;
  6. MUL = 2;
  7. DIV = 3;
  8. }

  9. // 服务请求
  10. struct ComputeRequest {
  11. 1:required i64 x;
  12. 2:required i64 y;
  13. 3:required ComputeType computeType;
  14. }

  15. // 服务响应
  16. struct ComputeResponse {
  17. 1:required i32 errorNo;
  18. 2:optional string errorMsg;
  19. 3:required i64 computeRet;
  20. }

  21. service ComputeServer {
  22. ComputeResponse getComputeResult(1:ComputeRequest request);
  23. }

执行编译命令:

thrift-0.11.0.exe -r -gen java computeServer.thrift

拷贝生成的 Service 类文件到 IDEA

服务端接口实现

public class ThriftTestImpl implements ComputeServer.Iface {
   private static final Logger logger = LogManager.getLogger(ThriftTestImpl.class);
   public ComputeResponse getComputeResult(ComputeRequest request) {
       ComputeType computeType = request.getComputeType();
       long x = request.getX();
       long y = request.getY();
       logger.info("get compute result begin. [x:{}] [y:{}] [type:{}]", x, y, computeType.toString());
       long begin = System.currentTimeMillis();
       ComputeResponse response = new ComputeResponse();
       response.setErrorNo(0);
       try {
           long ret;
           if (computeType == ComputeType.ADD) {
               ret = add(x, y);
               response.setComputeRet(ret);
           } else if (computeType == ComputeType.SUB) {
               ret = sub(x, y);
               response.setComputeRet(ret);
           } else if (computeType == ComputeType.MUL) {
               ret = mul(x, y);
               response.setComputeRet(ret);
           } else {
               ret = div(x, y);
               response.setComputeRet(ret);
           }
       } catch (Exception e) {
           response.setErrorNo(1001);
           response.setErrorMsg(e.getMessage());
           logger.error("exception:", e);
       }
       long end = System.currentTimeMillis();
       logger.info("get compute result end. [errno:{}] cost:[{}ms]", response.getErrorNo(), (end - begin));
       return response;
   }
   private long add(long x, long y) {
       return x + y;
   }
   private long sub(long x, long y) {
       return x - y;
   }
   private long mul(long x, long y) {
       return x * y;
   }
   private long div(long x, long y) {
       return x / y;
   }
}

服务端实现

  1. public class ServerMain {
  2. private static final Logger logger = LogManager.getLogger(ServerMain.class);

  3. public static void main(String[] args) {
  4. try {
  5. //实现服务处理接口impl
  6. ThriftTestImpl workImpl = new ThriftTestImpl();
  7. //创建TProcessor
  8. TProcessor tProcessor = new ComputeServer.Processor<ComputeServer.Iface>(workImpl);
  9. //创建TServerTransport,非阻塞式 I/O,服务端和客户端需要指定 TFramedTransport 数据传输的方式
  10. final TNonblockingServerTransport transport = new TNonblockingServerSocket(9999);
  11. //创建TProtocol
  12. TThreadedSelectorServer.Args ttpsArgs = new TThreadedSelectorServer.Args(transport);
  13. ttpsArgs.transportFactory(new TFramedTransport.Factory());
  14. //二进制格式反序列化
  15. ttpsArgs.protocolFactory(new TBinaryProtocol.Factory());
  16. ttpsArgs.processor(tProcessor);
  17. ttpsArgs.selectorThreads(16);
  18. ttpsArgs.workerThreads(32);
  19. logger.info("compute service server on port :" + 9999);
  20. //创建TServer
  21. TServer server = new TThreadedSelectorServer(ttpsArgs);
  22. //启动Server
  23. server.serve();
  24. } catch (Exception e) {
  25. logger.error(e);
  26. }
  27. }
  28. }

服务端整体代码结构

log4j2.xml配置文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
  3. <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
  4. <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
  5. <configuration status="INFO" monitorInterval="30">
  6. <!--先定义所有的appender-->
  7. <appenders>
  8. <!--这个输出控制台的配置-->
  9. <console name="Console" target="SYSTEM_OUT">
  10. <!--输出日志的格式-->
  11. <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [%l] %m%n}"/>
  12. </console>

  13. <RollingFile name="RollingFileInfo" fileName="log/log.log" filePattern="log/log.log.%d{yyyy-MM-dd}">
  14. <!-- 只接受level=INFO以上的日志 -->
  15. <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
  16. <PatternLayout pattern="[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n"/>
  17. <Policies>
  18. <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
  19. <SizeBasedTriggeringPolicy/>
  20. </Policies>
  21. </RollingFile>

  22. <RollingFile name="RollingFileError" fileName="log/error.log" filePattern="log/error.log.%d{yyyy-MM-dd}">
  23. <!-- 只接受level=WARN以上的日志 -->
  24. <Filters>
  25. <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />
  26. </Filters>
  27. <PatternLayout pattern="[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n"/>
  28. <Policies>
  29. <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
  30. <SizeBasedTriggeringPolicy/>
  31. </Policies>
  32. </RollingFile>

  33. </appenders>

  34. <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
  35. <loggers>
  36. <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
  37. <logger name="org.springframework" level="INFO"></logger>
  38. <logger name="org.mybatis" level="INFO"></logger>
  39. <root level="all">
  40. <appender-ref ref="Console"/>
  41. <appender-ref ref="RollingFileInfo"/>
  42. <appender-ref ref="RollingFileError"/>
  43. </root>
  44. </loggers>
  45. </configuration>

Jmeter测试类编写

利用JMeter调用Java测试类去调用对应的后台服务,并记住每次调用并获取反馈值的RT,ERR%,只需要按照单线程的方式去实现测试业务,也无需添加各种埋点收集数据

新建一个 JavaMaven 工程,添加 JMeterthrift 依赖包

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.apache.jmeter</groupId>
  4. <artifactId>ApacheJMeter_core</artifactId>
  5. <version>4.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.apache.jmeter</groupId>
  9. <artifactId>ApacheJMeter_java</artifactId>
  10. <version>4.0</version>
  11. </dependency>

  12. <dependency>
  13. <groupId>org.apache.thrift</groupId>
  14. <artifactId>libthrift</artifactId>
  15. <version>0.11.0</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.apache.logging.log4j</groupId>
  19. <artifactId>log4j-api</artifactId>
  20. <version>2.11.1</version>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.apache.logging.log4j</groupId>
  24. <artifactId>log4j-core</artifactId>
  25. <version>2.11.1</version>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.slf4j</groupId>
  29. <artifactId>slf4j-log4j12</artifactId>
  30. <version>1.7.25</version>
  31. </dependency>
  32. </dependencies>

  33. <build>
  34. <plugins>
  35. <plugin>
  36. <groupId>org.apache.maven.plugins</groupId>
  37. <artifactId>maven-compiler-plugin</artifactId>
  38. <version>3.7.0</version>
  39. <configuration>
  40. <source>1.8</source>
  41. <target>1.8</target>
  42. <encoding>utf-8</encoding>
  43. </configuration>
  44. </plugin>
  45. </plugins>
  46. </build>

ThriftClient测试类编写

  1. public class ThriftClient {
  2. private ComputeServer.Client client = null;
  3. private TTransport tTransport = null;

  4. public ThriftClient(String ip,int port){
  5. try {
  6. TTransport tTransport = new TFramedTransport(new TSocket(ip,port));
  7. tTransport.open();
  8. TProtocol tProtocol = new TBinaryProtocol(tTransport);
  9. client = new ComputeServer.Client(tProtocol);
  10. } catch (TTransportException e) {
  11. e.printStackTrace();
  12. }
  13. }

  14. public ComputeResponse getResponse(ComputeRequest request){
  15. try {
  16. ComputeResponse response = client.getComputeResult(request);
  17. return response;
  18. } catch (TException e) {
  19. e.printStackTrace();
  20. return null;
  21. }
  22. }

  23. public void close(){
  24. if (tTransport != null && tTransport.isOpen()){
  25. tTransport.close();
  26. }
  27. }
  28. }

注意:需要把编写 IDL 接口文件拷贝到工程里

新建一个 JavaClass ,如下例中的 TestThriftByJmeter ,并继承 AbstractJavaSamplerClientAbstractJavaSamplerClient 中默认实现了四个可以覆盖的方法,分别是 getDefaultParameters()setupTest()runTest()teardownTest() 方法。

  • getDefaultParameters 方法主要用于设置传入界面的参数;
  • setupTest 方法为初始化方法,用于初始化性能测试时的每个线程;
  • runTest 方法为性能测试时的线程运行体;
  • teardownTest 方法为测试结束方法,用于结束性能测试中的每个线程。

编写TestThriftByJmeter测试类

  1. public class TestThriftByJmeter extends AbstractJavaSamplerClient {
  2. private ThriftClient client;
  3. private ComputeRequest request;
  4. private ComputeResponse response;

  5. //设置传入界面的参数
  6. @Override
  7. public Arguments getDefaultParameters(){
  8. Arguments arguments = new Arguments();
  9. arguments.addArgument("ip","172.16.14.251");
  10. arguments.addArgument("port","9999");
  11. arguments.addArgument("X","0");
  12. arguments.addArgument("Y","0");
  13. arguments.addArgument("type","0");
  14. return arguments;
  15. }

  16. //初始化方法
  17. @Override
  18. public void setupTest(JavaSamplerContext context){
  19. //获取Jmeter中设置的参数
  20. String ip = context.getParameter("ip");
  21. int port = context.getIntParameter("port");
  22. int x = context.getIntParameter("X");
  23. int y = context.getIntParameter("Y");
  24. ComputeType type = ComputeType.findByValue(context.getIntParameter("type"));

  25. //创建客户端
  26. client = new ThriftClient(ip,port);
  27. //设置request请求
  28. request = new ComputeRequest(x,y,type);
  29. super.setupTest(context);
  30. }

  31. //性能测试线程运行体
  32. @Override
  33. public SampleResult runTest(JavaSamplerContext context) {
  34. SampleResult result = new SampleResult();
  35. //开始统计响应时间标记
  36. result.sampleStart();
  37. try {
  38. long begin = System.currentTimeMillis();
  39. response = client.getResponse(request);
  40. long cost = (System.currentTimeMillis() - begin);
  41. //打印时间戳差值。Java请求响应时间
  42. System.out.println(response.toString()+" 总计花费:["+cost+"ms]");

  43. if (response == null){
  44. //设置测试结果为fasle
  45. result.setSuccessful(false);
  46. return result;
  47. }
  48. if (response.getErrorNo() == 0){
  49. //设置测试结果为true
  50. result.setSuccessful(true);
  51. }else{
  52. result.setSuccessful(false);
  53. result.setResponseMessage("ERROR");
  54. }
  55. }catch (Exception e){
  56. result.setSuccessful(false);
  57. result.setResponseMessage("ERROR");
  58. e.printStackTrace();
  59. }finally {
  60. //结束统计响应时间标记
  61. result.sampleEnd();
  62. }
  63. return result;
  64. }

  65. //测试结束方法
  66. public void tearDownTest(JavaSamplerContext context) {
  67. if (client != null) {
  68. client.close();
  69. }

  70. super.teardownTest(context);
  71. }

  72. }

特别说明:

result.setSamplerLabel("7D"); //设置java Sampler的标题
result.setResponseOK();   //设置响应成功
result.setResponseData(); //设置响应内容

编写测试Run Main方法

  1. public class RunMain {
  2. public static void main(String[] args) {
  3. Arguments arguments = new Arguments();
  4. arguments.addArgument("ip","172.16.14.251");
  5. arguments.addArgument("port","9999");
  6. arguments.addArgument("X","1");
  7. arguments.addArgument("Y","3");
  8. arguments.addArgument("type","0");
  9. JavaSamplerContext context = new JavaSamplerContext(arguments);
  10. TestThriftByJmeter jmeter = new TestThriftByJmeter();

  11. jmeter.setupTest(context);
  12. jmeter.runTest(context);
  13. jmeter.tearDownTest(context);

  14. }
  15. }

测试结果通过

使用 mvn cleanpackage 打包测试代码

使用 mvn dependency:copy-dependencies-DoutputDirectory=lib 复制所依赖的jar包都会到项目下的lib目录下

复制测试代码 jar 包到 jmeter\lib\ext 目录下,复制依赖包到 jmeter\lib 目录下

这里有两点需要注意:

  • 如果你的jar依赖了其他第三方jar,需要将其一起放到lib/ext下,否则会出现ClassNotFound错误
  • 如果在将jar放入lib/ext后,你还是无法找到你编写的类,且此时你是开着JMeter的,则需要重启一下JMeter

打开 Jmeter ,在添加 Java 请求时,注意要选择 Jmeter 测试类,下面的列表中可以看到参数和默认值。

下面我们将进行性能压测,设置线程组,设置10个并发线程。

服务端日志:

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言