最近用户反映项目现场某些Spark服务会莫名宕掉,作为组内的救火小王子,急速远程到现场服务器排查相关问题。
打开宕掉服务的日志,发现该服务"遗言"如下:
23:46:02.441 [Thread-3] INFO o.a.spark.storage.DiskBlockManager - Shutdown hook called
23:46:02.441 [dispatcher-event-loop-9] ERROR o.a.s.e.CoarseGrainedExecutorBackend - Executor self-exiting due to : Driver XXX.XXX.XXX.XXX:33224 disassociated! Shutting down.
23:46:02.442 [Thread-3] INFO o.a.spark.storage.DiskBlockManager - Shutdown hook called
很明显,凶手是Driver XXX.XXX.XXX.XXX:33224,执行以下命令:
lsof -i:33224
得到:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 163110 root 320u IPv6 2070085583 0t0 TCP XXX:33224->XXX:50010 (CLOSE_WAIT)
可以看到,33224端口上的进程为163110,其在请求50010端口,接着看163110进程的具体信息:
ps -aux | grep 163110
得到:
root 49129 0.0 0.0 112704 972 pts/5 S+ 19:42 0:00 grep --color=auto 163110
root 163110 118 6.5 43294136 8674528 pts/4 Sl 18:38 75:00 java -jar ./XXX-1.0.0-SNAPSHOT.jar
很明显是XXX-1.0.0-SNAPSHOT.jar这个Java服务对50010端口的请求造成了现场某些Spark服务的停止。
对Hadoop配置比较熟悉的话,50010是dfs.datanode.address的默认配置,查看一下50010端口上有哪些请求。
lsof -i:50010
java 163110 root 320u IPv6 2070085583 0t0 TCP XXX:49163->XXX:50010 (CLOSE_WAIT)
发现基本都是163110进程的连接,且均处于CLOSE_WAIT状态,统计一下个数:
lsof -i:50010 | grep 163110 | wc -l
竟然有8305个之多。
很明显是XXX-1.0.0-SNAPSHOT.jar这个Java服务持有大量50010的连接,耗尽Hadoop的资源,导致其他服务对50010的连接被持续阻塞,直至逼停。
怀疑是XXX-1.0.0-SNAPSHOT.jar里存在连接未close的点,该服务主要实现的是多线程同时消费Kafka多个topic中的数据,然后实时存储到HDFS的相应目录中。
重新启动该服务,使用以下命令不断观察该服务进程对50010的连接数变化:
lsof -i:50010 | grep {服务的PID} | wc -l
发现连接数在半小时时间里就飙升到400多,且不断增加。关掉服务,尝试修复,修复的话,只能深入到该服务的源码里看看了。
经过一番排查,XXX-1.0.0-SNAPSHOT.jar里确实存在某些连接未close的点,修复后重新上传服务器启动。
此次,连接数看起来正常了,但还是呈增加的趋势,每小时增加十几个连接的水平,寻思可能因为该服务是多线程存储服务,资源增加是正常的,再观察一阵吧。
第二天再看,不得了,连接数竟然到了600多,这就很明显不正常了,很明显问题没有完全被修复,只是缓解了其耗尽Hadoop资源的速度。
继续排查代码,反反复复,昏天暗地,但确实是该释放的连接都释放了,简直了。
后来想到,会不是多线程环境下,HDFS的FileSystem使用上有啥坑,看了一下原有写法为:
Path filePath = new Path(fileUrl);
FileSystem fileSystem = filePath.getFileSystem(new Configuration());
点进去getFileSystem方法:
public FileSystem getFileSystem(Configuration conf) throws IOException {
return FileSystem.get(this.toUri(), conf);
}
继续往下跟:
public static FileSystem get(URI uri, Configuration conf) throws IOException {
String scheme = uri.getScheme();
String authority = uri.getAuthority();
if (scheme == null && authority == null) {
return get(conf);
} else {
if (scheme != null && authority == null) {
URI defaultUri = getDefaultUri(conf);
if (scheme.equals(defaultUri.getScheme()) && defaultUri.getAuthority() != null) {
return get(defaultUri, conf);
}
}
String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
return conf.getBoolean(disableCacheName, false) ? createFileSystem(uri, conf) : CACHE.get(uri, conf);
}
}
很明显,FileSystem类中有一个Cache内部类,用于缓存已经被实例化的FileSystem。注意这个跟连接池还是有区别的,Cache中的缓存只是一个map,可以被多个线程拿到。这就会有一个问题,当你多线程同时get FileSystem的时候,可能返回的是同一个对象,也就是说,该方法线程不安全。
那问题解决方法就简单了:
- 禁用缓存
Configuration conf = new Configuration();
conf.set("fs.hdfs.impl.disable.cache","true");
- 使用newInstance(conf)方法
FileSystem fileSystem = FileSystem.newInstance(conf);
该方法每次均会返回1个新连接,所以使用该方法,每次使用完均必须记得关闭。
同时,方法传入的conf里必须指定"fs.defaultFS"属性,否则默认走本地模式,即"file:///"。
conf.set("fs.defaultFS", fileUrl);
按照第2种解决方案把程序里所有FileSystem的创建点修改掉,打包重新上传到服务器,持续观察了几天,发现连接数稳定在10以内,未持续增加。
okay,问题解决,美滋滋!!!
网友评论