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