Skip to content

Commit a47d1e5

Browse files
committed
Update: 新增文章《发现 CMS GC 有点傻 —— 应用A Full GC 问题排查》
1 parent e877ba4 commit a47d1e5

8 files changed

+204
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
###### [【必读】故障案例征集 & Demo 模板.md](docs/case/【必读】故障案例征集&Demo模板.md)
2828
###### [【案例】Dubbo 稳定性:Nacos 注册中心可用性问题复盘](docs/case/【案例】Dubbo稳定性_Nacos注册中心可用性问题复盘.md)
2929
###### [【案例】记一次线上内存报警排查过程](docs/case/【案例】记一次线上内存报警排查过程.md)
30+
###### [【案例】发现CMS_GC有点傻--应用A_FullGC问题排查](docs/case/【案例】发现CMS_GC有点傻--应用A_FullGC问题排查.md)
3031

3132

3233
### 1. 事前防范
@@ -112,6 +113,8 @@
112113

113114

114115
## 版本迭代
116+
* 2022-11-03
117+
* [【案例】发现CMS_GC有点傻--应用A_FullGC问题排查](docs/case/【案例】发现CMS_GC有点傻--应用A_FullGC问题排查.md)@佐井
115118
* 2022-10-26
116119
* [链路追踪(Tracing)其实很简单——链路成本进阶指南](docs/diagnosis/tracing/链路追踪其实很简单——链路成本进阶指南.md)@涯海
117120
* [链路追踪(Tracing)其实很简单——链路功能进阶指南](docs/diagnosis/tracing/链路追踪其实很简单——链路功能进阶指南.md)@涯海

docs/case/image/CMS_GC有点傻_1.png

410 KB
Loading

docs/case/image/CMS_GC有点傻_2.png

523 KB
Loading

docs/case/image/CMS_GC有点傻_3.png

397 KB
Loading

docs/case/image/CMS_GC有点傻_4.png

439 KB
Loading

docs/case/image/CMS_GC有点傻_5.png

221 KB
Loading

docs/case/image/CMS_GC有点傻_6.png

