軟體效能測試分析與調優實踐之路-JMeter對RPC服務的效能壓測分析與調優-手稿節選

2022-06-03 18:00:42

一、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  來源於部落格園 ,本文摘選自《軟體效能測試分析與調優實踐之路》

未完待續,更多內容