关于后端:一次JVM-GC长暂停的排查过程

3次阅读

共计 5037 个字符,预计需要花费 13 分钟才能阅读完成。

作者:京东科技 徐传乐

背景

在高并发下,Java 程序的 GC 问题属于很典型的一类问题,带来的影响往往会被进一步放大。不论是「GC 频率过快」还是「GC 耗时太长」,因为 GC 期间都存在 Stop The World 问题,因而很容易导致服务超时,引发性能问题。

事件最后是线上某利用垃圾收集呈现 Full GC 异样的景象,利用中个别实例 Full GC 工夫特地长,持续时间约为 15~30 秒,均匀每 2 周左右触发一次;







JVM 参数配置“-Xms2048M –Xmx2048M –Xmn1024M –XX:MaxPermSize=512M”





排查过程

Ø 剖析 GC 日志

GC 日志它记录了每一次的 GC 的执行工夫和执行后果,通过剖析 GC 日志能够调优堆设置和 GC 设置,或者改良应用程序的对象分配模式。

这里 Full GC 的 reason 是 Ergonomics,是因为开启了 UseAdaptiveSizePolicy,jvm 本人进行自适应调整引发的 Full GC。

这份日志次要体现 GC 前后的变动,目前为止看不出个所以然来。





开启 GC 日志,须要增加如下 JVM 启动参数:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/export/log/risk_pillar/gc.log

常见的 Young GC、Full GC 日志含意如下:





Ø 进一步查看服务器性能指标

获取到了 GC 耗时的工夫后,通过监控平台获取到各个监控项,开始排查这个时点有异样的指标,最终剖析发现,在 5.06 分左右(GC 的时点),CPU 占用显著晋升,而 SWAP 呈现了开释资源、memory 资源增长呈现拐点的状况(详见下图红色框,橙色框中的变动是因批改配置导致,前面会介绍,暂且可疏忽)





JVM 用到了swap?是因为 GC 导致的 CPU 忽然飙升,并且开释了 swap 替换区这部分内存到 memory?

为了验证 JVM 是否用到 swap,咱们通过查看 proc 下的过程内存资源占用状况

for i in $(cd /proc;ls grep “^[0-9]” awk ‘ $0 >100’) ;do awk ‘/Swap:/{a=a+$2}END{print ‘”$i”‘,a/1024″M”}’ /proc/$i/smaps 2>/dev/null ; done sort -k2nr head -10 # head -10 示意 取出 前 10 个内存占用高的过程 # 取出的第一列为过程的 id 第二列过程占用 swap 大小

看到的确有用到 305MB 的 swap





这里简略介绍下什么是swap?

swap 指的是一个替换分区或文件,次要是在内存应用存在压力时,触发内存回收,这时可能会将局部内存的数据交换到 swap 空间,以便让零碎不会因为内存不够用而导致 oom 或者更致命的状况呈现。

当某过程向 OS 申请内存发现有余时,OS 会把内存中临时不必的数据交换进来,放在 swap 分区中,这个过程称为 swap out。

当某过程又须要这些数据且 OS 发现还有闲暇物理内存时,又会把 swap 分区中的数据交换回物理内存中,这个过程称为 swap in。



为了验证 GC 耗时与 swap 操作有必然关系,我抽查了十几台机器,重点关注耗时长的 GC 日志,通过工夫点确认到 GC 耗时的工夫点与 swap 操作的工夫点的确是统一的。

进一步查看虚拟机各实例 swappiness 参数,一个普遍现象是,但凡产生较长 Full GC 的实例都配置了参数 vm.swappiness = 30(值越大示意越偏向于应用 swap);而 GC 工夫绝对失常的实例配置参数 vm.swappiness = 0(最大限度地升高应用 swap)。

swappiness 能够设置为 0 到 100 之间的值,它是 Linux 的一个内核参数,控制系统在进 行 swap 时,内存应用的绝对权重。

Ø swappiness=0: 示意最大限度应用物理内存,而后才是 swap 空间

Ø swappiness=100: 示意踊跃的应用 swap 分区,并且把内存上的数据及时的替换到 swap 空间外面







对应的物理内存使用率和 swap 应用状况如下







至此,锋芒仿佛都指向了 swap。