426 KB
Loading
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# 【案例】发现 CMS GC 有点傻 —— 应用A Full GC 问题排查
2+
3+
> 作者:李成武(佐井)
4+
> 创作日期:2022-11-03
5+
> 专栏地址:[【稳定大于一切】](https://github.com/StabilityMan/StabilityGuide)
6+
7+
## 背景
8+
今年5月底开始,应用A机器陆续开始出现单机 Full GC,经过连续几天的排查也没有找到根因,临时解决办法是把 4c8g 的机器升配到 4c16g,缓解内存压力。
9+
10+
9月份开始,16g 的机器偶尔还是出现单机 Full GC,由于 16g 内存的机器整体性能还可以,一般单机 Full GC 一次后机器就恢复正常,很难再抓到现场。但是单机抖动对业务的影响还是非常大,需要彻底解决。
11+
12+
## 问题排查思路
13+
在5月份的时候,我们那个 4c8g 的机器挺不过 Full GC 压力直接被打死了,我们拿到了不少当时的 dump,但是通过分析也没有找到内存泄漏的点。而且9月份开始的 Full GC 几秒钟后就恢复了,很难抓取现场。所以换个思路:
14+
15+
1. 在 Full GC 发生的前后分析下堆的使用情况,看看内存到底是怎么被填满的,是逐渐填满的还是突然填满的。
16+
2. 怀疑在 Full GC 前有大对象内存分配,在还没来得及执行 CMS GC 的时候把堆撑爆了,触发了 Full GC,要尝试把大对象分配找出来。
17+
18+
## 先看看GC前后内存统计
19+
### 背景知识:PrintFLSStatistics
20+
#### -XX:PrintFLSStatistics=n
21+
22+
PrintFLSStatistics是用来统计内存分配情况和碎片统计的信息的,网上关于 PrintFLSStatistics 介绍比较少,相关细节需要看下源码,我们用的是 JDK8,可以看这个源码:
23+
[shenandoah-jdk8u/compactibleFreeListSpace.cpp at master · openjdk/shenandoah-jdk8u](https://github.com/openjdk/shenandoah-jdk8u/blob/master/hotspot/src/share/vm/gc_implementation/concurrentMarkSweep/compactibleFreeListSpace.cpp)
24+
25+
![image](image/CMS_GC有点傻_1.png)
26+
27+
源码显示当设置 PrintFLSStatistics > 0 的时候,会在 GC 的开始(prologue)和结束(epilogue)打印 FreeList 统计信息,FreeList 是空闲内存的索引,堆内存分配的时候需要通过 FreeList 查询合适的空间。由于 FreeList 里面查找的空闲块大小很难与申请的大小一致,申请后剩余的内存还会还回到 FreeList,这个过程会导致 FreeList 的空间不断别切割成更片段,造成内存不连续而无法使用,内存碎片就是这么产生的。
28+
29+
* 当 PrintFLSStatistics > 0 时,首先打印的是 BinaryTreeDictionary 信息,这个信息就是可用的连续内存。
30+
* 当 PrintFLSStatistics > 1 时,会打印 IndexedFreeLists 信息,这里面包含FreeList所有的碎片信息。
31+
32+
![image](image/CMS_GC有点傻_2.png)
33+
34+
### 应用A单机gc日志还原
35+
下面我们看下应用A xx.xx.243.44 这个机器在 Full GC 时候的统计日志。
36+
37+
我们先看 GC 开始前(Before GC):
38+
39+
```
40+
2022-10-13T10:13:15.350+0800: 1358193.250: [GC (Allocation Failure) Before GC:
41+
Statistics for BinaryTreeDictionary:
42+
------------------------------------
43+
Total Free Space: 16005
44+
Max Chunk Size: 16005
45+
Number of Blocks: 1
46+
Av. Block Size: 16005
47+
Tree Height: 1
48+
Statistics for IndexedFreeLists:
49+
--------------------------------
50+
Total Free Space: 165963191
51+
Max Chunk Size: 254
52+
Number of Blocks: 32379033
53+
Av. Block Size: 5
54+
free=165979196 frag=1.0000
55+
```
56+
57+
第一行 Allocation Failure 表示没有更多的空闲内存分配了(主意这里需要的是连续的空间),我们可以看到在IndexedFreeLists 统计下面的信息,剩余空间 Total Free Space: 165963191 = 165963191 * 8 / 1024 / 1024 / 1024 = 1.24GB,frag=1.0000 表示这个 1.24GB 空间 100% 的碎片率,最大的连续空间才 Max Chunk Size: 254 = 254 * 8 = 2k,平均空间大小 Av. Block Size: 5 = 5 * 8 = 40bit,真是碎的一地鸡毛啊。
58+
再看上面日志的 BinaryTreeDictionary,在 Full GC 前 Total Free Space: 16005 = 16005 * 8 / 1024 = 125k,也就说只要分配比125k大,内存就爆了。BinaryTreeDictionary (我们简单理解是多个大块的连续内存)是随着内存的分配不断减少的,Number of Blocks 是连续空间块的个数,也一样是随着内存分配不断减少。下图是在 Full GC 前几次BinaryTreeDictionary 统计信息,总大小和可用块数都在不断减少。
59+
60+
![image](image/CMS_GC有点傻_3.png)
61+
62+
我们知道碎片信息在 CMS GC 是无法避免的,只能通过 Full GC回收。
63+
[Monitoring CMS free list BinaryTreeDictionary statistics](https://mail.openjdk.org/pipermail/hotspot-gc-use/2010-December/000760.html)
64+
65+
![image](image/CMS_GC有点傻_4.png)
66+
67+
我们看下Full GC后(After GC)的内存统计情况:
68+
69+
```
70+
CMS: Large block 0x00000006bddd5a90
71+
: 4994842K->1529558K(6291456K), 6.2918770 secs] 8195071K->1529558K(9961472K), [Metaspace: 437386K->436770K(1476608K)]After GC:
72+
Statistics for BinaryTreeDictionary:
73+
------------------------------------
74+
Total Free Space: 609506478
75+
Max Chunk Size: 609506478
76+
Number of Blocks: 1
77+
Av. Block Size: 609506478
78+
Tree Height: 1
79+
Statistics for IndexedFreeLists:
80+
--------------------------------
81+
Total Free Space: 0
82+
Max Chunk Size: 0
83+
Number of Blocks: 0
84+
free=609506478 frag=0.0000
85+
, 7.2442236 secs] [Times: user=7.42 sys=0.00, real=7.25 secs]
86+
```
87+
Full GC 重建了世界的秩序,Total Free Space: 609506478,Number of Blocks: 1 表示整个空闲堆被整理成了1块连续的空间,大小=609506478 * 8 / 1024 / 1024 / 1024 = 4.5GB
88+
89+
IndexedFreeLists 显示,碎片全被干掉了,frag=0.0000,世界清静了,同时整个 GC 耗时 7.25 secs。对于 10G 的大堆,7秒的 Full GC 时间也没什么好惊讶的。
90+
91+
### 应用Afull gc原因
92+
好了,现在我们通过上面日志分析,解释下应用A Full GC 的原因,先贴上应用A的内存和 GC 参数:
93+
94+
```
95+
-server -Xmx10g -Xms10g -Xmn4g -Xss512k -XX:MetaspaceSize=512M
96+
-XX:CMSInitiatingOccupancyFraction=80
97+
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
98+
```
99+
100+
整个堆10G,new generation=4g,old generation=6g,CMSInitiatingOccupancyFraction=80 当老年代使用超过80%的时候还是CMS,就是说,剩余空间小于 6g * (1-80%) = 1.2G的时候才会触发 CMS GC,不幸的是通过我们上面的分析:
101+
剩余空间 Total Free Space: 165963191 = 165963191 * 8 / 1024 / 1024 / 1024 = 1.24GB
102+
明明1.24GB都是碎片,还认为有剩余的空间不进行 CMS GC,由于最后一次内存申请超过125k,雪崩了触发了STW的 Full GC(Allocation Failure),你说这算法是不是很傻?
103+
104+
总结一下:
105+
106+
> 应用A Full GC 的原因是碎片太大,超过了 OldGen * (1-CMSInitiatingOccupancyFraction),这个算法傻的地方是没有看内存的碎片率
107+
108+
109+
所以这里两个Action:
110+
111+
1. CMSInitiatingOccupancyFraction=70,可以缓解。
112+
2. 另外,在 G1 中是可以通过 Minor GC 消除碎片的,JVM 团队同学推荐用 JDK11 的 G1,优化的比较成熟。
113+
114+
## 看看大对象分配
115+
虽然通过上面的分析,即使没有大对象分配,也证明了会导致 Full GC。不过为了放心,索性把大对象分配的猜想也验证一下。
116+
117+
### 背景知识:打印大对象内存分配堆栈
118+
#### -XX:ArrayAllocationWarningSize=20480k
119+
120+
ArrayAllocationWarningSize参数是阿里ajdk特有的参数,在 Dragonwell 8.1.1 后的版本支持。
121+
122+
> 增加了参数**ArrayAllocationWarningSize**,默认值为512M。当分配的对象大小超过该值的时候,标准输出里会显示分配的堆栈。该参数可以通过jinfo动态修改
123+
124+
这里需要注意一下,ArrayAllocationWarningSize 的信息只会打印到标准输出里,所以在我们线上应用需要修改下启动脚本,把 Jetty 或 Tomcat 的标准输出重定向到一个文件,不然数据就丢失了,比如以 Jetty 为例:
125+
126+
![image](image/CMS_GC有点傻_5.png)
127+
128+
### 应用A 实验数据
129+
实验机器:应用A xx.xx.243.44
130+
131+
最近正好封网没有发布,经过5天的测试,在机器上发现的唯一大对象来自com.aliyun.rc.service.cache.ProductLocationWatcher.queryFullLocationTree(ProductLocationWatcher.java:176),分配了30MB。详细日志如下:
132+
133+
```
134+
==WARNING== allocating large array--thread_id[0x00007fd907e8e000]--thread_name[ProductLocationWatcher-1666232342702]--array_size[30669952 byt
135+
es]--array_length[15334967 elememts]
136+
os_prio=0 tid=0x00007fd907e8e000 nid=0x1fbe7 runnable
137+
at com.alibaba.fastjson.serializer.SerializeWriter.expandCapacity(SerializeWriter.java:306)
138+
at com.alibaba.fastjson.serializer.SerializeWriter.writeLong(SerializeWriter.java:757)
139+
at com.alibaba.fastjson.serializer.DateCodec.write(DateCodec.java:228)
140+
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
141+
at com.alibaba.fastjson.serializer.ASMSerializer_11_LocationAttribute.writeNormal(Unknown Source)
142+
at com.alibaba.fastjson.serializer.ASMSerializer_11_LocationAttribute.write(Unknown Source)
143+
at com.alibaba.fastjson.serializer.CollectionCodec.write(CollectionCodec.java:101)
144+
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
145+
at com.alibaba.fastjson.serializer.ASMSerializer_10_Location.writeNormal(Unknown Source)
146+
at com.alibaba.fastjson.serializer.ASMSerializer_10_Location.write(Unknown Source)
147+
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
148+
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:311)
149+
at com.alibaba.fastjson.serializer.ASMSerializer_9_ListResult.writeNormal(Unknown Source)
150+
at com.alibaba.fastjson.serializer.ASMSerializer_9_ListResult.write(Unknown Source)
151+
at com.alibaba.fastjson.serializer.JSONSerializer.writeWithFieldName(JSONSerializer.java:333)
152+
at com.alibaba.fastjson.serializer.ASMSerializer_7_RpcLogExt.writeNormal(Unknown Source)
153+
at com.alibaba.fastjson.serializer.ASMSerializer_7_RpcLogExt.write(Unknown Source)
154+
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:285)
155+
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:758)
156+
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:692)
157+
at com.aliyun.ecs.common.framework.trace.AbstractRpcLogEntryPointExt.toFullJsonString(AbstractRpcLogEntryPointExt.java:119)
158+
at com.aliyun.ecs.common.framework.trace.AbstractRpcLogEntryPointExt.processAndSaveRpcLog(AbstractRpcLogEntryPointExt.java:95)
159+
at com.aliyun.ecs.common.framework.trace.CommonRpcLogFilter.invoke(CommonRpcLogFilter.java:135)
160+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
161+
at com.aliyun.ecs.common.framework.sentinel.filter.CustomizeSentinelDubboConsumerFilter.invoke(CustomizeSentinelDubboConsumerFilter.ja
162+
va:50)
163+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
164+
at com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke(FutureFilter.java:53)
165+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
166+
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75)
167+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
168+
at com.alibaba.csp.sentinel.adapter.dubbo.DubboAppContextFilter.invoke(DubboAppContextFilter.java:41)
169+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
170+
at com.alibaba.dubbo.rpc.filter.ConsumerContextFilter.invoke(ConsumerContextFilter.java:48)
171+
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
172+
at com.alibaba.dubbo.rpc.listener.ListenerInvokerWrapper.invoke(ListenerInvokerWrapper.java:74)
173+
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53)
174+
at com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:77)
175+
at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:226)
176+
at com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72)
177+
at com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52)
178+
at com.alibaba.dubbo.common.bytecode.proxy0.queryFullLocationTree(proxy0.java)
179+
at com.aliyun.rc.service.cache.xxx.queryFullLocationTree(ProductLocationWatcher.java:176)
180+
at com.aliyun.rc.service.cache.xxx.refresh(ProductLocationWatcher.java:107)
181+
at com.aliyun.rc.service.cache.xxx.run(ProductLocationWatcher.java:59)
182+
at java.lang.Thread.run(Thread.java:858)
183+
```
184+
185+
![image](image/CMS_GC有点傻_6.png)
186+
187+
再也没有其他大对象分配了。
188+
189+
所以后面的Action:
190+
191+
1. 这个ArrayAllocationWarningSize=20480k 参数常态化部署到所有机器上。
192+
2. 针对应用的标准输出的大对象进行监控和报警。
193+
194+
最后,也证明了最近的 Full GC 不是大对象引起的,至此,问题基本结案了。
195+
196+
## 加入我们
197+
【稳定大于一切】打造国内稳定性领域知识库,**让无法解决的问题少一点点,让世界的确定性多一点点。**
198+
199+
- GitHub 专栏地址:[https://github.com/StabilityMan/StabilityGuide](https://github.com/StabilityMan/StabilityGuide)
200+
- 钉钉交流群号:30000312
201+
- 如果阅读本文有所收获,欢迎分享给身边的朋友,期待更多同学的加入!

0 commit comments

Comments
 (0)