最近在学习JVM内存调优,顺便对于Namenode的内存分析了一把,发现还是有很多有意思
但是危险
的地方。
1 结论
1.1 Namenode内存使用和趋势
截止2016年12月22号14点,集群的文件和文件夹总数量为
293444427
,block数量为348566581
,堆内存总大小为148GB
,Old区大小为130GB
,堆内存使用为110GB~126GB
。目前Namenode常驻内存(详细定义请参考
2.1
章节)使用为110GB
,Old区使用率84%
,即将突破90%
,需要采取扩大内存(降低文件数量)等措施来规避CMS GC降级为压缩式GC带来的漫长STW
风险。依据我们的集群监控系统数据,从12月1号到22号,集群文件数增长了
293444427 - 275847816=17596611
,大约1千7百万,常驻内存增长了110GB - 103GB = 7GB
。
按照这个速度,一个月
内堆内存常驻使用将会突破90%
警戒线。如果堆内存加大到
180GB
,全部给Old区的话增加30GB可用内存,按照估算(估算方法参考2.3
章节)可以容纳的文件数在362804019
左右,还可以增长的文件数需要控制在69359591
以内,大约7千万文件量。
1.2 Namenode内存配置修改建议
1.2.1 提升堆内存大小,降低Old区使用比例,规避压缩式GC的STW风险。
从2016年8月份至今,老年代的最大使用曾经达到122.25g(94%)
,目前使用110g(84%)
,平均使用达到116.1g(89%)
。
堆内存增加到180g后能容纳的文件数在362804019
左右,增长的文件数需要控制在69359591
以内,大约7千万文件量。
目前Namenode常驻内存内存使用为110GB
,Old区使用率84%
,按照这个月的增长速度估算,一个月内堆内存使用将会突破90%
警戒线。
依据监控系统数据,从12月1号到22号,集群文件数从275847816
增长到293444427
大约增长了1千7百万
,
堆内存常驻内存增长了从103GB
增长到110GB
,增长了7GB
。
详细分析请参考2. Name内存利用分析
章节。
修改:
1.2.2 增加ParNew和CMS GC使用的线程数量,目前是10,可以提升到18(24核下的默认值)。
提升ParNew和CMS的GC效率,降低ParNew STW时间,
缓解在临时对象数量激增时发生ParNew GC频繁导致服务延迟增加,吞吐量降低问题。
详细分析请参考3. Namenode启动参数分析
章节。
修改或者去掉以使用默认值:
1.2.3 设置当Old区域使用达到80%
时候,提前启动CMS GC。降低压缩式GC的STW风险。
目前GC配置缺少UseCMSInitiatingOccupancyOnly
这项,
所以CMS GC启动是根据JVM自动确定,时机不确定而且有缺陷(来源于社区)。
目前我们的Namenode Old区常驻内存使用率高,所以不适合设置比较低的值,常驻内存使用率高于这个值会造成频繁无效的CMS GC。
而如果设置过高的话,会造成Full GC风险升高。
详细分析请参考3. Namenode启动参数分析
章节。
修改和增加:
1.2.4 设置Java8的永生代初始值MetaspaceSize为较大的值512m,避免MetaSpace用满需要增长
而引发的Full GC。
参考文献[8],Java8采用MetaSpace替代PermGen,MetaSpace对于64bit Server模式的JVM默认初始大小21m
,比较小。
另外,Java8的永生代默认没有上限,在用户代码不规范时候有可能发生无限增长,但是对于Namnode这种稳定运行的程序没有太大问题。
增加:
1.2.5 打印更多的GC日志信息:JVM启动参数,晋升分布,停顿时间等。
JVM启动参数:可以查看一些隐藏和最终被设置的参数。
晋升分布作为调整-XX:MaxTenuringThreshold
参数和GC Young区域调优的重要参考。
停顿时间作为衡量JVM用户程序执行吞吐率的重要参考。
增加:
1.3 其他建议
- 控制集群的目录树/文件数/Block数量,采用合并小文件等措施,降低Namenode内存压力。
- 调整默认blockSize为256m,降低block数量。缺点是只能控制增量数据,修改存量数据代价较大。
- 调研G1内存回收策略。
2 Namenode内存利用分析
2.1 Namenode常驻内存使用和估算公式
定义
Namenode常驻内存使用
是位于Old区,存活稳定,不可GC的内存使用部分。这部分内存使用主要随着目录/文件/Block数量的变化而变化。
根据文献[3][4]和代码阅读,Namenode常驻内存数据结构
按照数据量主要包含以下两部分(节点和网络拓扑信息/LeaseManager/SnapShotManager/CacheManager等部分由于数据量较小忽略):
目录和文件树 Namespace
a) Directory数据结构总大小:
SumDir = (24+96+44+48) * CountDir + 8 * (CountDir + CountFile)
b) File数据结构总大小:
SumFile = (24+96+48) * CountFile + 8 * CountBlock
合并上述 a) 和 b) 两部分,Namespace总大小: SumNamespace = SumDir + SumFile = 220 * CountDir + 176 * CountFile + 8 * CountBlock
- 文件与块的映射 BlockMap
BlockMap数据结构总大小: SumBlockMap = 16 + 24 + JVMMemoryNN * 0.02 + (40 + 128) * CountBlock
- 计算NN内存使用
合并上述 1 和 2 两部分,得到NN内存使用总大小估算公式:MemoryUsedNN = SumNamespace + SumBlockMap = 220 * CountDir + 176 * CountFile + 176 * CountBlock + JVMMemoryNN * 0.02
假设文件数和目录数为1:1,NN内存使用总大小可以简化为:MemoryUsedNN = SumNamespace + SumBlockMap = 198 * CountDirAndFile + 176 * CountBlock + JVMMemoryNN * 0.02
如果文件数相对目录数量数量较多,上述估算结果会偏小一些
2.2 公司集群Namenode内存使用估算
截止2016年12月22号14点,公司集群的文件和文件夹总数量为293444427
,block数量为348566581
,堆内存总大小为148GB
,Old区大小为130GB
,堆内存使用为110GB~126GB
。
Old区常驻使用率在84%
(110/130),已经比较严重。
按照上述公式,得NN内存常驻使用总大小为:MemoryUsedNN = 198 * 293444427(54.1GB) + 176 * 348566581(57.1GB) + 0.02 * 148GB ~= 114.2GB(54.1G+57.1+3.0GB)
估算值与实际在GC日志中看到的ParNew GC和CMS GC后的内存使用110GB
(主要为常驻内存使用)相比,误差3.5%
,估计较为准确,误差应该主要由于文件和目录比例大于1:1导致。
2.3 内存分析结论
目前Namenode常驻内存内存使用为
110GB
,Old区使用率84%
,即将突破90%
,需要采取扩大内存(降低文件数量)等措施来规避STW风险。依据监控系统数据,从12月1号到22号,集群文件数增长了
293444427 - 275847816=17596611
,大约1千7百万,堆内存常驻内存增长了110GB - 103GB = 7GB
。按照这个速度,
一个月
内堆内存使用将会突破90%
警戒线如果堆内存增大到180GB,Old区域增加30GB,按照上述估算公式和常驻内存使用量85%,
能容纳的文件数在160 * 0.85 / 110 * 293444427 = 362804019
左右,也就是说还可以增长的文件数需要控制在69359591
以内,大约7千万文件量。
2.4 可以采取的措施
增大Namenode堆内存设置,目前是150GB,可以尝试增加到180GB。降低Old区使用比例,规避压缩式GC的STW风险。
增大Old区域的堆内存,缺点是会使得CMS GC的
Remark
时间增长,目前是0.8s
左右,影响不大。
参考文献[4]推荐,再进一步加大堆内存会有JVM内存管理
的额外风险。调整blockSize为256MB,降低block数量。缺点是只能修改增量数据。
3. Namenode启动参数分析
|
|
其中GC相关的关键属性(由于重复配置,GC相关的属性重复了三遍,以最后为准):
-Xms150g -Xmx150g
:堆内存大小最大和最小都是150g-Xmn20g
:新生代大小为20g,等于eden+2*survivor,意味着老年代为150-20=130g。-XX:SurvivorRatio=8
:Eden和Survivor的大小比值为8,意味着两个Survivor区和一个Eden区的比值为2:8,一个Survivor占整个年轻代的1/10-XX:ParallelGCThreads=10
:设置ParNew GC的线程并行数,默认为8 + (Runtime.availableProcessors - 8) * 5/8
,24核机器为18。这里是否需要提升?-XX:MaxTenuringThreshold=15
:设置对象在年轻代的最大年龄,超过这个年龄则会晋升到老年代-XX:+UseParNewGC
:设置新生代使用Parallel New GC-XX:+UseConcMarkSweepGC
:设置老年代使用CMS GC,当此项设置时候自动设置新生代为ParNew GC-XX:CMSInitiatingOccupancyFraction=70
:
老年代第一次占用达到该百分比时候,就会引发CMS的第一次垃圾回收周期。后继CMS GC由HotSpot自动优化计算得到。
如果后继也想指定老年代使用达到百分比,然后就进行CMS GC参数为-XX:+UseCMSInitiatingOccupancyOnly
,建议同时设置。
是否应该同时设置-XX:+UseCMSInitiatingOccupancyOnly?
这个值如果设置的太大,则很可能避免不了Full GC;如果设置的太小,CMS GC会进行的比较频繁。
对于Namenode这样的常驻内存使用主要是稳定的目录文件数信息,而且随着文件数的增长堆内存使用稳定增长的场景,
CMS GC很多情况都是做的无用功,从log里也可以观察CMS并发清除前后的堆内存使用
来验证,现象为CMS并发清除前后的堆内存使用差距很小,表明Old区域可以回收的对象基本诶呦。
目前GC配置缺少UseCMSInitiatingOccupancyOnly
这项,所以CMS GC启动是根据JVM自动确定,时机不确定而且有缺陷(来源于社区)。
目前线上的Namenode常驻内存使用率高,所以不适合设置比较低的值,会造成频繁无效的CMS GC。
如果设置太高的话,会造成Full GC风险升高。
因此可以考虑将这个值设置比较大,例如80~85
。
4. GC log分析
4.1 Minor GC日志解析
根据上述分析,新生代大小为20g,Eden为16g,Survivor大小2g,老年代大小为130g
新生代与CMS配套的GC算法为多线程ParNew或者单线程DefNew。
[ParNew: 16844397K->85085K(18874368K), 0.1314400 secs]
其中,16844397K
表示GC前的新生代占用量,85085K
表示GC后的新生代占用量,GC后Eden和一个Survivor为空,所以85085K
也是另一个Survivor的占用量。括号中的18874368K
是Eden+一个被占用Survivor的总和(18g)。0.1122703 secs
是新生代回收不可达对象的时间。可以看到每次ParNew GC后,基本对象都会被回收或者晋升,只剩下85085K=83M的数据
116885867K->100127390K(155189248K), 0.1318411 secs]
其中,分别是Java堆在垃圾回收前后的大小,和Java堆大小。说明堆使用为116885867K=111.47g,回收大小为100127390K=95.49g,堆大小为155189248K=148g(去掉其中一个Survivor),回收了16g空间,耗时0.13秒。从而可以推算老年代大小为148g-18g=130g,与上述参数一致。老年代使用大小为95.49g-85085K=95.41g,使用率73.4%。
[Times: user=1.29 sys=0.01, real=0.13 secs]
其中,时间=新生代垃圾收集+对象提升到老年代+垃圾清理。user指的是GC消耗的用户态度CPU时间,sys是内核态CPU时间,real指的是从开始到结束的时间计时。real包含了在GC过程中,IO或线程阻塞等待耗时。而user是CPU计算时间,在多核时通常会高于real时间。
4.2 CMS GC日志解析
利用工具分析的结果(请把本项目git clone到本地再打开文件):
GCLog分析
4.3 GC 日志相关参数
详见文献[9]
4.4 GC 日志抽取指标(未完成)
- 吞吐率=(程序运行时间-GC停顿时间)/程序运行时间1
- Old区域常驻内存使用率: 定义在CMS GC的sweep后,old区域的内存使用率
- 对象晋升率:定义为ParNew GC前后,Old区域内存使用差值
- 。。。
5. GC原理
5.1 Minor GC的触发条件
5.2 CMS GC的触发条件
5.3 CMS Full GC的触发条件
主要指的是由于某种原因CMS GS退化为单线程的Old区域压缩GC
(Serial Old GC),极其耗时,一旦发生,服务即不可用。
System.gc()
- 当永生代(Perm Gen)/老年代(Old Gen)/Metaspace(Java 8)使用比例100%时候
- 老生代碎片过大无法分配空间给晋升的对象,引发
concurrent mode failure
错误
6. TODO工作
- GC分析程序,接入监控。
- 调研新的低延迟GC策略G1。
- 如何用指标来度量GC的严重程度。
- 提升服务吞吐量和降低延迟是否可以兼得?