Ø 问题剖析

当内存使用率达到水位线 (vm.swappiness) 时,linux 会把一部分临时不应用的内存数据放到磁盘 swap 去,以便腾出更多可用内存空间;

当须要应用位于 swap 区的数据时,再将其换回内存中,当 JVM 进行 GC 时,须要对相应堆分区的已用内存进行遍历;

如果 GC 的时候,有堆的一部分内容被替换到 swap 空间中,遍历到这部分的时候就须要将其替换回内存,因为须要拜访磁盘,所以相比物理内存,它的速度必定慢的令人发指,GC 进展的工夫肯定会十分十分恐怖;

进而导致 Linux 对 swap 分区的回收滞后(内存到磁盘换入换出操作非常占用 CPU 与零碎 IO),在高并发 /QPS 服务中,这种滞后带来的后果是致命的(STW)。

Ø 问题解决

至此,答案仿佛很清晰,咱们只需尝试把 swap 敞开或开释掉,看看是否解决问题?

如何开释 swap?

  1. 设置 vm.swappiness=0(重启利用开释 swap 后失效),示意尽可能不应用替换内存

a、长期设置计划,重启后不失效

设置 vm.swappiness 为 0

sysctl vm.swappiness=0

查看 swappiness 值

cat /proc/sys/vm/swappiness

b、永恒设置计划,重启后依然失效

vi /etc/sysctl.conf

增加

vm.swappiness=0

  1. 敞开替换分区 swapoff –a

前提:首先要保障内存残余要大于等于 swap 使用量,否则会报 Cannot allocate memory!swap 分区一旦开释,所有寄存在 swap 分区的文件都会转存到物理内存上,可能会引发零碎 IO 或者其余问题。

a、查看以后 swap 分区挂载在哪?





b、关停分区





敞开 swap 替换区后的内存变动见下图橙色框,此时 swap 分区的文件都转存到了物理内存上





敞开 Swap 替换区后,于 2.23 再次发生 Full GC,耗时 190ms,问题失去解决。





Ø 纳闷

1、是不是只有开启了 swap 替换区的 JVM,在 GC 的时候都会耗时较长呢?

2、既然 JVM 对 swap 如此不待见,为何 JVM 不明令禁止应用呢?

3、swap 工作机制是怎么的?这台物理内存为 8g 的 server,应用了替换区内存(swap),阐明物理内存不够应用了,然而通过 free 命令查看内存应用状况,理论物理内存仿佛并没有占用那么多,反而 Swap 已占近 1G?





free:除了 buff/cache 残余了多少内存

shared:共享内存

buff/cache:缓冲、缓存区内存数(应用过高通常是程序频繁存取文件)

available:实在残余的可用内存数


大家能够想想,敞开替换磁盘缓存意味着什么?

其实大可不必如此激进,要晓得这个世界永远不是非 0 即 1 的,大家都会或多或少抉择走在两头,不过有些偏差 0,有些偏差 1 而已。

很显然,在 swap 这个问题上,JVM 能够抉择偏差尽量少用,从而升高 swap 影响,要升高 swap 影响有必要弄清楚 Linux 内存回收是怎么工作的,这样能力不脱漏任何可能的疑点。

先来看看 swap 是如何触发的?

Linux 会在两种场景下触发内存回收,一种是在内存调配时发现没有足够闲暇内存时会立即触发内存回收;另一种是开启了一个守护过程(kswapd 过程)周期性对系统内存进行查看,在可用内存升高到特定阈值之后被动触发内存回收。

通过如下图示能够很容易了解,详细信息参见:http://hbasefly.com/2017/05/2…





解答是不是只有开启了 swap 替换区的 JVM,在 GC 的时候都会耗时较长

笔者去查了一下另外的一个利用,相干指标信息请见下图。

实名服务的 QPS 是十分高的,同样能看到利用了 swap,GC 均匀耗时 576ms,这是为什么呢?







通过把工夫范畴聚焦到产生 GC 的某一时间段,从监控指标图能够看到 swapUsed 没有任何变动,也就是说没有 swap 流动,进而没有影响到垃级回收的总耗时。









