0


HDFS FileSystem 导致的内存泄露

** 感谢点赞和关注 ,每天进步一点点!加油!**

一、问题描述


ftp程序读取windows本地文件写入HDFS,5天左右程序 重启一次,怀疑是为OOM挂掉,马上想着就分析 GC日志了。

  1. ### 打印gc日志
  2. /usr/java/jdk1.8.0_162/bin/java \
  3. -Xmx1024m -Xms512m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 \
  4. -XX:-ResizePLAB -verbose:gc -XX:-PrintGCCause -XX:+PrintAdaptiveSizePolicy \
  5. -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/hadoop/datadir/windeploy/log/ETL/DS/gc.log-`date +'%Y%m%d%H%M'` \
  6. -classpath $jarPath com.winnerinf.dataplat.ftp.FtpUtilDS

程序分配内存 1024M ,从gc日志可以看出,old区域的占用大小一直从100M上升到了1G,后面频繁的fullGc,但是释放的空间并不多,最后程序由于内存空间不足导致OOM。

我们使用 jmap 命令将该进程的 JVM 快照导出来进行分析:

  1. jmap -dump:live,format=b,file=dump.hprof PID

这个命令会在当前目录下生成一个dump.hrpof文件,这里是二进制的格式,你不能直接打开看的,其把这一时刻JVM堆内存里所有对象的快照放到文件里去了,供你后续去分析。

Memory Analyzer Mat下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

https://kangll.blog.csdn.net/article/details/130222759?spm=1001.2014.3001.5502https://kangll.blog.csdn.net/article/details/130222759?spm=1001.2014.3001.5502**Mat 工具给出了两个问题:**

问题一

"org.apache.hadoop.fs.FileSystem$Cache"”的一个实例文件系统被"sun.misc.Launcher$AppClassLoader @ 0xc04e9290"加载。占用208,987,664字节(20.36%)。内存在“"org.apache.hadoop.fs.FileSystem$Cache"”的一个实例中累积。文件系统被程序"sun.misc.Launcher$AppClassLoader @ 0xc04e9290"加载。

问题二

7,670个“org.apache.hadoop.conf.Configuration”实例。配置”,由“sun.misc”加载。"sun.misc.Launcher$AppClassLoader @ 0xc04e9290" 占用813,527,552字节(79.25%)。这些实例是从"java.util.HashMap$Node[]",的一个实例中引用的。由"<系统类加载器>"加载。


二、问题定位和源码分析


问题的源头在于 org.apache.hadoop.fs.FileSystem 这个类,程序运行了5天, conf 类就产生了几千个实例。这些实例虽然占用的大小不大,但是每产生一个FileSystem实例时,它都会维护一个Properties对象(Hashtable的子类),用来存储hadoop的那些配置信息。hadoop的配置有几百个很正常,因此一个FileSystem实例就会对应上百个的Hashtable$Entity实例,就会占用大量内存。

为什么会有如此多的FileSystem实例呢?

以下是我们获取FIleSystem的方式:

  1. Configuration conf = new Configuration();
  2. conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
  3. conf.set("fs.file.impl", "org.apache.hadoop.fs.LocalFileSystem");
  4. FileSystem fileSystem = FileSystem.get(conf);

