nGrinder介紹、編寫腳本與執行(完整版)


目錄:

  • 性能測試工具的選型
  • nGrinder的介紹
  • nGrinder環境的搭建
  • Groovy語言的介紹
  • 常用的工具類
  • nGrinder代碼實例
  • 執行測試

一、性能測試工具的選型

1、主流的性能測試工具 LoadRunner JMeter 與 nGrinder對比

1.1、Loadrunner

  • 基於UI操作,容易上手。早期很流行,功能強大,但是太笨重,安裝很麻煩。
  • 不開源,擴展性不高,收費貴。往后的方向肯定是客戶端工具逐步向平台化發展,所以已經慢慢被替代了。

1.2、JMeter

  • 基於UI操作,容易上手,但是編程能力較弱(使用beanshell腳本語言)。
  • 其次JMeter基於線程(單進程,多線程,循環N次),單節點模擬數千用戶幾乎不可能。支持平台化集成,也誕生了一些開源平台,如MeterSphere

1.3、nGrinder

  • 單節點可支持5000+並發(多進程,多線程)、支持分布式、可監控被測服務器、可錄制腳本、開源、平台化
  • 易於二次開發
  • 參數化功能較弱
  • 對測試人員的代碼要求較高
比較點
JMeter
Ngrinder
LoadRunner
實現語言 Java java/python java/VB/C/.NET
使用方式 C/S或Command B/S C/S
支持分布式 master/slave  controller/agent master/slave
開源方式 免費,完全開源 免費,完全開源 收費
支持協議 多種協議 多種協議 多種協議
資源監控 monitor/plugin,如果二開,需要查找plugin的源碼 monitor方式 自帶資源監控功能
社區活躍度 文檔完善 有中文社區 網上資料和相關培訓很多,購買正版還可以得到技術支持
是否需要編碼 基本不需要 需要,Jython/Groovy 需要
腳本的維護 本地 內置SVN,可以修改成git 本地
腳本錄制 可使用BadBoy進行錄制 可通過PTS插件進行錄制 自帶錄制功能
可擴展性 可增加plugin,輸出結果可以再加工,擴展性強 可增加plugin,擴展性強 通過擴展函數庫實現
安裝 簡單,解壓即可 簡單,可以下載安裝包或綠色包解壓 安裝包比較大,安裝繁瑣
平台化 有開源或雲端的壓測平台 本身具備

官網給出的最大虛擬用戶數

二、nGrinder的介紹

1、背景

官網地址

    nGrinder是韓國一家公司居於Grinder二次開發的一個性能平台。nGrinder具有 開源、易用、高可用、高擴展等特性,在Grinder基礎上實現了多測試並行,通過web管理,實現了集群,同時支持Groovy和Jython腳本語言,(官方上說Groovy的性能會更好),也實現了對目標服務的監控以及插件的擴展,實現更多用戶虛擬用戶並發(官方上說,8G內存的4核cpu機器可以支持高到8000個虛擬用戶),在同一線程中,不斷重復的執行測試腳本,來模擬很多並發用戶

2、nGrinder 由三個主要的組件組成

Controller
   1. 分發調度測試任務
   2.協調測試進程
   3. 整理和顯示測試的統計結果
   4. 用戶創建和修改腳本
Monitor
    1.用於監控被測服務器的系統性能(例如:CPU/MEMORY)
    2.必須部署在被測服務器上
Agent
    1.壓測任務的拉取
    2.在代理服務器上加載運行測試進程和線程
    3. 監控施壓機器的系統性能(例如:CPU/MEMORY/網卡/磁盤)

3、工作原理

基於官方文檔翻譯

