Java实战篇——千里之堤,溃于GC

Java
303
0
0
2023-06-05

Java实战篇——千里之堤,溃于GC

文章主体共计2102字,阅读约需5分钟,其中涉及异常定位思路,建议先收藏再看。

引言

GC是 java 服务端开发过程中所必须关注的一个重要环节。但遗憾的是,往往很多人会等线上真的出现问题以后,才会关注GC调优的问题。当然,这样的做法是不可取的,良好的监控,及时预警,一旦发现GC性能恶化,及时调优,才能避免线上问题出现。

本文将描述一个真实的生产环境问题,以及笔者定位问题的过程。为读者提供一些案例经验,并通过这次定位问题的思路希望能有所收获。

Java实战篇——千里之堤,溃于GC

突然间决堤

服务报警

深夜11:43分,没有上线操作,即将卸下疲惫的程序员超哥,突然收到服务大量的异常量报警。

程序员超哥,急忙进入Kubernetes集群查看服务,事态虽急,但超哥心中已有对策,想着应该是机器负载过高引起的,负载过高的原因应该是机器上同时多个容器服务流量激增,这个问题对他来说会很熟练的解决!

top一下,发现接口服务的 cpu 确实很高,进行常规处理后(这里的处理主要是对集群的服务分配进行手动干预),服务的现状并没有好转。

可能原因并不是那么简单。

Java实战篇——千里之堤,溃于GC

初步定位

毕竟是久经沙场的老将,超哥对于解决问题并不担心,娴熟的操作打开服务日志进行查看,如下,看到一个平常并未见到的日志如下:

经过一番思考后,这个错误是可以预期的,并不会造成服务的崩塌,排除掉,需要继续定位问题

Java实战篇——千里之堤,溃于GC

问题初显

对于java服务,惯性思维,超哥开始查看gc的情况,输入指令查看后:

Java实战篇——千里之堤,溃于GC

gc结果令人背部发凉:full gc的次数过多,old区的爆炸,这可是生产环境,背后300多个业务线一瞬间损失不可估量。

超哥下一步检查 jvm 参数,如下:

-Xms2g -Xmx2g -Xmn1g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80

超哥先尝试改大jvm的参数到10g,扩容上线后很快内存打满,gc飙升。

这个时候你会怎么做?

友情提示:

jvm的参数是无法通过数值来评判好坏的,必须在服务搭建的时候不断的自测调优,根据服务真实情况,基于响应时间和吞吐量的取舍来做一个平衡。

如果你不会,那么请先参考友商的套路,这是一般程序员的风格。

如果只考虑因为机器强大就将服务的jvm参数设大一些,这种想法既不勤俭持家,也是对共载服务的不负责。

Java实战篇——千里之堤,溃于GC

深入跟踪

到此,现在知道有两个问题:

  1. 服务的cpu占用比以往要高
  2. gc是有问题的,需要排查一下是什么对象占用
  • 先解决第一个问题:

选一个所在负载较小的机器,jstack 查看,打印如下:

java. lang .Thread.State: RUNNABLE
at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
– locked <0x00000000ad45ec08> (a java.util.Collections$UnmodifiableSet)
– locked <0x00000000ad45eb30> (a sun.nio.ch.EPollSelectorImpl)
at java.lang.Thread.run(Thread.java:748)

看不出任何端倪,jstack 的打印非常多, 我们接下来需要grep拿到关键信息

查看进程内的 线程 ,看谁占用的资源最高,此处资源指cpu

使用命令top -Hp pid

[root@proxy proxy]# top -Hp 14
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
42 root 20 0 24.5g 3.7g 16m R 41.7 3.0 19:50.16 java

可以看到PID为42的占用cpu最高,把PID转换成二进制: 2a

jstack 14 |grep “nid=0x2a”

[root@proxy proxy]# jstack 14 |grep “nid=0x2a
“VM Thread” os_prio=0 tid=0x00007f0784186000 nid=0x2a runnable

占用cpu最高的jvm线程,不断的gc,那么接下来可以专注分析gc的问题了

再解决第二个问题:

jmap 查看存活对象

Java实战篇——千里之堤,溃于GC

上图画了两个箭头号:

  • 第一个Object 是什么暂时不得而知,很有可能是问题的原因。
  • 第二个String,因为在本服务中传送的数据对象是String类型。

String类型的对象并不多,那么并不是服务处理不过来性能产生了bug!

友情提示: 我们在分析存活对象的时候,尽量先对自己的代码保持高度的怀疑,然后再怀疑其他开源组件。这也是我没有把3-5行 netty 组件的对象标红的原因!

可以针对Object进行具体分析,但是这里取巧了一下,先看看fd的情况:

Java实战篇——千里之堤,溃于GC

fd的占用非常多,所有的IO操作都会通过fd来执行操作

在服务中无非就有三处关键地方会产生此问题:

  • 存储端
  • kafka端(用来做消息队列)
  • etcd端(做配置中心)

这三处是否存在大量创建对象的bug?

需要把代码拿出来鞭尸检阅。

Java实战篇——千里之堤,溃于GC

真相大白

其实还有一点从一开始被忽略,在本文开始的时候就看了被忽视的日志,往往简单的步骤最有效。

这里简单描述一下代码bug产生的原因:

由于扩建了新K8s集群,新老集群复用过程中,新老集群的etcd会被服用,新集群的应用创建导致老集群load不到某个新集群的etcd配置,从而etcd不断的重试,重试一次就会创建一个新对象!

另一个问题是,etcd并没有进行自动关闭连接,这也是在使用时需要注意的地方。

总结

整个定位和处理过程,一方面体验出GC定位在JAVA服务中问题处理的重要性,另一方面,也给我们提了个醒,开源工具虽好,但对于其特性也应该熟悉,不能只停留在简单的增删改上,任何小的忽视都有可能导致我们在服务可用性上的漏洞,切记切记。