可怕的 Full GC & OOM

最近我们部门发生了一个因为内存不够引起的网站崩溃订单下降的一级事故。三个月前还在内部做过技术分享,公众号也发了文章(亿级流量系统)分析过这个亿级流量系统要怎么玩,当时有人说亿级流量平摊到每台机器也就这么点流量,你们集群那么多机器, 8C16G 的配置足够了,只能说骚年要对 JVM 各种操蛋问题保持敬畏之心。

为了可能引起不必要的麻烦,我将我们系统更名成了最近比较火热的社区买菜团购系统,主要是用来做各种查询,公司的一些敏感信息做了替换,但基本过程是一致的。

事件回顾



故障描述
故障描述订单异常下降
事件号/优先级INC00xxxxxx(1级)
影响时间2020 年 12月的某一天
部门XX 部门


时间处理过程描述
17:00团购核心查询服务器开始不断拉出集群,造成线上订单开始下跌
17:03核心观察指标系统肉眼可见的观察到订单开始下降
17:03网站中心运维部门收到团购订单下降告警
17:03网站中心运维团队给团购 BU 发起电话会议
17:03运维反馈 APPID :10012345 A应用异常,多个调用方调用它报错, 依赖的 redis 异常, 运维拉入多个DBA 及redis 组同事,运维反馈 应用A 刚刚有发布,要求该项目立即回退
17:06运维反馈APPID 1005678 B 应用大概异常报错,集群机器性能异常,健康检查失败,多个应用调用报错
17:09集群机器太多,回退太慢,运维先将 A 、B 应用的机器分别重启
17:14运维扩容应用A 的 日版集群,由于故障持续时间较长,网站中心拉入部门老司机指挥排障
17:17团购底层的数据引擎服务降级关闭部分政策数据的输出(减少非核心的业务输出)
17:19大佬张三反馈A应用及其性能异常 /Full GC ,机器重启后很快就触发了 Full GC,导致健康检查失败,又被拉出集群
17:20团购系统的开发在发布系统里抓 dump 文件,但由于机器已重启,又切了流量,dump 文件无效
17:23A 应用日版集群扩容完成 运维将应用 A 的流量全部切到日版集群
17:24A 应用原来的两个集群的机器起来被打挂,再起再挂,继续扩容(多挣扎记下也是好的)
17:24团购系统的多个开发和运维发布的成员一起在发布系统里抓 dump 文件,dump 文件还是无效
17:25A 应用通过配置,关闭了response中 非核心商家数据节点的输出
17:34团购系统的系统恢复正常
17:38团购系统恢复刚刚关闭的配置,放开非核心商家数据节点的输出
17:51团购的底层引擎系统恢复关闭的政策

至此可以看到,从事故发生到 订单恢复的这个过程持续了 34 分钟。34 分钟可以说是很久了, 网站的核心功能必须达到4个99, 34 分钟显然不太合格, 这 34 分钟,团购项目组的每个成员,部门的每个成员内心都十分焦灼,但噩梦仍未结束。


时间处理过程描述
18:00团购核心查询服务的服务器又开始不断被拉出集群,造成线上订单又开始下跌
18:00网站中心运维团队给团购 BU 发起第二次电话会议
18:01团购的底层引擎系统关闭的政策,只提供最简单的基础的数据返回给团购系统
18:04运维使用 高配的 8C 32G 做扩容,继续保持原来低配的 8C 16G 机器
18:22订单恢复基本线
18:25开发李四登录问题服务器开始拉取 dump 文件 成功 上传给其他同事
18:30运维登录其他问题服务器开始拉取 dump 文件 成功
18:35开发王五下载 dump 文件完成, 开始分析 dump 文件
18:40团购团队拉出一台生产服务器,开始做生产流量的回放
18:59开发王五 dump 解析完成,查到GiftMgr实例占用7G内存,怀疑礼盒的本地缓存有问题
19:08生产流量回放导致团购查询系统的服务器异常,重现了线上问题
19:16回退下午发布时新增的礼盒配置
19:23 - 19:36回再次进行流量回放,测试是否是礼盒导致的问题,回放期间无异常,确定是礼盒导致的异常
19:40 - 20:10团购系统把关闭的配置全部恢复,保持业务正常使用数据
20:10 - 21:00持续监控生产环境机器运行情况及各项业务指标,无异常
21:10结束监控

故障检测分析:

6分钟, 17:00 - 17:06 (从第一个异常发生时间到团队采取第一个行动时间,这里要注意系统发出报警但是没有人采取行动不能算故障被检测到)

故障控制分析

故障控制时间:

  • 第一次 31分钟,17:55-18:26 (从团队采取第一个行动时间到异常消失时间)
  • 第二次 22分钟,18:53-19:15

故障控制提升空间:

(发现故障之后第一件事是要控制问题,恢复服务。)

  • 1. 在修改任何生产数据和配置时必须通知到相关人员,并且有记录可查询,发生线上故障后,优先回滚临近故障时间点的所有数据改动
  • 2. 制定更完善的服务降级方案,发生问题时逐级进行服务降级,优先保住核心业务
  • 3. 提升Dump抓取、分析效率;