3.1、非集群架構圖

 nGrinder 是一款在一系列機器上執行 Groovy 或 Jython 測試腳本的應用,內部引擎是基於 Grinder。 nGrinder 使用 controller 和 agent 分別包裝了 Grinder 的 console 和 agent ,而且擴展了多種功能使其能夠支持並發測試

 nGrinder 在 Grinder 的基礎上:

  • 實現多測試並行
  • 基於web的管理
  • 實現cluster
  • 內置svn,方便的腳本編輯、管理
  • 支持Groovy腳本,相對於Jython,可以啟動更多的虛擬用戶
  • 實現對目標服務器的監控
  • 插件系統擴展
3.2、集群架構
nGrinder 從 3.1 版本開始支持 controller集群

歸納

  • 由一個控制端controller和多個代理端agent組成,通過控制端(瀏覽器訪問)建立測試場景,然后分發到代理端進行壓力測試。
  • 用戶按照一定規范編寫測試腳本,controller會將腳本以及需要的資源分發到agent,用jython、groovy執行。
  • 在腳本執行的過程中收集運行情況、相應時間、測試目標服務器的運行情況等。並且保存這些數據生成測試報告,通過動態圖和數據表的形式展示出來。用戶可以方便的看到TPS、被測服務器的CPU和內存等情況。

三、nGrinder環境的搭建

以linux 這里采用的版本是centos 6 64bit,性能測試工具不建議在Windows上部署。

下載nGrinder

選擇最新的war包。

服務器端啟動: nohup java -XX:MaxPermSize-256m -jar ngrinder-controller-3.5.2.war --port 8080 >output 2>&1 &

這樣nGrinder的管理頁面就部署好,你可以簡單的把ngrinder-controller的功能理解為性能測試展示和控制,后面會進行詳細介紹。

打開網址:

http://xxx.xxx.xxx.xxx:8080/login

默認用戶名和密碼都為admin

下載和agent 

下載agent   tar包(進入主頁,點擊 admin →Download Agent),然后進行解壓  tar  -xvf  ngrinder-agent-3.5.2.tar

解壓agent 后,目錄結構如下,編輯進入__agent.conf 

如果controller和agent在同一台機器,則修改 agent.controller_host=127.0.0.1

啟動agent:   [ops@qa.test.jmeter-00.hz ngrinder-agent]$ nohup ./run_agent.sh >output 2>&1 &


四、Groovy語言的簡介

1.什么是groovy?

       Groovy是一種基於JVM(Java虛擬機)的敏捷開發語言,它結合了Python、Ruby和Smalltalk的許多強大的特性,Groovy 代碼能夠與 Java 代碼很好地結合,也能用於擴展現有代碼。由於其運行在 JVM 上的特性,Groovy 可以使用其他 Java 語言編寫的庫。

       Groovy是一種基於Java平台的面向對象語言。 Groovy 1.0於2007年1月2日發布,其中Groovy 2.4是當前的主要版本。 Groovy通過Apache License v 2.0發布。

      目前最新版本為2.5.3。

2.Groovy的特點

Groovy中有以下特點:

  • 同時支持靜態和動態類型
  • 支持運算符重載
  • 本地語法列表和關聯數組
  • 對正則表達式的本地支持
  • 各種標記語言,如XML和HTML原生支持
  • Groovy對於Java開發人員來說很簡單,因為Java和Groovy的語法非常相似
  • 您可以使用現有的Java庫
  • Groovy擴展了java.lang.Object

動態類型

類型對於變量,屬性,方法,閉包的參數以及方法的返回類型都是可有可無的,都是在給變量賦值的時候才決定它的類型, 不同的類型會在后面用到,任何類型都可以被使用,即使是基本類型 (通過自動包裝(autoboxing)). 當需要時,很多類型之間的轉換都會自動發生,比如在這些類型之間的轉換: 字符串(String),基本類型(如int) 和類型的包裝類 (如Integer)之間,可以把不同的基本類型添加到同一數組(collections)中。

運算符重載

運算符重載,就是對已有的運算符重新進行定義,賦予其另一種功能,以適應不同的數據類型。

3.類

Groovy類和java類一樣,完全可以用標准java bean的語法定義一個Groovy類。但作為另一種語言,可以使用更Groovy的方式定義類,這樣的好處是,可以少寫一半以上的javabean代碼。

