解析频繁触发FillGC

2023-07-28 jvm

背景:搜索微服务经常半夜频繁触发full GC,数据数量的10亿级别的,本篇文章将一步步分析问题产生的原因及解决方案,如果不知道full GC是什么见文章尾。

功能介绍:类似今日头条APP的搜索功能,搜索结果默认展示综合,可以切换到视频、咨询、小视频等等,这个功能使用ElasticSearch搜索引擎实现。

# 数据同步

用户搜索从ES获取数据,但数据并不是直接存放在ES搜索引擎中,用户数据存放在MySQL,通过增量同步或全量同步将数据存储到ES中。

# 增量同步

增量同步是指在数据同步过程中,只同步发生变化的数据部分,而不是全部数据。例如新增了一条新闻数据,只会将这一条数据插入到ES中,一般有两种方案。

  • 接口调用:新增/修改/删除数据时调用接口,也就是在操作数据库时同时操作ES,完成数据同步。
  • 基于Canal等插件:通过读取MySQL日志或其他方式,也就是监听数据库本身发生变动,将其同步更改至ES。

# 全量同步

全量同步是指将源数据全部复制到目标系统,无论数据是否发生变化,一般用于兜底操作,我们不能保证Canal或搜索引擎接口永远不出错误,全量同步就是兜底操作,保证即使某些数据因为接口故障等原因,在全量同步时得以修复,一般有两种方案:

  • 搜索服务定时读取业务方MySQL同步ElasticSearch中的数据。
  • 搜索服务调用业务方接口同步数据

经排查,半夜频繁触发full GC是搜索服务进行全量同步时导致的。

# 问题剖析

当前JVM配置为4G,那么按照默认比例young:old=1:2=1.3:2.7,young中比例为eden:from:to=8:1:1,整理后如图所示(图中结论仅为估计结论有误差,为方便撰写将其改为相近整数)

案例JVM初始状态

# 运行时长剖析

定时器夜间1点从MySQL从库每次读取2万条数据,然后将2万条数据写入ES的index库中,假设每条业务数据10个字段,平均每个字段100byte,也就是一条数据约1KB(1024byte)。

每条线程读取2万条数据,数据大小约20MB,每次花费2s处理(不是纯粹读写,还需要一定的业务处理)使用10条线程并行处理。

那么能得出2s产生的数据是多少,10线程=10*2w=20万条,约10*20MB=200MB,也就是2s能处理20万条数据,数据大小为200MB,那么1s能处理10万条数据,10亿条数据大概需要2.7个小时。理论上1点执行到4点执行完毕。

Eden区多久被塞满?Eden共1.1G,1s产生100MB数据,11秒就能填满Eden区。

# 每次YoungGC还有多少对象存活?

对象的存活,要先被栈帧引用,而栈帧存储在线程栈中。我们开了10条线程在并行,就有10个栈帧时刻运行着。也就是说,每时每刻都有10条线程运行,约有200mb的数据被线程栈帧引用。所以,每10秒触发1次ygc时,就有200mb的数据存活。

# 如何处理存活的200MB数据

每次YoungGC后,200mb通过复制算法,复制给survivor区,但是survivor被分割为from和to区各100MB,from100m放不下200m,只能先把from的50m-100m(动态年龄50%)塞满后,剩下的100m-150m复制到old区。

# old区多久被填满?

Old区有2.7G,每10秒触发一次YoungGC就有100m进old区,接下来来模拟这种动作:

  • 第1次10秒触发YoungGC,把Eden的存活200M其中100m复制进from, 100m进old,最后清空Eden
  • 第2次10秒触发YoungGC,把Eden+From的存活200M其中100m复制进to, 100m进Old,最后清空eden和from,并from与to互换(复制算法)。
    • 有个疑问,from第一次不是100m存活吗?怎么Eden+From的存活才200M,不是300M吗?
    • 因为第一次的from的100M经过10秒后,老早就没引用了,变为垃圾对象,所以Eden+From其实只有Eden200M。

就这样经过了,2.7G/100M=27次YoungGC后,old区满了27 * 10秒=270秒/60=4.5分钟,即每4.5分钟会触发full GC,这个频率还是比较高的,频繁的full GC会导致系统卡顿,影响性能。

搜索服务old填满分析

# 解决方案:jvm性能优化

此案例最大的问题就是频繁full GC导致系统卡顿,没有YoungGC都有100MB没有地方放,导致其进入Old区,但是通过场景分析我们知道,这100MB不是长期存活对象,是“被迫”进入Old区的。

**核心:如果年轻代的From区能存下200MB的数据,就不会触发此问题,**200MB会在使用后通过GC自动释放掉,因为这200MB可以理解为被JVM错误的认为是长期存活对象,导致其进入Old区,使old区被快速填满触发full GC

  1. 把堆再加1G变成5G,-Xms=5g -Xmx=5g
  2. 把年轻代设置为2g,-Xmn=2G
  3. 把survivor from to设置为256mb
    • -XX:SurvivorRation=6:设置年轻代Eden区与Survivor区的大小比值,Eden:From:To=6:1:1=1536:256:256

# 补充:什么是FullGC

FullGC是指Java虚拟机(JVM)在进行垃圾回收时,对堆存进行完整的垃圾回收操作,在JVM中堆内存被分为新生代和老年代,当新生代的Eden区和Survivor区无法容纳新对象时,会触发Minor GC,只清理新生代内存。

而FullGC则是在老年代或整个堆内存(包括新生代和老触发的一种垃圾回收机制。FullGC的执行需要清理整个堆内存,包括新生代和老年代,因此会导致较长的暂停时间,FullGC通常在以下情况下触发:

  • 当老年代的内存空间不足,无法容纳新对象时。
  • 当永久代(Permanent Generation)或元数据区(Metaspace)不足时。
  • 当由于调优目的需要手动FullGC的发生可能会导致较长的暂停时间,响应用程序的响应性能。因此,合理的堆内存分配和垃圾回收策略很重要,以减少FullGC的频率和持续时间。
上次更新: 5 个月前