故障起源分析(7 why)

Why1:为什么团购的订单会下降?

A:17:00 开始不断有机器开始频繁full gc,导致服务器僵死,健康检测失败,自动被拉出集群,机器拉出越来越多,导致订单下降

Why2: 为什么机器会僵死?

A:16:40-16:55 产品在平台上修改了26条礼盒规则,其中有5条礼盒的规则在本地缓存初始化时,内存占用会迅速膨胀,频繁GC导致大量对象晋升old gen,之后引发大量fullgc,但是由于此时内存中的对象都有引用,最终导致jvm一直在垃圾回收,执行业务代码的线程全部被挂起;

Why3:为什么频繁full gc而没有触发OOM?

A:本地的礼盒缓存使用了字典树的结构来存储,所以整颗树中不存在大的连续空间,当jvm分配离散的小对象时,即使内存中已经没有任何空间可以分配,gc也会继续尝试回收垃圾,由于此时执行的是full gc,一次12G heap回收需要约30s,经过测试环境重现,触发gc overhead的条件需要20+min,但这段时间内jvm会挂起所有业务逻辑线程,导致tomcat进程几乎无法工作。这段时间内由于jvm不抛出OOM,只能手工重启

Why4:为什么内存暴增?

A:单独分析一条出问题的规则,根据公式可算出节点数 未修改数据前,一条规则占用约107W+个节点,当产品将XX政策从 X 改成38条之后,一条规则占用约3371W+个节点,膨胀约31倍, 缓存每5min刷新一次,刷新时要先创建新的字典树对象,过程中jvm heap会持有双份节点,即3371W 乘以 2个节点,按一个节点144Byte估算,总共需要约10G内存, 团购查询服务器配置是8C16G,采用默认策略分配的heap大小为12G,无法满足缓存刷新时需要的内存开销

Why5:为什么采用字典树结构?

A:初衷是用空间换性能,但是使用的时候开发没有评估字典树所带来的内存膨胀风险

Why6:为什么之前字典树没有发生问题?

A:字典树组件内部没有对节点膨胀做保护机制,树中node的数量完全由维护的数据决定,随时都存在着潜在的风险,之前问题一直隐藏着没有被触发

Why7: 为什么定位问题较慢?

A:礼盒表的数据修改没有邮件通知团购查询的开发,所以定位问题时只能通过分析dump定位问题,但是抓dump+文件解析效率不高

Why8: 为什么 dump 文件开始抓不下来?

JVM 参数里配置的是发生 OOM 的时候将 dump 文件保存到 opt 的某个目录夹下, 又因为在那么情况危急的情况下Full GC 的时间太久了,还没有发生 OOM 机器就被运维或者开发重启了, 所以导致 dump 文件没有生成。后来第二次出问题恢复系统后保留了 OOM 现场才拿到 dump 文件, 服务器带宽也不够, dump 文件整个过程花了9分钟。

Why9 为什么字典树一条规则会有107W 这么多个节点?

字典树定义:字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。

业务场景如下,如果想在团购列表页快速的找到某个摊位的数据,通过 北京-A城区-C小区-D摊贩,很快的就可以定位到这个摊贩,假设有三个城市,每个城市都有6个城区,每个城区都有6个小区,每个小区都有6个摊贩,那这颗字典树就有 3 * 6 * 6 * 6 个节点。显示这做了一次笛卡尔集求和。团购查询系统当时是做了一个七层索引的查询,极限的在利用空间换时间,空间的代价有点大了。


天了噜!亿级流量系统发生 OOM_java


吸取的教训


  • 内存不正确地使用带来的影响是灾难级的
  • 要事先准备好完善的降级措施,发生问题时有序降级非核心业务
  • Dump工具不可用时,登录生产机器抓成功功率最高,并且由于dump分析需要大内存,建议用生产高配服务器分析

改正/提高措施


  • 排查所有用到字典树的本地缓存,判定风险等级
  • 生产发布时,所有开发人员需要知晓配置文件及相关的配置改动,要敏感
  • 撰写dump指导手册
  • 构建部门共享pool用来分析dump
  • 编码规范中限制多级索引的最大层级
  • Sonar检测添加多层Map的规则
  • 解析gc log,将每次gc回收的内存大小数据推送到监控平台
  • 针对gc回收的内存大小建告警机制

小结


  • 生产环境发生内存异常的情况, 第一时间回退版本、回退配置,如果想通过扩容解决问题也需要扩容高配机器,因为如果不是因为流量问题引起的内存上升,扩容同配置的机器问题并不能解决,反而拖长了出问题的时间。

  • 保持敬畏之心, 对自己写的每一行代码需要清楚背后的原理,消耗多少 CPU 、 消耗多少内存、网络IO 耗时多久,不然出问题排查起来根本没思路。

  • 坚持学习, 技术 业务 思维能力 各方面都不能落下。

看完如果觉得有收获      

麻烦帮忙关注、转发、点赞 天了噜!亿级流量系统发生 OOM_java_02天了噜!亿级流量系统发生 OOM_java_02