天天看点

【性能测试】记一次性能测试

目录

一.测试环境介绍

二.本次测试参数介绍

三.解码程序介绍

四.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.性能测试影响的因素很多,不是几个测试就能找出最优配置,环境的差异,软件层面的差异,你的测试方法都会很大程度上影响最终结果,这些只能在工作中慢慢摸索,积累,在后面的工作中给你帮助。