目錄:
-
性能測試工具的選型
-
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上部署。
選擇最新的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)) }
測試腳本示例
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(){
}
}
|
使用工具進行腳本的編寫
進程和線程的用法
六、執行測試
6.1、創建腳本
6.2、 創建測試計划
6.3、執行控制面板
6.4、測試報告面板
思考:
為什么要部署多個agent?
當線程數量過多的時候,實際的壓力可能不會提升。由於agent本身的瓶頸,導致壓力下發不下去。 當壓力測試結果表現為:線程數量增多,響應時間和tps數卻無變化,說明agent本身已經達到瓶頸了,無法再增加更多的壓力。 這時候就需要部署多個agent給被測服務。