(1)不需public修飾符

如前面所言,Groovy的默認訪問修飾符就是public,如果Groovy類成員需要public修飾,則根本不用寫它。

(2)不需要類型說明

同樣前面也說過,Groovy也不關心變量和方法參數的具體類型。

(3)不需要getter/setter方法

在很多ide(如eclipse)早就可以為程序員自動產生getter/setter方法了,在Groovy中,不需要getter/setter方法--所有類成員(如果是默認的public)根本不用通過getter/setter方法引用它們(當然,如果一定要通過getter/setter方法訪問成員屬性,Groovy也提供了它們)。

(4)不需要構造函數

不再需要程序員聲明任何構造函數,因為實際上只需要兩個構造函數(1個不帶參數的默認構造函數,1個只帶一個map參數的構造函數--由於是map類型,通過這個參數可以構造對象時任意初始化它的成員變量)。

(5)不需要;結尾

Groovy中每一行代碼不需要分號作為結束符。

五、常用的工具類

1、生成隨機字符串(import org.apache.commons.lang.RandomStringUtils)
    數字:RandomStringUtils.randomNumeric(length); 字母:RandomStringUtils.randomAlphabetic(length); 字母加數字:RandomStringUtils.randomAlphanumeric(length); 所有ASCCII字符:RandomStringUtils.randomAscii(length); 自定義混合字符:RandomStringUtils.randomAscii(length, string); 
2、生成隨機數字:(import java.util.concurrent.ThreadLocalRandom;)
    數字:int random_number = ThreadLocalRandom.current().nextInt(min_num, max_num); 
3、獲取項目數據文件路徑
    common項目:"/resources/account.txt" maven項目:Thread.currentThread().getContextClassLoader().getResource("/account.txt").getPath(); maven項目獲取文件內容:ReflectionUtils.getCallingClass(0).getResourceAsStream("/account.txt").getText("UTF-8") 
4、讀取文件:
    txt每行單數據:   String[] file_arrary = new File("/resources/account.txt") as String[]; String file_data = file_arrary[arrary_index]; txt每行雙數據: String[] file_arrary = new File("/resources/account.txt") as String[]; String data_one = file_arrary[arrary_index].split(",")[0]; String data_two = file_arrary[arrary_index].split(",")[1]; 另一種方法: List<String> reqDataArrList = new File(dataFilePath).readLines() String data_one = reqDataArrList.get(arrary_index).split(",")[0]; String data_two = reqDataArrList.get(arrary_index).split(",")[1]; txt每行多數據可參考雙數據方法。也可以參考json方式存儲: BufferedReader txt_content=new BufferedReader(new FileReader(new File("/resources/account.txt"))) data_json = new JSONObject() String text_line = "" while(( text_line=txt_content.readLine())!=null){ data_json.put(text_line.split(",")[0],text_line.split(",")[1]) } String data_one = data_json.keys[0] String data_two = data_json.getString(data_one) 
5、寫入文件:
    覆蓋寫入:   def write = new File(file_path, file_name).newPrintWriter(); write.write(write_text); write.flush(); write.close() 追加寫入: def write = new File(file_path, file_name).newPrintWriter(); write.append(write_text); write.flush(); write.close() 
6、json文件的數據處理(import org.ngrinder.recorder.RecorderUtils)
    json文件讀取: String json_str = new File(file_path).getText("UTF-8") def json_object = RecorderUtils.parseRequestToJson(json_str) 長度:json_object.length() 關鍵字:json_object.keys() 添加元素:json_object.put(name, value) 修改元素:json_object.put(name, value) 刪除元素:json_object.remove(name, value) 獲取對應value:json_object.getString(name) 
7、字符串的處理
    字符串截取:String new_str = old_str[0..3] 字符串替換:String string = str.replace("old","new") 字符串統計:int count = string.count("char") 字符串轉化:int int_num = Integer.parseInt(string) 