FileSystem.get底层是有使用缓存的,因此我们在每次使用完并没有关闭fileSystem,只是httpfs服务关闭时才去关闭FileSystem。

  1. /****************************************************************
  2. *
  3. * </ol>
  4. *****************************************************************/
  5. @SuppressWarnings("DeprecatedIsStillUsed")
  6. @InterfaceAudience.Public
  7. @InterfaceStability.Stable
  8. public abstract class FileSystem extends Configured
  9. implements Closeable, DelegationTokenIssuer {
  10. public static final String FS_DEFAULT_NAME_KEY =
  11. CommonConfigurationKeys.FS_DEFAULT_NAME_KEY;
  12. public static final String DEFAULT_FS =
  13. CommonConfigurationKeys.FS_DEFAULT_NAME_DEFAULT;
  14. /**
  15. * 获取这个URI的方案和权限的文件系统
  16. * 如果配置有属性{@code "fs.$SCHEME.impl.disable。缓存"}设置为true,
  17. * 将创建一个新实例,并使用提供的URI和进行初始化配置,然后返回而不被缓存
  18. *
  19. * 如果有一个缓存的FS实例匹配相同的URI,它将被退回
  20. * 否则:将创建一个新的FS实例,初始化配置和URI,缓存并返回给调用者。
  21. */
  22. public static FileSystem get(URI uri, Configuration conf) throws IOException {
  23. String scheme = uri.getScheme();
  24. String authority = uri.getAuthority();
  25. if (scheme == null && authority == null) { // use default FS
  26. return get(conf);
  27. }
  28. if (scheme != null && authority == null) { // no authority
  29. URI defaultUri = getDefaultUri(conf);
  30. if (scheme.equals(defaultUri.getScheme()) // if scheme matches default
  31. && defaultUri.getAuthority() != null) { // & default has authority
  32. return get(defaultUri, conf); // return default
  33. }
  34. }
  35. // //如果cache被关闭了,每次都会创建一个新的FileSystem
  36. String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
  37. if (conf.getBoolean(disableCacheName, false)) {
  38. LOGGER.debug("Bypassing cache to create filesystem {}", uri);
  39. return createFileSystem(uri, conf);
  40. }
  41. return CACHE.get(uri, conf);
  42. }
  43. /** Caching FileSystem objects. */
  44. static class Cache {
  45. // ....
  46. FileSystem get(URI uri, Configuration conf) throws IOException{
  47. // key 的问题 ,详细我们看下 Key
  48. Key key = new Key(uri, conf);
  49. return getInternal(uri, conf, key);
  50. }
  51. }
  52. /**
  53. *如果键映射到实例,则获取FS实例,如果没有找到FS,则创建并初始化FS。
  54. * 如果这是映射中的第一个条目,并且JVM没有关闭,那么它会注册一个关闭钩子来关闭文件系统,并将这个FS添加到{@code toAutoClose}集合中,
  55. * 如果{@code " FS .automatic。Close "}在配置中设置(默认为true)。
  56. */
  57. private FileSystem getInternal(URI uri, Configuration conf, Key key)
  58. throws IOException{
  59. FileSystem fs;
  60. synchronized (this) {
  61. fs = map.get(key);
  62. }
  63. if (fs != null) {
  64. return fs;
  65. }
  66. fs = createFileSystem(uri, conf);
  67. final long timeout = conf.getTimeDuration(SERVICE_SHUTDOWN_TIMEOUT,
  68. SERVICE_SHUTDOWN_TIMEOUT_DEFAULT,
  69. ShutdownHookManager.TIME_UNIT_DEFAULT);
  70. synchronized (this) { // refetch the lock again
  71. FileSystem oldfs = map.get(key);
  72. if (oldfs != null) { // a file system is created while lock is releasing
  73. fs.close(); // close the new file system
  74. return oldfs; // return the old file system
  75. }
  76. // now insert the new file system into the map
  77. if (map.isEmpty()
  78. && !ShutdownHookManager.get().isShutdownInProgress()) {
  79. ShutdownHookManager.get().addShutdownHook(clientFinalizer,
  80. SHUTDOWN_HOOK_PRIORITY, timeout,
  81. ShutdownHookManager.TIME_UNIT_DEFAULT);
  82. }
  83. fs.key = key;
  84. map.put(key, fs);
  85. if (conf.getBoolean(
  86. FS_AUTOMATIC_CLOSE_KEY, FS_AUTOMATIC_CLOSE_DEFAULT)) {
  87. toAutoClose.add(key);
  88. }
  89. return fs;
  90. }
  91. }
  92. }

