目錄
一.測試環境介紹
二.本次測試參數介紹
三.解碼程式介紹
四.HDFS測試
五.本地測試
六.CPU測試
七.pod負載問題
總結:
公司建立了一個全局的kubernetes叢集,伺服器在泰國,使用rancher2.5.5搭建的。因為裡面配置的資源很大,但是還沒有正式的應用部署在上面,上司要求部署幾個app上去跑跑資源。決定下來将解碼程式放上去跑跑,解碼程式是将工廠測試資料的檔案解碼出來,一個serial numner檔案一般有240張表,全部解出寫入存儲目錄,一個serial number一張表為一個目錄,先不壓縮,直接寫txt檔案。
一.測試環境介紹
Rancher版本2.5.1
節點配置
master | 3 | cpu | 48core | memrory | 125G |
node | 8 | cpu | 16core | memrory | 48G |
二.本次測試參數介紹
測試資料量
sn數量:1780sn
寫入資料大小:95G
寫入格式:txt
測試參數
Cpu:pod的cpu參數(基本測試了2core和4core的情況)
Memrory: Pod的memrory參數(基本測試了8G和12G的情況)
Concurrent Thread: sn的并發數,請求數,同時有多少個sn在請求服務(1,16,24,32,48,65,80)
Table write Thread: 每個sn會解碼出240張表左右,此參數為并發多少個table同時寫存儲(1,16,50)
Pod number: 服務端程式負載均衡的Pod數量(6,8,12,16,20)
三.解碼程式介紹
服務端使用springboot打包,并使用docker打成鏡像
服務端程式代碼部分,傳入本次reqeustId和serial num相關的資訊。解碼分三步,第一步根據sn檔案位址去下載下傳sn檔案,第二步解碼sn檔案成一個hashmap,key是table name,value是一個List<String>,第三步将hashmap寫入存儲。每一步記錄處理時間,傳回到client。
/**
*解碼程式,三步
*1.download檔案
*2.解碼
*3.寫入存儲
*/
@PostMapping(value = "/parseFileBySerialModelWriteAllTable")
public String parseFileBySerialModelWriteAllTable(@RequestParam(name = "requestId") String requestId,
@RequestParam(name = "serialNum") String serialNum,
@RequestParam(name = "host") String host,
@RequestParam(name = "remotePath") String remotePath,
@RequestParam(name = "fileName") String fileName,
@RequestParam(name = "type") String type,
@RequestParam(name = "fileType") String fileType,
@RequestParam(name = "tableAlias") String tableAlias,
@RequestParam(name = "tableThread") String tableThread) throws Exception {
logger.info("parseFileBySerialModelWriteAllTable in");
String errorInf = "";
long downloadTime = 0;
long parseTime = 0;
long writeTableTime = 0;
int tableSize = 0;
try {
long t1 = System.currentTimeMillis();
DownloadHelper downloadHelper = new DownloadHelper();
byte[] bytes7 = downloadHelper.download(host, remotePath, fileName, Integer.parseInt(type), fileType);//下載下傳檔案==================================
downloadTime = System.currentTimeMillis() - t1;
t1 = System.currentTimeMillis();
logger.info("download end");
/*InputStream in = new FileInputStream(remotePath);
byte[] bytes7 = IOReader.read(name, in, fileType);*/
if(bytes7 == null || bytes7.length == 0) {
throw new Exception("download empty !" + serialNum);
}
ParserBasic parser = new ParserBasic() {
@Override
public boolean include(Object tableCode) {
if(tableAlias == null || "".equals(tableAlias))
return true;
else if(tableAlias.equals(tableCode.toString()))
return true;
else
return false;
}
@Override
public String getTableRow(Map<String, String> tbStates, String tableRow) {
// System.out.println("TEST_DATE = " + tbStates.get(SysCnt.TEST_DATE));
return tbStates.get(SysCnt.SEQ) + "," +
tbStates.get(SysCnt.TEST_TIME) + ","+
tbStates.get(SysCnt.SPC_ID) + ","+
tbStates.get(SysCnt.TEST_SEQ_EVENT) + ","+
tbStates.get(SysCnt.STATE_NAME) + ","+
tbStates.get(SysCnt.OCCURRENCE) + "," +
StringUtil.removeBlank(tableRow) + ENTER;
}
@Override
public String getMapKey(Map<String, String> tbStates, int globalTableId) throws Exception {
return String.valueOf(globalTableId);
}
};
parser.setParseDefine(true);
Map<String, List<String>> tables = (fileType.equals(SysCnt.FILE_TYPE_BTR)) ? parser.parser(bytes7): parser.parser_csv(bytes7);//解碼程式===========================
tableSize = tables.size();
if(tables == null || tables.keySet().size() == 0) {
throw new Exception("parser table empty !" + serialNum);
}
parseTime = System.currentTimeMillis() - t1;
t1 = System.currentTimeMillis();
String writePath = requestId + "/" + serialNum;
parseService.writeTableData(writePath, tables,Integer.parseInt(tableThread));//寫入資料=================================
writeTableTime = System.currentTimeMillis() - t1;
parser.emptyParser();
logger.info("parseFileBySerialModel tables length:"+tables.keySet().size());
logger.info("parseFileBySerialModel =============================================" + serialNum);
}catch(Exception e) {
logger.error(e.getMessage(),e);
errorInf = e.getMessage();
}
if(!"".equals(errorInf)) {
return "{success: 0,error:\""+errorInf+"\",IP:"+InetAddress.getLocalHost().getHostAddress()+",downloadTime:"+downloadTime+",parseTime:"+parseTime+",tableSize:"+tableSize+",writeTableTime:"+writeTableTime+"}";
}else {
return "{success: 1,error:\"\",IP:"+InetAddress.getLocalHost().getHostAddress()+",downloadTime:"+downloadTime+",parseTime:"+parseTime+",tableSize:"+tableSize+",writeTableTime:"+writeTableTime+"}";
}
}
/**
*寫資料,兩種方式
*1.寫入hdfs
*2.寫本地
*
*/
public void writeTableData(String writePath,Map<String, List<String>> tables,int tableThread) throws InterruptedException {
Set<String> tableKey = tables.keySet();
ExecutorService executeService = Executors.newFixedThreadPool(tableThread);
Iterator<String> tableNameList = tableKey.iterator();
while(tableNameList.hasNext()) {
String tableName = tableNameList.next();
String path = comPath + writePath + "/" + tableName + "/";
String input = list2line(tables.get(tableName));
//setTableIndex();
executeService.execute(new Runnable() {
public void run(){
try {
write2Hdfs(path,input);
} catch (Exception e) {
System.out.println("iii");
e.printStackTrace();
}
}
});
}
executeService.shutdown();
//System.out.println("=============write tables threads end!");
while(true){
if(executeService.isTerminated()){
System.out.println("=============write tables end!");
break;
}
Thread.sleep(1000);
}
}
/**
*處理lsit<string>
*/
public static String list2line(List<String> list) {
StringBuilder sb = new StringBuilder(1024 * 1024);
for (String s : list) {
sb.append(s).append("\n");
}
return sb.toString();
}
/**
*寫hdfs方法
*/
public static void write2Hdfs(String location, String input) throws IOException {
FileSystem fs = ServiceSupport.getFileSystem();
//System.out.println("fs address:"+fs.toString());
FSDataOutputStream fsdos = fs.create(new Path(location + "data.txt"));
fsdos.write(input.getBytes());
System.out.println("write path success:"+location);
fsdos.flush();
fsdos.close();
fsdos = null;
//fs.close();
}
/**
*寫本地方法
*/
public static void write2local(String location, String input) throws Exception {
File file = new File(location + "data.txt");
if(!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
byte bt[] = new byte[1024];
bt = input.getBytes();
try {
FileOutputStream in = new FileOutputStream(file);
try {
in.write(bt, 0, bt.length);
} catch (IOException e) {
throw e;
} finally {
in.close();
}
} catch (FileNotFoundException e) {
throw e;
}
}
注:為保證能支援多種case,服務端程式寫了多個restful api,如下
/parseFileBySerialModelWriteAllTable:寫全表,每個表一個檔案,寫入hdfs
/parseFileBySerialModelWriteSplitTable:一定數量表做成一個輸入寫一個檔案,寫入hdfs
/parseFileBySerialModelWriteOneFile:所有表寫一個檔案,寫入hdfs
/parseFileBySerialModelNotWrite:隻解碼,不寫入任何存儲
/parseFileBySerialModelWriteAllTableLocal:寫全表,每個表一個檔案,寫入本地存儲
/parseFileBySerialModelWriteAllTableLocalOneFile:所有表寫一個檔案,寫入本地存儲
----------------------------------------分割線-------------------------------------------------------------------------------
用戶端程式
/**
*擷取sn的資訊,控制并發數,調用服務端
*此處使用的測試資料是89個sn的資料,并使用循環,将89個sn的資料模拟成20倍1780個sn
*/
public void callParseBySameSn(String path) throws Exception {
System.out.println("parse start:12G,16POD,4core,50Thread,1780sn,write one file for all table");
System.out.println("=============getSnByInputPath start !");
List<String> snList = parseService.get89Sn();//擷取89個sn
/*String model1 = "WAF2LZPK,9";
List<String> snList = new ArrayList<>();
snList.add(model1);*/
System.out.println("=============getSnByInputPath end !");
List<SnModel> models = parseService.getSnModelFile(snList);//擷取89sn資料的檔案位址
System.out.println("=============getSnModelFile end !");
//String requestId = UUID.randomUUID() + "";
long t1 = System.currentTimeMillis();
ExecutorService service = Executors.newFixedThreadPool(24);//并發數,代表有多少個sn同時調用解碼程式
String majorDir = "M_"+getLocalTimeFormat("yyyyMMddHHmmssSSS");//每次調用的唯一辨別,會作為目錄傳入服務端
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
for(int loop=0;loop<20;loop++){
String dateMill = getLocalTimeFormat("yyyyMMddHHmmssSSS");
String requestId = majorDir + "/" + dateMill;//唯一辨別補充,因為循環20次,每次生成一個不同的目錄。相當于有20個目錄,存儲了同一個份資料(89sn的解碼資料)
for (int i = 0; i < models.size(); i++) {
/*String[] mod = snList.get(i).split(",");
String serialNum = mod[0];
String transSeq = mod[1];
String host = mod[4];
String remotePath = mod[3];;
String fileName = mod[2];;
String type = "0";
String fileType = mod[6];
String tableAlias = "626";
//String tableAlias = "P250_ERROR_RATE_BY_ZONE";*/
String serialNum = models.get(i).getSerialNum()+"_"+models.get(i).getTransSeq();
String transSeq = models.get(i).getTransSeq();
String orginHost = models.get(i).getHost();
String orginPath = "";
if(orginHost.indexOf(".") == -1){
String hostName = setHost(new File(orginHost+"/").getPath().replace("\\", "/"));
orginHost = hostName+".wux.chin.seagate.com";
String location = new File(hostName+ "/" + models.get(i).getPath() + "/" + models.get(i).getFileName()).getPath().replace("\\", "/");
orginPath = location;
}else{
String location = new File(models.get(i).getHost() + "/" + models.get(i).getPath() + "/" + models.get(i).getFileName()).getPath().replace("\\", "/");
orginHost = setHost(location);
orginPath = setPath(location, orginHost);
}
String host = orginHost;
String remotePath = orginPath;
/*String location = new File(models.get(i).getHost() + "/" + models.get(i).getPath() + "/" + models.get(i).getFileName()).getPath().replace("\\", "/");
String host = setHost(location);
String remotePath = setPath(location,host);*/
String fileName = models.get(i).getFileName();
String type = "0";
String fileType = models.get(i).getFileType();
String tableAlias = "";
//String tableAlias = "P250_ERROR_RATE_BY_ZONE";
service.submit(new Runnable() {
@Override
public void run() {
try {
//調用服務端,此處的最後兩個參數說明,16是解碼後寫會有240個表生成的hashmap,這裡定義這些表以多少個線程去寫這些表。50是另一個參數,沒有這個參數的時候,每個表會生成一個目錄,這個參數設定的是将多少個表合并,再統一寫入一個目錄,此參數是為了測試降低寫入資料的頻次,此參數在本次測試隻用了一次,在下文未提及此參數時,都是按照每個表寫一個目錄的情況
String out = CallRestUrl.callSeadoopMrPostBySerial(
requestId, serialNum, transSeq, host,
remotePath, fileName, type, fileType,
tableAlias,"16","50");
String key = out.split("IP:")[1].split(",")[0];
if(map.containsKey(key)){
map.put(key, map.get(key)+1);
}else{
map.put(key, 1);
}
System.out.println("Success :"+getSuccessCount()+" "+serialNum+" end,cost "+(System.currentTimeMillis() - t1)+" :" + out);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
}
System.out.println("=============All threads have stated !");
service.shutdown();
while(true){
if(service.isTerminated()){
System.out.println("=============All threads have end!");
Enumeration<String> em = map.keys();
while(em.hasMoreElements()){
String tt = em.nextElement();
System.out.println(tt+":"+map.get(tt));
}
break;
}
Thread.sleep(1000);
}
}
/**
*遠端調用service
*/
public static String callSeadoopMrPostBySerial(String requestId,String serialNum,String transSeq,
String host,String remotePath,String fileName,String type,String fileType,String tableAlias,String tableThread,String tableCount) throws Exception{
//URL postUrl = new URL("http://10.38.199.201:30088/parseFileBySerialModelWriteAllTableLocal?");
//URL postUrl = new URL("http://10.38.150.64:8088/parseFileBySerialNum?");
//URL postUrl = new URL("http://10.43.79.103:8088/parseFileBySerialModelWriteAllTableLocal?");
//URL postUrl = new URL("http://10.38.199.203:8088/parseFileBySerialModel?");
URL postUrl = new URL("http://10.4.140.149:30088/parseFileBySerialModelWriteAllTableLocal?");//GIS dev environment
HttpURLConnection connection = (HttpURLConnection) postUrl.openConnection();
connection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(true);
connection.connect();
connection.setReadTimeout(0);
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append("uuid=" + URLEncoder.encode(UUID.randomUUID() + "", "UTF-8")).append("&");
sb.append("requestId=" + URLEncoder.encode(requestId, "UTF-8")).append("&");
sb.append("serialNum=" + URLEncoder.encode(serialNum, "UTF-8")).append("&");
sb.append("transSeq=" + URLEncoder.encode(transSeq, "UTF-8")).append("&");
sb.append("host=" + URLEncoder.encode(host, "UTF-8")).append("&");
sb.append("remotePath=" + URLEncoder.encode(remotePath, "UTF-8")).append("&");
sb.append("fileName=" + URLEncoder.encode(fileName, "UTF-8")).append("&");
sb.append("type=" + URLEncoder.encode(type, "UTF-8")).append("&");
sb.append("fileType=" + URLEncoder.encode(fileType, "UTF-8")).append("&");
sb.append("tableAlias=" + URLEncoder.encode(tableAlias, "UTF-8")).append("&");
sb.append("tableThread=" + URLEncoder.encode(tableThread, "UTF-8")).append("&");
sb.append("tableCount=" + URLEncoder.encode(tableCount, "UTF-8"));
out.writeBytes(sb.toString());
out.flush();
out.close();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line = reader.readLine();
/*while ((line = reader.readLine()) != null){
System.out.println(line);
}*/
reader.close();
connection.disconnect();
return line;
}
寫入後的資料目錄如下
寫入的資料格式如下
四.HDFS測試
在考慮資料往什麼地方寫的時候,一個往hdfs寫,或者使用kubernetes的pv,還有就是往對象存儲ceph或者S3寫。考慮現有環境,還是往hdfs寫是最友善的。應用部署在一個199網段的叢集裡,裡面裝了Rancher2.5.1,建立了kubernetes叢集。因為199叢集的Hadoop環境存在問題,datanode很多都挂了,起不來,是以想把資料直接寫到另一個測試環境的149網段hadoop環境裡。
可以預見的,從199往149寫,網絡傳輸肯定是一個問題,其次hdfs寫資料的性能也是關鍵因素。往hdfs寫資料使用的java支援的hdfs jar包。
從之前設計的幾個方案調整了參數,初步的測試結果如下
1780sn,90G,txt | |||||||||||
memory | 12G | ||||||||||
pod num | 8 | 16 | 25 | ||||||||
cpu | 2 | 4 | 8 | 2 | 4 | 2 | |||||
concurrent thread | 50 | 1 | 50 | 50 | 50 | 50 | 50 | 100 | 100 | 25 | 50 |
table write thread | 50 | 50 | 16 | 50 | 50 | 50 | 16 | 50 | 50 | 50 | 50 |
Time(min) | 18.1 | 206sn/18.2 | 18.6 | 18.1 | 18.4 | 17.8 | 18.7 | 16 | 17.4 | 21.9 | 18.1 |
從表格可以看出,不論哪種配置,最終跑完的時間都在18分鐘左右(紅色字型是跑了206個serial number,18.2分鐘),這似乎與測試場景不符合,那麼肯定是有瓶頸。具體在哪裡,就需要排查了。
首先從傳回的日志來看,時間主要耗費在寫資料,可以看出download和parse解碼時間不是很長,寫資料基本都在十幾秒到二十秒,這裡一定是不正常。
為了測試寫資料是否是真的寫的慢還是由于資源競争導緻的,我測試了并發數為一個的情況,就是表格中紅字的那個測試場景,結果如下,可以看出單跑一個并發,不管是download,parse還是寫資料都是很快的。由此可以看出上圖出現的寫資料慢必定是資源競争造成的。
那麼到底是什麼資源在競争,哪個才是瓶頸?無非幾個:cpu,memrory,network,還有磁盤寫入的IO。咱們一個個看。
第一個看cpu,在跑的過程中,我用rancher的grafana插件檢視cpu的占用,2core的cpu在程式運作期間,整個負載并不高。在檢視4core的時候,也是類似情況,這裡說明整個程式cpu的占用率并不高。
從另一個監控看cpu的使用率,主要看cpu usage,在跑的過程中,cpu usage始終不高(注:1000m為一個cpu核數)
第二再看memrory,這裡使用了kubernete指令檢視了pod的記憶體變化,記憶體逐漸在上升,且到達頂點後基本就不動了,因為是java程式,jvm到達最大記憶體後,是不會釋放記憶體的,記憶體的管理都是在jvm裡面,從這裡并不能看出記憶體的影響,需要到jvm内部檢視記憶體的影響。
[[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 1287m 7793Mi
parse-dev-575f597d85-4qs2t 11m 4492Mi
parse-dev-575f597d85-5l7gb 1105m 7372Mi
parse-dev-575f597d85-6nmqf 2423m 10609Mi
parse-dev-575f597d85-csxfw 797m 6621Mi
parse-dev-575f597d85-jrj24 1611m 10242Mi
parse-dev-575f597d85-qjt2t 975m 6948Mi
parse-dev-575f597d85-snql8 1097m 8500Mi
[ro[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 1214m 9419Mi
parse-dev-575f597d85-4qs2t 384m 5026Mi
parse-dev-575f597d85-5l7gb 743m 8551Mi
parse-dev-575f597d85-6nmqf 1964m 12836Mi
parse-dev-575f597d85-csxfw 195m 7891Mi
parse-dev-575f597d85-jrj24 2315m 12708Mi
parse-dev-575f597d85-qjt2t 1319m 9285Mi
parse-dev-575f597d85-snql8 1927m 11488Mi
[[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 1149m 12612Mi
parse-dev-575f597d85-4qs2t 277m 5251Mi
parse-dev-575f597d85-5l7gb 1770m 10514Mi
parse-dev-575f597d85-6nmqf 792m 12844Mi
parse-dev-575f597d85-csxfw 1463m 8387Mi
parse-dev-575f597d85-jrj24 844m 12719Mi
parse-dev-575f597d85-qjt2t 954m 11074Mi
parse-dev-575f597d85-snql8 1366m 12671Mi
[[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 468m 12614Mi
parse-dev-575f597d85-4qs2t 6m 6168Mi
parse-dev-575f597d85-5l7gb 501m 12692Mi
parse-dev-575f597d85-6nmqf 1611m 12855Mi
parse-dev-575f597d85-csxfw 1298m 11440Mi
parse-dev-575f597d85-jrj24 1549m 12736Mi
parse-dev-575f597d85-qjt2t 863m 12596Mi
parse-dev-575f597d85-snql8 1288m 12726Mi
[[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 641m 12748Mi
parse-dev-575f597d85-4qs2t 5m 6846Mi
parse-dev-575f597d85-5l7gb 841m 12706Mi
parse-dev-575f597d85-6nmqf 1073m 12860Mi
parse-dev-575f597d85-csxfw 986m 12629Mi
parse-dev-575f597d85-jrj24 1458m 12744Mi
parse-dev-575f597d85-qjt2t 1212m 12790Mi
parse-dev-575f597d85-snql8 996m 12726Mi
[[email protected] seadoopmr]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
parse-dev-575f597d85-44x47 1/1 Running 0 29s 10.42.17.182 t008 <none> <none>
parse-dev-575f597d85-4qs2t 1/1 Running 0 30s 10.42.14.132 t020 <none> <none>
parse-dev-575f597d85-5l7gb 1/1 Running 0 29s 10.42.20.171 t013 <none> <none>
parse-dev-575f597d85-6nmqf 1/1 Running 0 30s 10.42.19.197 t012 <none> <none>
parse-dev-575f597d85-csxfw 1/1 Running 0 29s 10.42.16.179 t019 <none> <none>
parse-dev-575f597d85-jrj24 1/1 Running 0 29s 10.42.18.170 t011 <none> <none>
parse-dev-575f597d85-qjt2t 1/1 Running 0 29s 10.42.10.178 t014 <none> <none>
parse-dev-575f597d85-snql8 1/1 Running 0 30s 10.42.11.173 t015 <none> <none>
[[email protected] seadoopmr]# kubectl top pods
NAME CPU(cores) MEMORY(bytes)
parse-dev-575f597d85-44x47 673m 12768Mi
parse-dev-575f597d85-4qs2t 202m 8539Mi
parse-dev-575f597d85-5l7gb 1169m 12727Mi
parse-dev-575f597d85-6nmqf 1440m 12878Mi
parse-dev-575f597d85-csxfw 251m 12696Mi
parse-dev-575f597d85-jrj24 850m 12802Mi
parse-dev-575f597d85-qjt2t 471m 12815Mi
parse-dev-575f597d85-snql8 1498m 12744Mi
Rancher的監控如下,可見記憶體在一直上升,且到達jvm最大記憶體後趨平。另外還有一個情況,為甚有些線在最高點突然會降到最低點500M,原因是pod記憶體超出了限制,導緻pod重新開機,pod重新開機後記憶體重新劃分到jvm最低啟動記憶體500M,所有會出現從高點到低點的現象。至于pod為何會重新開機,首先設定的jvm堆記憶體是12G,設定pod的記憶體資源限制是14G。因為java程式運作時,不僅使用堆記憶體,也會使用本地記憶體,當本地記憶體超出了14-12=2G,整個pod的記憶體會超過14G,此時kubernetes會殺掉該pod,重新開機啟動,這也導緻了這樣的現象。
pod restart的情況
從pod内部檢視memror變化。在程式跑的時候,我進入到多個pod内部,檢視了jvm記憶體的變化及GC的時間,如下。可以看出jvm的記憶體在一直變化,但是整個程式跑完GC的時間也就39s左右,并不是很長,也沒有出現頻繁full GC的情況,可以看出記憶體雖然一直在工作,但并不是程式的瓶頸。
程式過程中的GC情況
跑完之後的GC情況
排除了cpu和memrory,那麼再看網絡,總的計算18分鐘,寫了95G的資料,可以計算寫入速率是90MB/s。
下圖是每個pod的IO情況,看Transmit,從該點看,總網絡傳輸為87.9MB/s
下圖展示kubernetes叢集的最高網絡傳輸為88.6MB/s
從這裡可以看出網絡傳入的速率和最後寫入的速率基本相等,可以推斷目前的瓶頸應該就是網絡傳輸了。這裡我後來咨詢了網絡管理者,199網段和149網段之間的帶寬是1000Mbps,換算為MB為125MB/s,考慮網絡網卡與交換機之間的損耗,與計算的90MB/s相差不多,可以佐證該點。
那麼我們基本确定了上面測試的情況為network網絡瓶頸。我們再來看看hdfs datanode的IO問題。找到幾台data node,使用iostat去檢視io的狀态,如下圖。檢視了某幾台IO的情況,IO的寫入速率最高在10MB/s左右,cpu的iowait百分比最高也隻有10,可見IO寫入速率也不高,可以排除IO瓶頸。
[[email protected] ~]# iostat -m 30 100
Linux 3.10.0-1062.18.1.el7.x86_64 (seadoop-test128.wux.chin.seagate.com) 05/26/2021 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.88 0.00 1.92 1.03 0.00 84.17
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sda 42.22 0.38 1.40 13488093 49805393
sdb 11.05 0.28 0.04 10065196 1532877
sdc 10.39 0.31 0.03 11129480 1019587
avg-cpu: %user %nice %system %iowait %steal %idle
10.36 0.00 2.98 0.23 0.00 86.43
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sda 121.53 0.01 6.67 0 200
sdb 114.40 0.00 7.38 0 221
sdc 122.17 0.00 8.28 0 248
avg-cpu: %user %nice %system %iowait %steal %idle
14.40 0.00 3.78 0.11 0.00 81.70
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sda 128.20 0.01 9.81 0 294
sdb 102.80 0.01 5.64 0 169
sdc 61.30 0.00 3.17 0 95
avg-cpu: %user %nice %system %iowait %steal %idle
10.91 0.00 3.22 0.10 0.00 85.76
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sda 116.53 0.01 6.59 0 197
sdb 65.33 0.00 4.71 0 141
sdc 120.87 0.01 7.08 0 212
[[email protected] ~]# iostat -m 30 100
Linux 3.10.0-1062.18.1.el7.x86_64 (seadoop-test133.wux.chin.seagate.com) 05/26/2021 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
15.31 0.00 1.00 1.24 0.00 82.45
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 14.00 0.26 0.11 9264726 3909152
sdc 13.37 0.24 0.10 8449701 3523896
sda 22.88 0.25 0.25 8766283 8770946
avg-cpu: %user %nice %system %iowait %steal %idle
12.67 0.00 4.36 10.79 0.00 72.17
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 1936.80 7.72 0.18 231 5
sdc 1637.47 6.37 0.61 191 18
sda 74.47 1.86 0.25 55 7
avg-cpu: %user %nice %system %iowait %steal %idle
15.45 0.00 3.95 10.48 0.00 70.13
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 796.67 2.96 5.31 88 159
sdc 126.70 0.21 5.88 6 176
sda 690.20 2.80 3.76 84 112
avg-cpu: %user %nice %system %iowait %steal %idle
17.57 0.00 4.07 9.24 0.00 69.12
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 85.27 0.13 5.55 3 166
sdc 84.33 0.06 3.95 1 118
sda 1817.13 7.18 3.86 215 115
avg-cpu: %user %nice %system %iowait %steal %idle
13.07 0.00 3.82 10.74 0.00 72.37
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 93.20 0.09 3.45 2 103
sdc 93.70 0.02 6.70 0 200
sda 1804.10 7.12 8.89 213 266
avg-cpu: %user %nice %system %iowait %steal %idle
14.82 0.00 4.67 9.00 0.00 71.50
Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
sdb 100.53 0.32 7.32 9 219
sdc 123.20 0.02 8.34 0 250
sda 1860.23 7.33 6.01 220 180
綜上可以得出結論,不管調cpu,memrory,或是并發數,table寫入線程,還是pod number的數量,總時間基本都在18分鐘,且最終的瓶頸分析為網絡。
五.本地測試
因為在hdfs測試并發數和table線程數設定過大,推斷出網絡的瓶頸,沒有調試出最佳的配置,于是決定寫本地資料,找出最佳的配置(主要cpu,memrory和pod number)。
修改pod的deployment,挂載本地磁盤,使用hostpath挂載pod内部目錄/tmp/parse到本機目錄/tmp/parse
調整參數,得出如下結果,分兩個測試8G和12G,這裡table write thread之是以設定為16,是因為node節點的cpu核數的16,也就是cpu同時可以有16個線程運作,如果單個節點隻配置設定到一個request,那麼能確定最大的線程數在寫。
Case 1 | |||||||||||||||||
memory | 8 | ||||||||||||||||
pod num | 6 | 8 | 12 | 16 | 20 | ||||||||||||
cpu | 2 | 2 | 2 | 2 | 4 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
concurrent thread | 16 | 24 | 24 | 32 | 24 | 24 | 32 | 48 | 24 | 32 | 48 | 65 | 24 | 32 | 48 | 65 | 80 |
table write thread | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 |
Time(min) | 15.1 | 14.1 | 10.8 | 9.8 | 9.7 | 7.6 | 7.7 | 7.5 | 7.9 | 6.6 | 6 | 6.7 | 6.8 | 6.7 | 6.7 | 5.5 | 6.9 |
Case2 | |||||||||||||||||
memory | 12 | ||||||||||||||||
pod num | 6 | 8 | 12 | 16 | 20 | ||||||||||||
cpu | 2 | 2 | 2 | 2 | 4 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
concurrent thread | 16 | 24 | 24 | 32 | 24 | 24 | 32 | 48 | 24 | 32 | 48 | 65 | 24 | 32 | 48 | 65 | 80 |
table write thread | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 |
Time(min) | 14.3 | 10.7 | 8.9 | 10.2 | 7.9 | 8.7 | 6.4 | 7.6 | 7.6 | 5.9 | 5.8 | 5.6 | 6.2 | 6.9 | 5.3 | 6.3 | 6.7 |
分析兩次測試memrory升高的影響一般,基本沒有影響,cpu也沒有太大的影響,但是pod數量的變化對最終時間是有影響的,而并發數的變化對結果影響也不大。這裡再次分析了相關參數的變化,看出在增加pod數量到16後,與20pod的時間變化不大。
檢視傳回的日志,可以看出往本地寫,沒有網絡傳輸,table寫入資料的時間也不高了。
其次檢視了pod的restart情況,pod的restart變多了,原因分析本地寫入速度變快,每個pod處理request的頻次變高,pod記憶體加速上升了,是以pod的restart情況也會變多。
再分析了幾個參數對結果的影響因素,大概判斷這次測試的瓶頸可能在IO,因為調試了記憶體,pod number等參數,并沒有大的改善,而且pod number在16個之後,變化不大,于是去主要檢視了node IO情況。截取了2個node節點的監控名額,可以看出IO的速率上限在40MB/s。考慮到目前有8個節點在工作,兩者相乘整個叢集的IO寫入為320MB/s。 而從資料總量(95G)和寫入時間(最低5.3min)計算,得出305MB/s,符合叢集總體寫入速率。
為了測試節點的IO上限,還特别做了單節點IO測試,如下圖看出單個節點并發數在3以後,就達到了IO上限40MB/s,後面再增加并發,cpu的IO wait時間百分比線性上升,可以得出本次此時的瓶頸确實是在IO。
t011測試機控制台iostat 30s列印一次IO
分析為何pod到16的時候,總時間基本沒有變化。原因分析如下:
從io的測試來看,一個節點并發數在2-3的時候IO已經到上限了,目前節點node一共8個,16個pod配置設定到8個node的時候正好每個節點2個pod,每個節點2個pod同時寫資料時就基本能達到節點IO的上限了,是以在增加pod時,到20個,那麼某幾個節點會有3個pod,而每個節點2個pod就達到IO上限,是以再增加pod數量并不會對IO有變化,是以得出pod數量在16個之後,因為節點IO的問題,總時間不會有大的變化。是以對于本case IO瓶頸的情況,16個pod,cpu設定為2,memrory設定為8G是最佳配置。
六.CPU測試
為了測試cpu的使用情況,做了下面的測試,不寫資料時,隻解碼。
12pod,2core,12G
從上圖可以看出,在不寫資料,隻解碼的情況下,cpu的使用率是很高的,這與上面的測試形成了對比,分析在IO和網絡是瓶頸的時候,cpu的使用率就不明顯了。
七.pod負載問題
在測試中,pod負載量也是一個參考因素,因為pod負載不均衡,那麼就會導緻測試時間的差異。我在此測試中,就出現了該問題,kubernetes自帶的負載均衡政策隻有兩種,一種是輪詢,一種是sessionAffinity:ClientIP綁定IP,就是同一個IP的request指定到固定的pod,當然我測試的時候用的是輪詢,但是在輪詢的情況,pod的負載情況也不是很均衡,如下圖。這種情況建議用外部的負載均衡器
總結:
1.本次測試了兩種方式,分别是往hdfs寫資料和往本地寫資料
2.往hdfs寫網絡是瓶頸,往本地寫io是瓶頸
3.本次測試的場景,雖然結果來看主要是IO密集型任務,但是在撇除寫資料的功能,解碼這一塊也是會耗費大量的cpu操作(做過測試,不寫資料,隻解碼,速度很快,但是cpu很高),隻是在IO時間的影響下,cpu的操作顯得不明顯了。
4.在選擇最佳配置的時候,先考慮網絡和IO的瓶頸。如果是網絡瓶頸,降低并發數,找到網絡上限最低的并發數,在這個并發數的情況下,再去調cpu,memory和pod number的配置情況。如果是IO瓶頸,先測試單台node IO最大時的最低并發數,在此基礎上設定pod數量為node數量的該并發數的倍數。再去調cpu和memory。
5.性能測試影響的因素很多,不是幾個測試就能找出最優配置,環境的差異,軟體層面的差異,你的測試方法都會很大程度上影響最終結果,這些隻能在工作中慢慢摸索,積累,在後面的工作中給你幫助。