1、設置多個請求事務(即多個test方法)
    1)設置多個靜態Gtest對象: public static GTest test1 public static GTest test2 2)實例化多個Gtest對象: test1 = new GTest(1, "test1"); test2 = new GTest(2, "test2"); 3)監聽多個test請求: test1.record(this, "test1") test2.record(this, "test2") 4)定義多個test方法: public void test1(){ grinder.logger.info("---ones: {}---", grinder.threadNumber+1) } public void test2(){ grinder.logger.info("---twos: {}---", grinder.threadNumber+1) } 
2、Ngrinder定義請求參數集:
    add方法: List<NVPair> paramList = new ArrayList<NVPair>(); paramList.add(new NVPair("name", "value")); paramList.add(new NVPair("name", "value")); params = paramList.toArray(); new方法: params = [new NVPair("name", "value"), new NVPair("name", "value")]; 
3、Ngrinder處理日志:
    日志級別(三種常見): grinder.logger.info("----before process.----"); grinder.logger.warn("----before process.----"); grinder.logger.error("----before process.----"); 日志限定(僅打印error級別) : 1)導入依賴包 import ch.qos.logback.classic.Level; import org.slf4j.LoggerFactory; 2)設定級別 @BeforeThread LoggerFactory.getLogger("worker").setLevel(Level.ERROR); 3)設置打印語句 @test grinder.logger.error("----error.----"); 日志輸出(輸出所有進程日志):將每個agent的.ngrinder_agent/agent.conf中一項修改為agent.all_logs=true 日志打印:打印變量:grinder.logger.error("{},{}",variable1,variable2); // 換行或縮進可在""中加\n或\t 
4、Ngrinder的cookie處理
    1) 登錄產生cookie @BeforeThread login_get_cookie(); // 調用登錄方法 cookies = CookieModule.listAllCookies(HTTPPluginControl.getThreadHTTPClientContext()); // 配置cookie管理器 2) 讀取控制器中cookie @Before cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) } 
5、Ngrinder請求方式:
    1)通過url加參數直接訪問: post方法: HTTPResponse result = request.POST("http://192.168.2.135:8080/blogs", params, headers) get方法: HTTPResponse result = request.GET("http://192.168.2.135:8080/blogs", params, headers) 參數是json:設置請求頭參數{"Content-Type": "application/json"} 2)通過參數化所有請求數據為json對象(導入import org.ngrinder.recorder.RecorderUtils) HTTPResponse result = RecorderUtils.sendBy(request, req_data_json) HTTPResponse result = RecorderUtils.sendBy(request, req_data_json) 
6、Ngringer的test運行次數設定(將總運行測試次數按百分比例分配到相應test):
    1)引用依賴包: import net.grinder.scriptengine.groovy.junit.annotation.RunRate 2)設置運行次數百分比(所有test設定的比例值不夠100,那不滿的部分不運行,比如設定總比80,只運行這80部分): @RunRate(50) // 數字代表百分比 @Test public void test1(){} @RunRate(50) // 數字代表百分比 @Test public void test2(){} 
7、Ngringer獲取設置的加壓機總數、進程總數、線程總數等信息:
    int tota_agents = Integer.parseInt(grinder.getProperties().get("grinder.agents").toString()) // 設置的總加壓機數 int total_processes = Integer.parseInt(grinder.properties().get("grinder.processes").toString()) // 設置的總進程數 int total_threads = Integer.parseInt(grinder.properties().get("grinder.threads").toString()) // 設置的總線程數 int total_runs = Integer.parseInt(grinder.properties().get("grinder.runs").toString()) // 設置的總運行次數(若設置的是運行時長,則得到0) 
8、Ngringer獲取當前運行的加壓機編號、進程編號、線程編號等信息(都從0遞增):
    int agent_number = grinder.agentNumber // 當前運行的加壓機編號 int process_number = grinder.processNumber // 當前運行的進程編號 int thread_number = grinder.threadNumber // 當前運行的線程編號 int run_number = grinder.runNumber // 當前運行的運行次數編號 
