记一次 HDFS NameNode GC 调优

没有碰到过 GC 问题的人生对写 Java 的人来说是不完整的。大数据生态圈的框架大都以 JVM 系语言开发(Java Scala 为主),毕竟生态成熟嘛要啥有啥。

HDFS 作为大数据领域的默认分布式文件系统,其运作方式导致了非常容易碰到 GC 问题:

  • 大量的元数据需要保存在内存中,使得很容易就需要几十 G 甚至 100 多 G 的堆

  • 大量且高并发的文件读写操作使得频繁地产生新对象

下面就举两个例子,简单分享下我们做的一些调优。

案例一

有业务同事反馈任务跑的慢,虽然后来确认是其他原因导致的,但在分析过程中,我们从监控观察到 RPC 排队时间和处理时间不是很稳定,有时会出现几秒甚至10多秒的的毛刺,进而注意到 GC。

记一次 HDFS NameNode GC 调优_第1张图片

一分钟一个点,大概每分钟有 2、3秒花在 GC 上。我们用的是经典的 ParNew + CMS 的组合,查看 GC 日志发现大部分都是新生代的 GC,也就意味着有 3% - 5%的时间是 STW 的。这个比例看着不大,但在 NameNode 每秒几 K 甚至几十 K 的事务的压力下,绝对数值和对具体业务的影响还是不能忽视的。

知道了原因,调整就很简单了。从 NameNode 的工作原理分析,大量文件的读写确实会创建很多临时对象,调大新生代就是很自然也很正确的办法。一方面,更大的新生代能减少 minor gc 的次数;另一方面,更多临时对象在新生代回收也减少了晋升到老年代的对象的数量,减少了 CMS GC 的压力。

但还有个不能忽视的问题,NameNode 的元数据会占据非常大的老年代空间,因此新生代也不能调的太大,否则可能触发频繁的 CMS GC 甚至 Full GC。

具体调多大,很显然不会有标准值,只能根据实际数据总量和 TPS 来综合考虑。好在 NameNode 每个元数据的内存占用是确定的,乘以对象数,再留些余量,就能准确计算出老年代需要的内存。剩下的就可以分给新生代了。如果余量不足,也可以考虑适当调大整个堆的大小。

在我们的生产环境下,80+G 的 heap,我们把新生代设置为 12G,到达了可以接受的效果。

记一次 HDFS NameNode GC 调优_第2张图片

除了 GC 时间变为几分之一外,GC 次数也减少了几乎一个数量级,RPC 排队时间的毛刺也减少到百毫秒级别,恶劣情况下 2 秒左右,CMS GC 的次数也减少到 10 多个小时 1 次。

案例二

第一个案例调整过程中,有一天突然收到流量异常的告警,查看 NameNode 监控,发现这样的情况:

记一次 HDFS NameNode GC 调优_第3张图片

大部分监控数据都出现了中断,但 Host CPU Usage 这类机器级别的监控却正常,可以排除监控、机器和网络问题,确定是服务问题。很快发现是发生了一次 Full GC。

640?wx_fmt=png

我们使用的 ParNew + CMS 组合,发生 Full GC 通常有这么几种原因:

  1. 主动触发,比如程序调用 System.gc()

  2. 老年代空间不足,可能是 promotion failed 或者 concurrent mode failure 等

  3. 永久代空间不足

首先排除 1。而观察日志,老年代空间还很充足,也没有发现 concurrent mode failure 和 concurrent mode failure 这样的 log,Full GC 发生前全是 ParNew GC,都没有发生  CMS GC。

那就只能是 3,永久代空间不足,发生了扩容,导致发生 Full GC。

我们查看了默认的永久代大小:

640?wx_fmt=png

默认值是 20.75M,实在有点小,和 log 中的值对比,也确实发生了扩容,然后又再次逼近了扩容后的值。

解决方法也很简单,我们把 -XX:PermSize 调整到 256M,并把 -XX:MaxPermSize 设置成一样大,这样初始值够用,也不会再有扩容的问题。

设置上线后,再也没有出现过 Full GC。

这两个案例都不算难,相信大家都能顺利解决。我想更值得总结的是,怎样能更快地解决。

对 GC 的理解,对 NameNode 工作原理的理解,从诸多现象中快速分析出重点,这三点结合在一起才能保证你能迅速解决掉问题。前两点需要平时系统性地学习,最后一点需要点天赋但也能通过不断训练加强。

你可能感兴趣的:(记一次 HDFS NameNode GC 调优)