效能工具之Jmeter壓測Thrift RPC服務

Zee_7D發表於2021-06-27

概述

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依賴包

  1.  <dependencies>

  2.        <dependency>

  3.            <groupId>org.apache.thrift</groupId>

  4.            <artifactId>libthrift</artifactId>

  5.            <version>0.9.3</version>

  6.        </dependency>

  7.        <dependency>

  8.            <groupId>org.slf4j</groupId>

  9.            <artifactId>slf4j-log4j12</artifactId>

  10.            <version>1.7.12</version>

  11.        </dependency>

  12.        <dependency>

  13.            <groupId>org.apache.logging.log4j</groupId>

  14.            <artifactId>log4j-api</artifactId>

  15.            <version>2.7</version>

  16.        </dependency>

  17.        <dependency>

  18.            <groupId>org.apache.logging.log4j</groupId>

  19.            <artifactId>log4j-core</artifactId>

  20.            <version>2.7</version>

  21.        </dependency>

  22.    </dependencies>

  23.    <build>

  24.        <plugins>

  25.            <plugin>

  26.                <groupId>org.apache.maven.plugins</groupId>

  27.                <artifactId>maven-compiler-plugin</artifactId>

  28.                <version>3.3</version>

  29.                <configuration>

  30.                    <source>1.8</source>

  31.                    <target>1.8</target>

  32.                    <encoding>utf-8</encoding>

  33.                </configuration>

  34.            </plugin>

  35.        </plugins>

  36.    </build>


編寫 IDL介面並生成介面檔案

  1. namespace java thrift.service

  2.  

  3. // 計算型別 - 僅限整數四則運算

  4. enum ComputeType {

  5.    ADD = 0;

  6.    SUB = 1;

  7.    MUL = 2;

  8.    DIV = 3;

  9. }

  10.  

  11. // 服務請求

  12. struct ComputeRequest {

  13.    1:required i64 x;

  14.    2:required i64 y;

  15.    3:required ComputeType computeType;

  16. }

  17.  

  18. // 服務響應

  19. struct ComputeResponse {

  20.    1:required i32 errorNo;

  21.    2:optional string errorMsg;

  22.    3:required i64 computeRet;

  23. }

  24.  

  25. service ComputeServer {

  26.    ComputeResponse getComputeResult(1:ComputeRequest request);

  27. }


執行編譯命令:

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

拷貝生成的 Service類檔案到 IDEA 

服務端介面實現

  1. public class ThriftTestImpl implements ComputeServer.Iface {

  2.    private static final Logger logger = LogManager.getLogger(ThriftTestImpl.class);

  3.    public ComputeResponse getComputeResult(ComputeRequest request) {

  4.        ComputeType computeType = request.getComputeType();

  5.        long x = request.getX();

  6.        long y = request.getY();

  7.        logger.info("get compute result begin. [x:{}] [y:{}] [type:{}]", x, y, computeType.toString());

  8.        long begin = System.currentTimeMillis();

  9.        ComputeResponse response = new ComputeResponse();

  10.        response.setErrorNo(0);

  11.        try {

  12.            long ret;

  13.            if (computeType == ComputeType.ADD) {

  14.                ret = add(x, y);

  15.                response.setComputeRet(ret);

  16.            } else if (computeType == ComputeType.SUB) {

  17.                ret = sub(x, y);

  18.                response.setComputeRet(ret);

  19.            } else if (computeType == ComputeType.MUL) {

  20.                ret = mul(x, y);

  21.                response.setComputeRet(ret);

  22.            } else {

  23.                ret = div(x, y);

  24.                response.setComputeRet(ret);

  25.            }

  26.        } catch (Exception e) {

  27.            response.setErrorNo(1001);

  28.            response.setErrorMsg(e.getMessage());

  29.            logger.error("exception:", e);

  30.        }

  31.        long end = System.currentTimeMillis();

  32.        logger.info("get compute result end. [errno:{}] cost:[{}ms]", response.getErrorNo(), (end - begin));

  33.        return response;

  34.    }

  35.    private long add(long x, long y) {

  36.        return x + y;

  37.    }

  38.    private long sub(long x, long y) {

  39.        return x - y;

  40.    }

  41.    private long mul(long x, long y) {

  42.        return x * y;

  43.    }

  44.    private long div(long x, long y) {

  45.        return x / y;

  46.    }

  47. }