通过如下命令列举出各过程 swap 空间占用状况,很分明的看到实名这个服务 swap 空间占用的较少(仅 54.2MB)





另一个显著的景象是实名服务 Full GC 距离较短(几个小时一次),而我的服务均匀距离 2 周一次 Full GC









基于以上揣测

1、实名服务因为 GC 距离较短,内存中的货色基本没有机会置换到 swap 中就被回收了,GC 的时候不须要将 swap 分区中的数据交换回物理内存中,齐全基于内存计算,所以要快很多

2、将哪些内存数据置换进 swap 替换区的筛选策略应该是相似于 LRU 算法(最近起码应用准则)

为了证实上述猜想,咱们只需跟踪 swap 变更日志,监控数据变动即可失去答案,这里采纳一段 shell 脚本实现

#!/bin/bash 
echo -e `date +%y%m%d%H%M%S` 
echo -e "PID\t\tSwap\t\tProc_Name" 

#拿出 /proc 目录下所有以数字为名的目录(过程名是数字才是过程,其余如 sys,net 等寄存的是其余信息)for pid in `ls -l /proc | grep ^d | awk '{print $9}'| grep -v [^0-9]` 
do 
    if [$pid -eq 1];then continue;fi 
    grep -q "Swap" /proc/$pid/smaps 2>/dev/null 
    if [$? -eq 0];then 
        swap=$(gawk '/Swap/{ sum+=$2;} END{print sum}' /proc/$pid/smaps) #统计占用的 swap 分区的 大小 单位是 KB 
        proc_name=$(ps aux | grep -w "$pid" | awk '!/grep/{ for(i=11;i<=NF;i++){printf("%s ",$i); }}') #取出过程的名字 
        if [$swap -gt 0];then #判断是否占用 swap 只有占用才会输入 
            echo -e "${pid}\t${swap}\t${proc_name:0:100}" 
    fi 
   fi
done | sort -k2nr | head -10 | gawk -F'\t' '{ #排序取前 10 
    pid[NR]=$1; 
    size[NR]=$2; 
    name[NR]=$3; 
} 
END{for(id=1;id<=length(pid);id++) 
    {if(size[id]<1024) 
        printf("%-10s\t%15sKB\t%s\n",pid[id],size[id],name[id]); 
    else if(size[id]<1048576) 
        printf("%-10s\t%15.2fMB\t%s\n",pid[id],size[id]/1024,name[id]);
    else 
    printf("%-10s\t%15.2fGB\t%s\n",pid[id],size[id]/1048576,name[id]); 
    } 
}'

因为下面图中 2022.3.2 19:57:00 至 2022.3.2 19:58:00 产生了一次 Full GC,咱们重点关注下这一分钟内 swap 替换区的变动即可,我这里每 10s 做一次信息采集,能够看到在 GC 时点前后,swap 的确没有变动




通过上述剖析,回归本文外围问题上,当初看来我的解决形式过于激进了,其实也能够不必敞开 swap,通过适当升高堆大小,也是可能解决问题的。

这也侧面的阐明,部署 Java 服务的 Linux 零碎,在内存调配上并不是无脑大而全,须要综合思考不同场景下 JVM 对 Java 永恒代、Java 堆(新生代和老年代)、线程栈、Java NIO 所应用内存的需要。

总结

综上,咱们得出结论,swap 和 GC 同一时候产生会导致 GC 工夫十分长,JVM 重大卡顿,极其的状况下会导致服务解体。

次要起因是:JVM 进行 GC 时,须要对对应堆分区的已用内存进行遍历,如果 GC 的时候,有堆的一部分内容被替换到 swap 中,遍历到这部分的时候就须要将其替换回内存;更极其状况同一时刻因为内存空间有余,就须要把内存中堆的另外一部分换到 SWAP 中去,于是在遍历堆分区的过程中,会把整个堆分区轮流往 SWAP 写一遍,导致 GC 工夫超长。线上应该限度 swap 区的大小,如果 swap 占用比例较高应该进行排查和解决,适当的时候能够通过升高堆大小,或者增加物理内存。

因而,部署 Java 服务的 Linux 零碎,在内存调配上要谨慎。

以上内容心愿能够起到抛转引玉的作用,如有了解不到位的中央烦请指出。

正文完
 0