美文网首页
多线程环境下HDFS的FileSystem使用踩坑

多线程环境下HDFS的FileSystem使用踩坑

作者: 安中古天乐 | 来源:发表于2020-04-02 20:43 被阅读0次

最近用户反映项目现场某些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的时候,可能返回的是同一个对象,也就是说,该方法线程不安全。

那问题解决方法就简单了:

  1. 禁用缓存
Configuration conf = new Configuration();
conf.set("fs.hdfs.impl.disable.cache","true");
  1. 使用newInstance(conf)方法
FileSystem fileSystem = FileSystem.newInstance(conf);

该方法每次均会返回1个新连接,所以使用该方法,每次使用完均必须记得关闭。

同时,方法传入的conf里必须指定"fs.defaultFS"属性,否则默认走本地模式,即"file:///"。

conf.set("fs.defaultFS", fileUrl);

按照第2种解决方案把程序里所有FileSystem的创建点修改掉,打包重新上传到服务器,持续观察了几天,发现连接数稳定在10以内,未持续增加。

okay,问题解决,美滋滋!!!

相关文章

网友评论

      本文标题:多线程环境下HDFS的FileSystem使用踩坑

      本文链接:https://www.haomeiwen.com/subject/ehvfphtx.html