服務端實現

  1. public class ServerMain {

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

  3.  

  4.    public static void main(String[] args) {

  5.        try {

  6.            //實現服務處理介面impl

  7.            ThriftTestImpl workImpl = new ThriftTestImpl();

  8.            //建立TProcessor

  9.            TProcessor tProcessor = new ComputeServer.Processor<ComputeServer.Iface>(workImpl);

  10.            //建立TServerTransport,非阻塞式 I/O,服務端和客戶端需要指定 TFramedTransport 資料傳輸的方式

  11.            final TNonblockingServerTransport transport = new TNonblockingServerSocket(9999);

  12.            //建立TProtocol

  13.            TThreadedSelectorServer.Args ttpsArgs = new TThreadedSelectorServer.Args(transport);

  14.            ttpsArgs.transportFactory(new TFramedTransport.Factory());

  15.            //二進位制格式反序列化

  16.            ttpsArgs.protocolFactory(new TBinaryProtocol.Factory());

  17.            ttpsArgs.processor(tProcessor);

  18.            ttpsArgs.selectorThreads(16);

  19.            ttpsArgs.workerThreads(32);

  20.            logger.info("compute service server on port :" + 9999);

  21.            //建立TServer

  22.            TServer server = new TThreadedSelectorServer(ttpsArgs);

  23.            //啟動Server

  24.            server.serve();

  25.        } catch (Exception e) {

  26.            logger.error(e);

  27.        }

  28.    }

  29. }


服務端整體程式碼結構 

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.  

  14.        <RollingFile name="RollingFileInfo" fileName="log/log.log" filePattern="log/log.log.%d{yyyy-MM-dd}">

  15.            <!-- 只接受level=INFO以上的日誌 -->

  16.            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>

  17.            <PatternLayout pattern="[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n"/>

  18.            <Policies>

  19.                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>

  20.                <SizeBasedTriggeringPolicy/>

  21.            </Policies>

  22.        </RollingFile>

  23.  

  24.        <RollingFile name="RollingFileError" fileName="log/error.log" filePattern="log/error.log.%d{yyyy-MM-dd}">

  25.            <!-- 只接受level=WARN以上的日誌 -->

  26.            <Filters>

  27.                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />

  28.            </Filters>

  29.            <PatternLayout pattern="[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n"/>

  30.            <Policies>

  31.                <TimeBasedTriggeringPolicy modulate="true" interval="1"/>

  32.                <SizeBasedTriggeringPolicy/>

  33.            </Policies>

  34.        </RollingFile>

  35.  

  36.    </appenders>

  37.  

  38.    <!--然後定義logger,只有定義了logger並引入的appender,appender才會生效-->

  39.    <loggers>

  40.        <!--過濾掉spring和mybatis的一些無用的DEBUG資訊-->

  41.        <logger name="org.springframework" level="INFO"></logger>

  42.        <logger name="org.mybatis" level="INFO"></logger>

  43.        <root level="all">

  44.            <appender-ref ref="Console"/>

  45.            <appender-ref ref="RollingFileInfo"/>

  46.            <appender-ref ref="RollingFileError"/>

  47.        </root>

  48.    </loggers>

  49. </configuration>


Jmeter測試類編寫

利用JMeter呼叫Java測試類去呼叫對應的後臺服務,並記住每次呼叫並獲取反饋值的RT,ERR%,只需要按照單執行緒的方式去實現測試業務,也無需新增各種埋點收集資料 

 

新建一個 JavaMaven工程,新增 JMeter及 thrift依賴包

  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.  

  13.        <dependency>

  14.            <groupId>org.apache.thrift</groupId>

  15.            <artifactId>libthrift</artifactId>

  16.            <version>0.11.0</version>

  17.        </dependency>

  18.        <dependency>

  19.            <groupId>org.apache.logging.log4j</groupId>

  20.            <artifactId>log4j-api</artifactId>

  21.            <version>2.11.1</version>

  22.        </dependency>

  23.        <dependency>

  24.            <groupId>org.apache.logging.log4j</groupId>

  25.            <artifactId>log4j-core</artifactId>

  26.            <version>2.11.1</version>

  27.        </dependency>

  28.        <dependency>

  29.            <groupId>org.slf4j</groupId>

  30.            <artifactId>slf4j-log4j12</artifactId>

  31.            <version>1.7.25</version>

  32.        </dependency>

  33.    </dependencies>

  34.  

  35.    <build>

  36.        <plugins>

  37.            <plugin>

  38.                <groupId>org.apache.maven.plugins</groupId>

  39.                <artifactId>maven-compiler-plugin</artifactId>

  40.                <version>3.7.0</version>

  41.                <configuration>

  42.                    <source>1.8</source>

  43.                    <target>1.8</target>

  44.                    <encoding>utf-8</encoding>

  45.                </configuration>

  46.            </plugin>

  47.        </plugins>

  48.    </build>


