在初次使用HDFS客户端下载文件时,很容易写出下面的代码
FileSystem fileSystem = FileSystem.get(uri, conf, "hadoopuser");
// 使用fileSystem做操作
try (BufferedReader br =
new BufferedReader(
new InputStreamReader(fileSystem.open(new Path("/data.txt"))))) {
// 读取文件
}
看起来还使用了try-with-resource
, 以为最后fileSystem会被关闭,实际上 fileSystem.open()
返回了 FSDataInputStream
后就不管了。
fileSystem
本身还是存在的,并没有关闭。
如果fileSystem
没有关闭会出现什么情况?
稍微跟一下代码就会知道,FileSystem
里面有一个静态的缓存Map,
/** FileSystem cache */
static final Cache CACHE = new Cache();
如果采用默认配置:fs.hadoopuser.impl.disable.cache
是false
,也就是开启缓存,每次会从CACHE
里获取。
public static FileSystem get(URI uri, Configuration conf) throws IOException {
...
String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
if (conf.getBoolean(disableCacheName, false)) {
return createFileSystem(uri, conf);
}
return CACHE.get(uri, conf);
}
而这个缓存里的key构造如下:
FileSystem get(URI uri, Configuration conf) throws IOException{
Key key = new Key(uri, conf);
return getInternal(uri, conf, key);
}
static class Key {
final String scheme;
final String authority;
final UserGroupInformation ugi;
final long unique; // an artificial way to make a key unique
Key(URI uri, Configuration conf, long unique) throws IOException {
scheme = uri.getScheme()==null ?
"" : StringUtils.toLowerCase(uri.getScheme());
authority = uri.getAuthority()==null ?
"" : StringUtils.toLowerCase(uri.getAuthority());
this.unique = unique;
this.ugi = UserGroupInformation.getCurrentUser();
}
@Override
public int hashCode() {
return (scheme + authority).hashCode() + ugi.hashCode() + (int)unique;
}
我们都知道map是根据hashcode来判断key是否相同的,来前面的scheme, authority,unique
都是一样的,而 this.ugi = UserGroupInformation.getCurrentUser();
跟到最后发现,这个 ugi里每次都new了一个新对象,所以,缓存CACHE会无限增加,最终OOM。
为什么会出现这种情况?
其实,如果你只是在一个方法里完成上述过程,即便 FileSystem没有被关闭,也会在方法结束后被GC给回收了。
问题就在于 FileSystem.get(uri, conf, "hadoopuser")
比较耗时,所以,程序里一般会复用一个 FileSystem
, 我们以为每次获取的是一个实例,谁知道是不同的,因为是静态资源,所以得不到回收,这就导致实例在内存堆积。
如何解决?
其实解决方法也很简单:
- 每次使用后就关闭
- 修改代码能够利用缓存
方法1 就不说了。
方法2,肯定不是让我们去修改 HDFS Client的代码,而是修改应用代码:
private FileSystem getFileSystem() throws IOException, URISyntaxException {
Configuration conf = HadoopUtils.getHadoopConfiguration(hdfsConfig.getConfigDir());
String hdfsPath = hdfsConfig.getPath();
System.setProperty("HADOOP_USER_NAME", hadoopUser);
return FileSystem.get(new URI(hdfsPath), conf);
}
重点就在 System.setProperty("HADOOP_USER_NAME", hadoopUser);
, 之前我们将用户传进去,现在我们通过环境变量设置 HADOOP_USER_NAME
的方式传用户,问题就解决了。
为什么?
实际上,我们的应用一般不会和Hadoop集群部署在一起,所以一般是没有这个环境变量的,而这个环境变量是Hadoop根据Java安全策略构造 ugi
的选择,有了这个用户,每次获取到的 ugi
就是一样的了。