我们服务的fs.%s.impl.disable.cache并没有开启,因此肯定有使用cache。所以问题很可能是Cache的key判断有问题。

  1. /** FileSystem.Cache.Key */
  2. static class Key {
  3. final String scheme;
  4. final String authority;
  5. final UserGroupInformation ugi;
  6. final long unique; // an artificial way to make a key unique
  7. Key(URI uri, Configuration conf) throws IOException {
  8. this(uri, conf, 0);
  9. }
  10. Key(URI uri, Configuration conf, long unique) throws IOException {
  11. scheme = uri.getScheme()==null ?
  12. "" : StringUtils.toLowerCase(uri.getScheme());
  13. authority = uri.getAuthority()==null ?
  14. "" : StringUtils.toLowerCase(uri.getAuthority());
  15. this.unique = unique;
  16. this.ugi = UserGroupInformation.getCurrentUser();
  17. }
  18. @Override
  19. public int hashCode() {
  20. return (scheme + authority).hashCode() + ugi.hashCode() + (int)unique;
  21. }
  22. static boolean isEqual(Object a, Object b) {
  23. return a == b || (a != null && a.equals(b));
  24. }
  25. @Override
  26. public boolean equals(Object obj) {
  27. if (obj == this) {
  28. return true;
  29. }
  30. if (obj instanceof Key) {
  31. Key that = (Key)obj;
  32. return isEqual(this.scheme, that.scheme)
  33. && isEqual(this.authority, that.authority)
  34. && isEqual(this.ugi, that.ugi)
  35. && (this.unique == that.unique);
  36. }
  37. return false;
  38. }
  39. @Override
  40. public String toString() {
  41. return "("+ugi.toString() + ")@" + scheme + "://" + authority;
  42. }
  43. }

我们使用 Mat工具 看下对象对比 scheme ,authority,ugi 的 哈希值

查看外部应用对象

  • java.util.HashMap$Node @ 0xc6756200
  • java.util.HashMap$Node @ 0xca5f51f8

我们集群开启了kerberos配置,可以看到 UGI对象的 hashCode不一样。

我们来看下 UGI类

  1. public static FileSystem get(final URI uri, final Configuration conf,
  2. final String user) throws IOException, InterruptedException {
  3. String ticketCachePath =
  4. conf.get(CommonConfigurationKeys.KERBEROS_TICKET_CACHE_PATH);
  5. //这里每次获取的ugi的hashcode都不一样
  6. UserGroupInformation ugi =
  7. UserGroupInformation.getBestUGI(ticketCachePath, user);
  8. return ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {
  9. @Override
  10. public FileSystem run() throws IOException {
  11. return get(uri, conf);
  12. }
  13. });
  14. }
  15. // UserGroupInformation.getBestUGI
  16. public static UserGroupInformation getBestUGI(
  17. String ticketCachePath, String user) throws IOException {
  18. if (ticketCachePath != null) {
  19. return getUGIFromTicketCache(ticketCachePath, user);
  20. } else if (user == null) {
  21. return getCurrentUser();
  22. } else {
  23. //最终走到这里
  24. return createRemoteUser(user);
  25. }
  26. }
  27. // UserGroupInformation.createRemoteUser
  28. public static UserGroupInformation createRemoteUser(String user, AuthMethod authMethod)
  29. {
  30. if (user == null || user.isEmpty()) {
  31. throw new IllegalArgumentException("Null user");
  32. }
  33. Subject subject = new Subject();
  34. subject.getPrincipals().add(new User(user));
  35. UserGroupInformation result = new UserGroupInformation(subject);
  36. result.setAuthenticationMethod(authMethod);
  37. return result;
  38. }

虽然使用cache,但是由于Key的判断问题,所以基本每次请求都会生成一个新的实例,就会出现内存泄露的问题。

ugi对象的不同是由于我们获取FileSystem时指定了用户有关 ,我们开启了 Kereros 而且指定了用户,根据下图的 UGI 对象可以看出每次请求都会生成一个新的实例,就会出现内存泄露的问题。

