結合jenkins以及PTP平臺的效能迴歸測試
此文已由作者餘笑天授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
1背景簡介
1.1 jenkins
Jenkins是一個用Java編寫的開源的持續整合工具。在與Oracle發生爭執後,專案從Hudson專案復刻。Jenkins提供了軟體開發的持續整合服務。它執行在Servlet容器中(例如Apache Tomcat)。它支援軟體配置管理(SCM)工具(包括AccuRev SCM、CVS、Subversion、Git、Perforce、Clearcase和RTC),可以執行基於Apache Ant和Apache Maven的專案,以及任意的Shell指令碼和Windows
1.2 PTP平臺
效能測試一直是業界重點關注的部分,但是複雜的效能測試過程卻讓很多人望而生畏:管理測試用例、收集測試資料、進行資料分析、編寫測試報告,每一項都需要耗費很多心血。
於是,PTP平臺就這樣應運而生了,它是網易自主開發的自動化效能測試平臺,致力於將效能測試過程自動化、標準化、一體化,並且將效能測試過程持續起來,進行更多資料分析。
2自動化流程
2.1建立任務
QA管理員擁有新建節點許可權,如需增加新節點,請找各自的QA管理員。QA管理員在Jenkins上新增一個新節點步驟如下:
(1)點選連結進入
(2)輸入節點名稱,節點名稱通常以伺服器hostname或者機器描述命名,比如qa10.server,ddb-23.photo,QA_AutoTest_1等。
(3)選擇Dumb Slave選項,點選OK按鈕
(4)輸入以下設定:
a.# of executors:輸入執行器的個數(一個或者多個):這個值控制著Jenkins併發構建的數量, 因此這個值會影響Jenkins系統的負載壓力。使用處理器個數作為其值會是比較好的選擇。
b.Remote FS root:輸入slave機器作為持續整合Home的路徑
c.Labels:用來對多節點分組,在目前杭研的應用中,我們一般設定其跟節點名稱一樣
d.用法:一般選只執行繫結到這臺機器的job
e.Launch Method選擇Launch slave agents via Java Web Start
(5)儲存
Node Properties可設定環境變數,如果不設定就會使用jenkins主機上全域性定義的環境變數,如下圖所示:
更詳細的建立教程可參見wiki:http://doc.hz.netease.com/pages/viewpage.action?pageId=36463105
2.2 自動化環境部署
Jenkins上新增配置好的節點,如下所示:
編寫自動化部署指令碼:
import requests import time import os import sys # web is deployed on two servers,the arguments in url:moduleId,envId,instanceId test_web_arg_1 = ('***','***','***') basi_url = 'http://omad.hz.netease.com/api' productId = '***' envName='urs-regzj-perftest' branch='perftest_jenkins' def get_token(appId, appSecret): r = requests.get(basi_url + '/cli/login?appId=%s&appSecret=%s' % (appId, appSecret)).json return r['params']['token'] def deploy_web(appId, appSecret,moduleId,envId): test_web_url = '/cli/deploy?token=%s&moduleId=%s&envId=%s'%(get_token(appId, appSecret),moduleId, envId) r = requests.get(basi_url + test_web_url).json print 'Deploy result:' def get_status(appId, appSecret,envId,instanceId): status_url = '/cli/istatus?token=%s&envId=%s&instanceId=%s'%(get_token(appId, appSecret), envId, instanceId) r = requests.get(basi_url + status_url).json return r['deployStatus'],r['status'] def check_deploy_result(appId, appSecret,envId,instanceId): status = get_status(appId, appSecret,envId,instanceId) print 'building .......' times = 0 while status[0] == 'success': status = get_status(appId, appSecret,envId,instanceId) times += 1
該過程主要是呼叫OMAD介面實現了自動化部署,分為以下幾個步驟:
(1)呼叫/api/cli/login介面獲取個人token資訊;
(2)呼叫/api/cli/vcchange介面對指定產品的指定環境切換成指定分支;
(3)呼叫/api/cli/ls介面獲取當前使用者有許可權的所有產品的所有工程的資訊;
(4)呼叫/api/cli/deploy介面對指定環境的指定分支進行構建部署。
執行方式為python omad.py AccessKeyAccessSecret,其中$AccessKey和$AccessSecret為登入OMAD後的個人認證資訊。
2.3 自動化指令碼除錯
在指令碼執行前,我們需要指令碼除錯這個過程,該過程用來驗證指令碼是否能被正確執行,若指令碼本來就存在問題等到執行時再去發現問題就可能浪費大量執行時間,因此在這個階段,我們需要執行一次指令碼,並驗證指令碼是否正確。
首先我們需要將所有的指令碼上傳到節點上,並保證該節點機安裝有一些壓測工具,這裡以grinder為例,首先需要配置grinder.properties檔案,以我的例子來說明:
script1 = createUser script2 = updateUinfo script3 = updateToken script4 = getUserInfo script5 = setSpecialRelation script6 = updateUserID script7 = getToken script8 = addFriend script9 = getFriendRelation script10 = updateRelationship script11 = addGroup script12 = queryTeam script13 = queryTeamNoUser script14 = joinTeams script15 = sendTeamMsg script16 = SendCustomMessage script17 = sendGroupMessage script18 = sendBatchAttachMsg script19 = sendBatchMsg script20 = kick grinder.script = Serial.py grinder.processes = 1 grinder.threads = 1 grinder.runs = 1
script.*代表是待除錯指令碼的名稱,Serial.py是主指令碼名,grinder.processes ,grinder.threads,grinder.runs 分別是grinder的程序,執行緒,以及執行次數,因為這部分主要是除錯指令碼,這裡的引數全部設定為1。Serial.py實際是一個序列指令碼,它負責順序執行各指令碼,程式碼如下所示:
from net.grinder.script.Grinder import grinder from java.util import TreeMap # TreeMap is the simplest way to sort a Java map. scripts = TreeMap(grinder.properties.getPropertySubset("script")) # Ensure modules are initialised in the process thread. for module in scripts.values(): exec("import %s" % module) def create_test_runner(module): x='' exec("x = %s.TestRunner()" % module) return x class TestRunner: def __init__(self): self.testRunners = [create_test_runner(m) for m in scripts.values()] # This method is called for every run. def __call__(self): #create_test_runner() for testRunner in self.testRunners: testRunner()
執行完該指令碼後需要驗證該指令碼的正確性,我的做法是驗證classb-im14-0-data.log下的日誌資訊,讀取error列的值,具體程式碼如下:
info = [] f = open('result.txt', 'w') path = os.getcwd() #print path path+='/logs' os.chdir(path) path = os.getcwd() #print path file=open('classb-im14-0-data.log','r') count=len(file.readlines()) while(count!=interfaceNum): count=len(file.readlines()) file=open('classb-im14-0-data.log','r') for line in file: info.append(line.strip()) if line.find("Thread")>=0: continue else: vec=line.split(',') if vec[5].strip()!='0': #print vec[5] str=testIdToScene(vec[2].strip()) if str==None: f.write('testId does not exit') excuteflag=False break else: str+=(' Error\n') f.write(str) flag=False if flag==True and excuteflag==True: f.write('All interfaces have been successfully executed') f.close() file.close()
以上指令碼實現了讀取error值的功能,但是在jenkins上即使執行過程中產生錯誤,只要構建過程中每個程式的退出狀態是正常的,仍然會顯示構建成功,為此需要編寫以下指令碼,使指令碼執行失敗時保證該構建過程同時失敗:
#!/bin/bash if grep "All interfaces have been successfully executed" result.txt then echo "result is right" exit 0 else echo "result is wrong" exit 1 fi
該指令碼在有指令碼執行失敗的情況下會強制退出狀態為1,從而使得構建失敗。
2.4 自動化指令碼執行以及結果收集
指令碼執行需要藉助ptp平臺的外掛,具體如圖所示:
執行完成後,需要獲取PTP平臺的執行結果,判斷執行過程中是否有錯誤產生,具體指令碼如下所示:
import os flagSucess=True path = os.getcwd() path_pertest=path path+='/projects' path_curr=path f=open("/home/qatest/monitorTools/conf/topnFilesRes.txt") file = open('result.txt', 'w') info=[] for line in f: tmp=line.strip() path+="/"+tmp info.append(path) path=path_curr for i in info: i+="/logs" os.chdir(i) fileSize = os.path.getsize("error_grinder.log") if fileSize!=0: flagSucess=False os.chdir(path_pertest) i += " make an error" file.write(i) if flagSucess: file.write("All rounds have been successfully executed")
完成該部分後需要將測試結果持久化到資料庫,這部分的思路是呼叫平臺的/api/v1.0/round/${roundId}/summary介面,解析json資料,然後插入到資料庫,具體程式碼如下。
首先需要利用httpclient獲取該介面的結果然後進行解析:
public class GetRoundsAndJasonParse { @SuppressWarnings("finally") public String getJasonRes(String roundID) throws HttpException { String res=null; String prefix="http://perf.hz.netease.com/api/v1.0/round/"; prefix+=roundID; prefix+="/summary"; HttpClient client = new HttpClient(); GetMethod getMethod = new GetMethod(prefix); try { client.executeMethod(getMethod); //res = new String(getMethod.getResponseBodyAsString()); BufferedReader reader = new BufferedReader(new InputStreamReader(getMethod.getResponseBodyAsStream())); StringBuffer stringBuffer = new StringBuffer(); String str = ""; while((str = reader.readLine())!=null) { stringBuffer.append(str); } res = stringBuffer.toString(); } catch (HttpException e) { e.printStackTrace(); } finally { getMethod.releaseConnection(); return res; } } public ArrayList<Perf> getValue(JsonObject json,String[] key) { FormattingPerf fp = new FormattingPerf(); ArrayList<Perf> res=new ArrayList<Perf>(); ArrayList<String> values=new ArrayList<String>(); String machine_name=null; String test_id=null; String tmp=null; try { //if(json.containsKey(key)) String resStr = json.get("success").getAsString(); if(resStr.equals("false")) System.out.println("Check your roundID"); else { JsonArray array=json.get("data").getAsJsonArray(); for(int i=0;i<array.size();i++) { JsonObject subObject=array.get(i).getAsJsonObject(); machine_name=subObject.get("machine_name").getAsString(); test_id=subObject.get("test_id").getAsString(); if(machine_name.equals("all")&&!test_id.equals("0")) { for(int j=0;j<key.length;j++) { tmp=subObject.get(key[j]).getAsString(); values.add(tmp); } Perf perf=new Perf(values); fp.formatPerf(perf); res.add(perf); values.clear(); } } } } catch (Exception e) { e.printStackTrace(); } return res; } @SuppressWarnings("finally") public ArrayList<Perf> parseJason(String jasonbody) throws JsonIOException, JsonSyntaxException { //ArrayList<String> res=new ArrayList<String>(); ArrayList<Perf> res=new ArrayList<Perf>(); JsonParser parse =new JsonParser(); try { JsonObject json=(JsonObject) parse.parse(jasonbody); String[] key={"test_id","perf_round_id","tps","response_ave","response90","err_rate","mean_response_length"}; res=getValue(json,key); } catch (JsonIOException e) { e.printStackTrace(); } catch (JsonSyntaxException e) { e.printStackTrace(); } finally { return res; } }
然後需要進行進行資料持久化的操作,這部分的程式碼實現的方式有多重,就不在此贅述,至此完成了自動化迴歸的部分過程,後續的結合哨兵監控以及對資源、效能資料進行進一步分析可以做更多的工作,歡迎有興趣的同學一起來討論。
更多網易技術、產品、運營經驗分享請點選。
相關文章:
【推薦】 HBase原理–所有Region切分的細節都在這裡了