容器重启23次,原因竟然是。。。。

最烦的事情,莫过于服务莫名其妙的重启,当你看到一个服务一天重启23次,你会是怎样的一个感觉,反正博主我快要摔电脑了。。。。

 

问题既然已经发生了,肯定得动手术刀解决它。在开始看代码之前,我们可以先来假想一下,发生服务重启的原因可能有哪些,然后再根据可能性一条条的排查,这种方式可以快速的帮助我们分析并找到最终的问题点。

服务重启的可能原因:

  1. 第三方软件失效导致容器重启(MySQL、Redis、MQ等)
  2. 并发过高,导致cpu满负荷,服务宕机重启
  3. 容器所需资源被其它容器所干扰,导致资源不够重启
  4. 占用内存过多,被linux进程杀死

1.第三方软件失效

因为项目日志级别是error,所以一旦发生第三方配置失效,会马上打印出错误信息,但是查看了错误日志并没有此类的错误日志,而且查看了MySQL、Redis、MQ等运行情况,没有发现任何异常信息,所以基本可以排除是因为第三方导致容器重启。

2.并发过高

并发的可能性,我们可以通过APM(应用性能管理)来查看,这边博主的系统中APM是采用SkyWalking管理工具,并发的指标如下图所示:

系统的并发为平均每分钟224.20,可以看出该服务的并发不是特别高,服务的配置完全可以支撑的住,所以也不太可能是因为并发问题导致的服务重启。

3.容器被干扰

至于第三个原因,因为博主的项目是部署在腾讯云上面,可以精确的对各个容器资源进行设置,所以也不可能出现互相干扰的情况。

4.内存问题

分析到这边,问题就转化为JVM内存溢出问题排查了,我们首先开启JVM垃圾回收日志,JVM命令如下所示:

-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

运行一段时间后,我们可以看到如下所示的JVM垃圾回收日志:

如果童靴们看不懂这些日志的话,可以参考一下下面两幅说明图:

Major GC:

Full GC:

通过GC日志,我们可以看到,在很短的时间内发生了多次Major GC而且相对于老年代,年轻代的内存设置很小,我们来看看这个问题服务的JVM参数配置信息:

-Xms4000m
-Xmx4000m
-Xss512k
-Xmn800m
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

这里的JVM很简单,就设置了最大堆、最小堆、年轻代、永久代、线程大小、以及GC日志相关配置,那这个JVM参数问题出现在哪里?

童靴们要知道JVM参数的配置是要根据实际项目来的,不同类型的项目JVM参数会截然不同,这里博主就简单以这个问题项目举例说明,如果想了解更多JVM内容,可以自行百度学习,这边不进行过多称述。

首先要明确项目类型是数据服务,还是API服务,博主这个服务就是一个API服务,所以对实时性要求比较高。如果对实时性要求比较高,就要控制GC的停顿时间,因为在垃圾回收的时候不管是什么垃圾回收算法都会有一定的性能损耗。比如新生代的复制算法,老年代的标记整理清除算法等都是有一定性能损耗的。

推测一:大对象推测导致服务重启

这些GC的细节博主就不过多陈述了,我们来分析分析重启服务GC参数的问题,为什么会导致服务重启呢?按照以往的经验,博主第一时间想到的会不会是大对象、大集合引起的内存泄漏导致的。后面博主通过RamUsageEstimator.humanSizeOf()工具类来打印对象内存使用情况

List<TAmzdbProduct> resultList = tAmzdbProductDao.getProudctList(map);
long ramSize = RamUsageEstimator.sizeOf(resultList) / (1024 * 1024);
// 内存大小大于5M
if (ramSize >= 5) {
    log.error("getProudctList接口,list数据过大,size:{},accountId:{}", ramSize, accountId);
}

从测试结果可以得出,没有超过5M的大对象,所以不可能是因为一个突然的大对象导致的内存泄漏问题。(当然也可以通过jmap 、jstat 、jstack等命令查看)

推测二:Full GC不及时

如果不是大对象引起的内存泄漏问题,那还会是什么呢?博主通过认真分析JVM垃圾回收日志,终于发现了蛛丝马迹,JVM垃圾回收日志显示,Major GC特别频繁,Full GC几乎很少发生,那我们是不是可以推测,是因为年轻代设置的太小,然后每次用户请求所产生的内存对象也很小,导致对象随着时间的推移全部跑到老年代中,而且因为对象内存很小,所以不会促发Full GC,等老年代满的时候着正要触发Full GC的时候,因为服务内存使用过大,导致服务被进程杀死呢?

针对这一推测,我们先来调整一下JVM参数,将年轻代设置大一些,老年代在达到内存70%的时候就触发Full GC,通过这种方式就可以解决上面的问题。

JVM参数

-Xms4000m
-Xmx4000m
-Xss512k
-Xmn1500m 
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+DisableExplicitGC
-XX:SurvivorRatio=6
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=2
-XX:+CMSClassUnloadingEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC

JVM参数设置完毕之后,通过持续性跟踪服务运行状况,结果很喜人,运行了2天多的时间,服务没有重启过一次,所以可以充分说法我们上述的推测是正确的。

问题已经得到修复,但是修复问题并不是我们最终的目的,博主写这篇文章的目的,主要是为了分享基于已经存在的问题,我们应该运用怎么样的思维模式去解决它呢?如何才能更高效,更便捷呢?当然每个人的思考模式都不一样,正所谓一千个读者就有一千个哈姆雷特,童鞋们不必死记硬背,活学活用才是最终的目的。

流程总结:

  1. 发现问题
  2. 通过问题分析,提出可能引发问题的原因
  3. 一一验证假设原因,排除错误的假设原因
  4. 确定假设原因,缩小范围继续排查
  5. 通过测试实验得出结果
  6. 修改代码并发布进行结果验证
  7. 验证通过,问题解决

想要更多干货、技术猛料的孩子,快点拿起手机扫码关注我,我在这里等你哦~

林老师带你学编程https://wolzq.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章