最后我们再看下程序的DEBUG日志:

  1. 2022-12-22 09:49:35 INFO [main] (HDFS.java:384) - FileSystem is
  2. 2022-12-22 09:49:35 DEBUG [IPC Parameter Sending Thread #0] (Client.java:1122) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM sending #9 org.apache.hadoop.hdfs.protocol.ClientProtocol.getFileInfo
  3. 2022-12-22 09:49:35 DEBUG [IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM] (Client.java:1176) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM got value #9
  4. 2022-12-22 09:49:35 DEBUG [main] (ProtobufRpcEngine.java:249) - Call: getFileInfo took 1ms
  5. 2022-12-22 09:49:35 DEBUG [main] (UserGroupInformation.java:254) - hadoop login
  6. 2022-12-22 09:49:35 DEBUG [main] (UserGroupInformation.java:187) - hadoop login commit
  7. 2022-12-22 09:49:35 DEBUG [main] (UserGroupInformation.java:199) - using kerberos user:winner_spark@WINNER.COM
  8. 2022-12-22 09:49:35 DEBUG [main] (UserGroupInformation.java:221) - Using user: "winner_spark@WINNER.COM" with name winner_spark@WINNER.COM
  9. 2022-12-22 09:49:35 DEBUG [main] (UserGroupInformation.java:235) - User entry: "winner_spark@WINNER.COM"
  10. 2022-12-22 09:49:35 INFO [main] (UserGroupInformation.java:1009) - Login successful for user winner_spark@WINNER.COM using keytab file /etc/security/keytabs/winner_spark.keytab
  11. 2022-12-22 09:49:35 DEBUG [IPC Parameter Sending Thread #0] (Client.java:1122) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM sending #10 org.apache.hadoop.hdfs.protocol.ClientProtocol.getListing
  12. 2022-12-22 09:49:35 DEBUG [IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM] (Client.java:1176) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM got value #10
  13. 2022-12-22 09:49:35 DEBUG [main] (ProtobufRpcEngine.java:249) - Call: getListing took 15ms
  14. 2022-12-22 09:49:35 DEBUG [IPC Parameter Sending Thread #0] (Client.java:1122) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM sending #11 org.apache.hadoop.hdfs.protocol.ClientProtocol.getListing
  15. 2022-12-22 09:49:35 DEBUG [IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM] (Client.java:1176) - IPC Client (1861781750) connection to hdp103/192.168.2.152:8020 from winner_spark@WINNER.COM got value #11
  16. 2022-12-22 09:49:35 DEBUG [main] (ProtobufRpcEngine.java:249) - Call: getListing took 10ms
  17. 2022-12-22 09:49:35 INFO [main] (HDFS.java:279) - Get a list of files and sort them by access time!
  18. 2022-12-22 09:49:35 INFO [main] (FtpUtilDS.java:192) - ----------- not exist rename failure file ---------------

程序DEBUG 日志 也显示每次IPC 遍历HDFS的文件的时候kerberos 还会进行一次登录。虽然fileSystem使用cache,但是由于Key的判断问题,所以基本每次请求都会生成一个新的实例,就会出现内存泄露的问题。

可以得出如果指定了用户,每次都会构造一个新的Subject,因此计算出来的UserGroupInformation的hashcode不一样。这样也最终导致FileSystem的Cache不生效。

** 基于以上的分析,我将过去的旧代码做稍微修改:**

  1. Configuration conf = null;
  2. FileSystem fileSystem = null;
  3. ....
  4. finally {
  5. if (fileSystem != null) {
  6. fileSystem.close();
  7. conf.clear();
  8. }
  9. }

我们再看 程序运行了 两个多月没有重启,查看 堆内存发现 在新生代大部分的对象被回收,而且老年代占用内存持续维持在 10% 以下,至此问题解决。

感谢点赞和关注!

标签: jvm FileSystem MAT

本文转载自: https://blog.csdn.net/qq_35995514/article/details/130273664
版权归原作者 开着拖拉机回家 所有, 如有侵权,请联系我们删除。

“HDFS FileSystem 导致的内存泄露”的评论:

还没有评论