ThriftClient測試類編寫

  1. public class ThriftClient {

  2.    private ComputeServer.Client client = null;

  3.    private TTransport tTransport = null;

  4.  

  5.    public ThriftClient(String ip,int port){

  6.        try {

  7.            TTransport tTransport = new TFramedTransport(new TSocket(ip,port));

  8.            tTransport.open();

  9.            TProtocol tProtocol = new TBinaryProtocol(tTransport);

  10.            client = new ComputeServer.Client(tProtocol);

  11.        } catch (TTransportException e) {

  12.            e.printStackTrace();

  13.        }

  14.    }

  15.  

  16.    public ComputeResponse getResponse(ComputeRequest request){

  17.        try {

  18.            ComputeResponse response = client.getComputeResult(request);

  19.            return response;

  20.        } catch (TException e) {

  21.            e.printStackTrace();

  22.            return null;

  23.        }

  24.    }

  25.  

  26.    public void  close(){

  27.        if (tTransport != null && tTransport.isOpen()){

  28.            tTransport.close();

  29.        }

  30.    }

  31. }


注意:需要把編寫 IDL介面檔案拷貝到工程裡

新建一個 JavaClass,如下例中的 TestThriftByJmeter,並繼承 AbstractJavaSamplerClient。 AbstractJavaSamplerClient中預設實現了四個可以覆蓋的方法,分別是 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.    //設定傳入介面的引數

  7.    @Override

  8.    public Arguments getDefaultParameters(){

  9.        Arguments arguments = new Arguments();

  10.        arguments.addArgument("ip","172.16.14.251");

  11.        arguments.addArgument("port","9999");

  12.        arguments.addArgument("X","0");

  13.        arguments.addArgument("Y","0");

  14.        arguments.addArgument("type","0");

  15.        return arguments;

  16.    }

  17.  

  18.    //初始化方法

  19.    @Override

  20.    public void setupTest(JavaSamplerContext context){

  21.        //獲取Jmeter中設定的引數

  22.        String ip = context.getParameter("ip");

  23.        int port = context.getIntParameter("port");

  24.        int x = context.getIntParameter("X");

  25.        int y = context.getIntParameter("Y");

  26.        ComputeType type = ComputeType.findByValue(context.getIntParameter("type"));

  27.  

  28.        //建立客戶端

  29.        client = new ThriftClient(ip,port);

  30.        //設定request請求

  31.        request = new ComputeRequest(x,y,type);

  32.        super.setupTest(context);

  33.    }

  34.  

  35.    //效能測試執行緒執行體

  36.    @Override

  37.    public SampleResult runTest(JavaSamplerContext context) {

  38.        SampleResult result = new SampleResult();

  39.        //開始統計響應時間標記

  40.        result.sampleStart();

  41.        try {

  42.            long begin = System.currentTimeMillis();

  43.            response = client.getResponse(request);

  44.            long cost = (System.currentTimeMillis() - begin);

  45.            //列印時間戳差值。Java請求響應時間

  46.            System.out.println(response.toString()+" 總計花費:["+cost+"ms]");

  47.  

  48.            if (response == null){

  49.                //設定測試結果為fasle

  50.                result.setSuccessful(false);

  51.                return result;

  52.            }

  53.            if (response.getErrorNo() == 0){

  54.                //設定測試結果為true

  55.                result.setSuccessful(true);

  56.            }else{

  57.                result.setSuccessful(false);

  58.                result.setResponseMessage("ERROR");

  59.            }

  60.        }catch (Exception e){

  61.            result.setSuccessful(false);

  62.            result.setResponseMessage("ERROR");

  63.            e.printStackTrace();

  64.        }finally {

  65.            //結束統計響應時間標記

  66.            result.sampleEnd();

  67.        }

  68.        return result;

  69.    }

  70.  

  71.    //測試結束方法

  72.    public void tearDownTest(JavaSamplerContext context) {

  73.        if (client != null) {

  74.            client.close();

  75.        }

  76.  

  77.        super.teardownTest(context);

  78.    }

  79.  

  80. }


特別說明:

  1. result.setSamplerLabel("7D"); //設定java Sampler的標題

  2. result.setResponseOK();   //設定響應成功

  3. 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.  

  12.        jmeter.setupTest(context);

  13.        jmeter.runTest(context);

  14.        jmeter.tearDownTest(context);

  15.  

  16.    }

  17. }


測試結果通過

使用 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個併發執行緒。

服務端日誌:

相關文章