9、Ngringer獲取唯一遞增值方法(從1遞增,不重復):
    // 傳遞接口參數runNumber(即def runNumber = grinder.runNumber) private int getIncrementId(int runNumber){ // 獲取壓力機總數、進程總數、線程總數 int totalAgents = Integer.parseInt(grinder.getProperties().get("grinder.agents").toString()) int totalProcess = Integer.parseInt(grinder.getProperties().get("grinder.processes").toString()) int totalThreads = Integer.parseInt(grinder.getProperties().get("grinder.threads").toString()) // 獲取當前壓力機數、進程數、線程數 int agentNum = grinder.agentNumber int processNum = grinder.processNumber int threadNum = grinder.threadNumber // 獲取唯一遞增數id int incrementId = agentNum * totalProcess * totalThreads + processNum * totalThreads + threadNum + totalAgents * totalProcess * totalThreads * runNumber return incrementId } 
10、Ngringer根據唯一遞增值獲取參數化文件中的唯一行號:
    1)需要設置靜態變量:private enum WhenOutOfValues { AbortVuser, ContinueInCycleManner, ContinueWithLastValue } 2)傳遞接口參數fileDataList(即def fileDataList = new File(dataFilePath).readLines()) private int getLineNum(def fileDataList) { // 獲取當前運行數、數據讀取行數、數據最大行數 int counter = getIncrementId(grinder.runNumber) int lineNum = counter + 1 int maxLineNum = fileDataList.size() - 1 // 讀取最大值的判斷處理 WhenOutOfValues outHandler = WhenOutOfValues.AbortVuser if (lineNum > maxLineNum) { if(outHandler.equals(WhenOutOfValues.AbortVuser)) { lineNum = maxLineNum //grinder.stopThisWorkerThread() } else if (outHandler.equals(WhenOutOfValues.ContinueInCycleManner)) { lineNum = (lineNum - 1) % maxLineNum + 1 } else if (outHandler.equals(WhenOutOfValues.ContinueWithLastValue)) { lineNum = maxLineNum } } return lineNum } 
11、Ngrinder日志輸出配置的測試信息:(import java.text.SimpleDateFormat)
    public static String getTestInfo(){ String time_string = "" // 獲取壓測時設置的進程總數、線程總數、運行次數並在log中打印 int all_process = grinder.getProperties().getInt("grinder.processes", 1) // 設置的總進程數 int all_threads = grinder.getProperties().getInt("grinder.threads", 1) // 設置的總線程數 int all_runs = grinder.getProperties().getInt("grinder.runs", 1) // 設置的總運行次數(若設置的是運行時長,則得到0) int all_duration = grinder.getProperties().getLong("grinder.duration", 1) // 設置的總運行時長(若設置的是運行次數,則得到0) // 格式化時間毫秒輸出(輸出格式00:00:00) SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss") formatter.setTimeZone(TimeZone.getTimeZone("GMT+00:00")) String all_duration_str = formatter.format(all_duration) if (all_duration_str.equals("00:00:00")) time_string = "Test information: the processes is "+all_process+", the threads is "+all_threads+", the run count is "+all_runs+"." else time_string = "Test information: the processes is "+all_process+", the threads is "+all_threads+", the run time is "+all_duration_str+"." return time_string } 
12、Ngrinder打印所有的配置信息
        String property = grinder.getProperties(); grinder.logger.info("------- {}", property) ; 
13、Ngrinder獲取請求返回值:
        HTTPResponse result = request.POST("http://192.168.2.135:8080/blogs", params, headers) 返回的文本:grinder.logger.info("----{}----", result.getText()) // 或者result.text 返回的狀態碼:grinder.logger.info("----{}----", result.getStatusCode()) // 或者result.statusCode 返回的url:grinder.logger.info("----{}----", result.getEffectiveURI()) 返回的請求頭所有參數:grinder.logger.info("---\n{}---", result) 返回的請求頭某參數:grinder.logger.info("----{}---- ", result.getHeader("Content-type")) 
14、Ngrinder返回值的匹配:
       匹配狀態碼:assertThat(result.getStatusCode(), is(200)) 匹配包含文本:assertThat(result.getText(), containsString("success")) 
15、Ngrinder獲取所有虛擬用戶數:
public int getVusers() { int totalAgents = Integer.parseInt(grinder.getProperties().get("grinder.agents").toString()); int totalProcesses = Integer.parseInt(grinder.getProperties().get("grinder.processes").toString()); int totalThreads = Integer.parseInt(grinder.getProperties().get("grinder.threads").toString()); int vusers = totalAgents * totalProcesses * totalThreads; return vusers; } 
 
        
16、Ngrinder的斷言和error日志輸出
if (result.statusCode == 301 || result.statusCode == 302) { grinder.logger.error("Possible error: {} expected: <200> but was: <{}>.",result.getEffectiveURI(),result.statusCode); } else { assertEquals((String)result.getEffectiveURI(), result.statusCode, 200) assertThat((String)result.getEffectiveURI(), result.statusCode, is(200)) } 

測試腳本示例

nGrinder測試腳本
import  HTTPClient.HTTPResponse
import  HTTPClient.NVPair
import  net.grinder.plugin.http.HTTPPluginControl
import  net.grinder.plugin.http.HTTPRequest
import  net.grinder.script.GTest
import  net.grinder.scriptengine.groovy.junit.GrinderRunner
import  net.grinder.scriptengine.groovy.junit.annotation.AfterThread
import  net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import  net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import  org.junit.Test
import  org.junit.runner.RunWith
import  static  org.junit.Assert.assertThat
import  static  org.hamcrest.Matchers.is
import  static  net.grinder.script.Grinder.grinder
 
@RunWith (GrinderRunner) // 每個測試類加這注解
class  TestRunner{
 
     public  static  GTest test1
     public  static  HTTPRequest request
 
     @BeforeProcess // 在每個進程啟動前執行
     static  void  beforeProcess() {
         HTTPPluginControl.getConnectionDefaults().timeout =  8000
         test1 =  new  GTest( 3  , "queryLoanCounts" );
         request =  new  HTTPRequest()
     }
     @BeforeThread  // 在每個線程執行前執行
     void  beforeThread() {
         test1.record( this , "queryLoanCounts" );
         // 延時生成報告
         grinder.statistics.delayReports= true ;
     }
     private  NVPair[] headers() {
         return  [
                 new  NVPair( "Content-type" "application/json;charset=UTF-8" )
         ];
     }
     @Test
     public  void  queryLoanCounts(){
         String json =  "{\"uid\": \"1_2000008\"}" ;
         HTTPResponse result = request.POST( "http://core.com/query-loaning-count" ,json.getBytes(), headers());
         grinder.logger.info(result.getText());
         if  (!result.statusCode ==  200 ) {
             grinder.logger.warn( "Warning. The response may not be correct. The response code was {}." , result.statusCode);
         else  {
             assertThat( "判斷響應結果:" ,result.statusCode, is( 200 ));
         }
     }
 
     @AfterThread
     public  void  afterThread(){
 
     }
}

使用工具進行腳本的編寫

IDE編寫測試腳本

 

進程和線程的用法

六、執行測試

6.1、創建腳本

6.2、 創建測試計划

6.3、執行控制面板

6.4、測試報告面板

 

 

 

 

 

 

 

 

 

 

 

思考:

為什么要部署多個agent?

當線程數量過多的時候,實際的壓力可能不會提升。由於agent本身的瓶頸,導致壓力下發不下去。 當壓力測試結果表現為:線程數量增多,響應時間和tps數卻無變化,說明agent本身已經達到瓶頸了,無法再增加更多的壓力。 這時候就需要部署多個agent給被測服務。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM