热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【事故复盘】吐血整理一场线上事故——CPU飙升200%

🔍经验使你在第二次犯相同错误时及时发现。——琼斯🔖事故时间:12.7日17:43~21:03🔖事故影响:





🔍 经验使你在第二次犯相同错误时及时发现。 —— 琼斯



🔖 事故时间:12.7日 17:43 ~ 21:03


🔖 事故影响:服务接口查询慢,用户查看数据转圈


🔖 事故过程:

那是一个宛如平静的下午,一群程序员像往常一样工作着....
突然,在17:43时,运营和运维反馈我们的产品转圈了,且监控发现大量异常发生。
随后我去APM上看了监控,发现service请求响应时间激增。
第一时间怀疑是数据库TiDB的问题(因为就在前几天有个TiDB问题,且还没有修复上线)。
17:50 为了防止大量请求堆积把服务打挂,于是我便进行了服务的重启。
17:55 重启完发现响应时间确实降下来了。
18:20 观察了一阵,发现接口查询已经缓解,于是便先通知了运营。
此时已经算是松了一口气,随后,后端同学便开始排查问题原因。
最开始的排查思路,是这样的:
因为17:30 时突然有大量请求,怀疑是大量查询造成了DB压力,然后造成了查询超时。
于是开始逐一排查激增时间段的这些请求接口。
(我认为这里有一个误区,就当服务整体不可用时,单看某一个或者某几个接口很难排查到问题原因,
应尽量先从全局共享资源去考虑,例如:数据库、缓存、服务CPU、服务内存、网络等)
期间有一个schedule问题,也怀疑过是这个问题造成的,但最终分析不是主要原因。
随着时间的流逝,发现服务每半小时还是有抖动。
不料,20:14 运维和运营同学又相继反馈大量异常和查询慢问题。
20:31,后端同学发现service服务CPU自从12.6发版后异常的高,比平常要翻一倍多。且GC次数也异常增多。
20:33,拉上其他同学,召开紧急会议,确定回退服务对超级店长没有影响时,便进行了版本回滚。
20:49,把服务回滚完毕,继续观察。回滚完CPU果然回到了正常,一切恢复平静,总算松了一口气。
随后,便反馈给运营同学。
12.8 ~ 12.11号,后端同学耗费了大量时间和人力,终于排查到问题根因,以此文章记录事故发生过程及问题排查思路。

🔖 结果根因

根本原因是由于DevOps平台的maven构建近期可能进行了更新,后端同学在构建时发现报错,于是在pom中指定了maven-plugin的版本为项目的springboot版本,也就是2.3.2-RELEASE,怎料spring-boot-loader在2.3.2-REALEASE版本👉👉👉👉👉存在BUG:



当加载不存在类时,会产生大量JarFile对象实例未被引用,从而产生内存泄露,引发服务进行频繁GC而抢占大量的CPU资源,从而引发CPU占用激增,造成服务整体卡顿。
https://github.com/spring-projects/spring-boot/issues/22991
https://github.com/spring-projects/spring-boot/issues/28042



🔖 排查方式



我们花了四天时间,从各个角度对服务进行了问题排查,希望对以后排查问题的同学能有前车之鉴。
下面是不同角度的排查思路:



1.代码

用了一天的时间进行了新发代码的Review,发现除了有个interface版本不一致问题(后来验证该问题不影响主要逻辑),其他业务代码并没有致命问题。
(但最终问题还是因为一行代码引发的血泪事故,这告诫我们——不要忽略任何一行不起眼的代码)


2.日志

除了零碎的几个NPM异常,并没有其他的大量错误日志。


3.JVM(调试三件套:IDEA + arthas + jProfiler)

此处涉及大量的底层知识,如果有些你看不懂,证明你该学习了。

3.1 堆内存

起初的堆内存配置是这样的:

java -jar
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=1899
-Dcom.sun.management.jmxremote.ssl=false
-Xms1024m -Xmx6144m -Xmn2048m -Xss2048K
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m

通过Arthas的memory命令监控得到:
在这里插入图片描述

持续监控,发现堆内存的survivor区域持续100%,也就是新生代在不停的进行ParaNewGC,以至于大对象会随着GC次数的增加逐渐进入老年代,而老年代用CSM收集器再会进行回收。

在这里插入图片描述

我画了一下此时的JVM内存结构图:

在这里插入图片描述
这里的话,有一个问题就是初始堆内存分配的1024M(-Xms1024m),有点小的其实。既然Pod已经分配了4~8个g内存,干脆直接把起始内存分配的大一些。通过实践,发现确实有所改善,但并不是引发事故的主要原因。



-Xms4096m -Xmx8000m -Xmn2048m -Xss2048K -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m



3.2 线程

通过arthas的thread --state BLOCKED命令查看阻塞线程,发现确实每隔几秒钟就有几个线程处于BLOCKED阻塞状态,其中占用CPU资源最大的就是Finalizer线程。
在这里插入图片描述
那么什么是Finalizer线程?它都做了什么事情?
https://binkery.com/archives/362.html

其实通过这里,其实已经可以定位到是由于GC大量消耗CPU造成的资源紧张了。

顺着上面的思路,我们生成几份JVM堆内存快照,拷贝下来进行分析:
arthas生成内存快照命令:

heapdump --live /root/jvm.hprof

这里有一个Tips,就是如果你拉下来了大量不同间隔时间内存快照,怎么知道优先分析哪个?
原则是——优先选择文件最大的那个
因为文件越大,证明占用内存越多,越容易得到更多的信息。

通过JProfiler利器可以对快照进行分析:
异常Pod和正常Pod的快照实例数对比,发现异常Pod的Finalizer类很多,相差数倍:
在这里插入图片描述
在这里插入图片描述

继续分析Finalizer这个对象,发现它大量都与JarFile这个类相关联,而JarFile是做什么的呢?
它是spring boot 对JDK的JarFile的一种拓展,用于我们的spring boot Jar包运行时,把包中的依赖包加载到内存中,通过spring boot的类加载器进行加载。

在这里插入图片描述
在这里拓展一些知识:
就是spring boot 打包后的结构(你可以自己解压一个jar包试试):

project/
├── BOOT-INF/
│ ├── classes # 当前项目结果文件放置在 classes 路径下
│ │ │ └── application.properties # 项目中配置文件
│ │ ├── org/ # 项目中 java 路径下,编译成 class 文件路径
│ │ ├── static/ # 项目中 resources 路径下的静态文件夹
│ │ └── templates/ # 项目中 resources 路径下的模板文件夹
│ └── lib/ # 项目所依赖的第三方 jar(Tomcat,SpringBoot 等)
├── META-INF/
│ └── MANIFEST.MF # 清单文件,用于描述可执行 jar 的一些基本信息
└── org/springframework/boot/loader/ # jar 包启动相关的引导
├── archive/
├── data
├── ExectableArchiveLauncher.class
├── jar/
├── JarLauncher.class
├── LaunchedURLClassLoader.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── util/
└── WarLauncher.class

MANIFEST.MF结构:

Manifest-Version: 1.0 # 清单版本号
Start-Class: org.incoder.start.SpringbootStartApplication # 项目 main 方法所在的类
Spring-Boot-Classes: BOOT-INF/classes/ # 项目相关代码在打包后 jar 中的路径
Spring-Boot-Lib: BOOT-INF/lib/ # 项目中所依赖的第三方 jar 在打包后 jar 中的路径
Spring-Boot-Version: 2.1.6.RELEASE # 项目 SpringBoot 版本
Main-Class: org.springframework.boot.loader.JarLauncher # 当前 jar 文件的执行入口类(main 方法所在的类)
回车换行(在清单文件中,必须有,否则会出错)

再拓展一个知识,spring boot的启动流程:
在这里插入图片描述
要知道我们平时启动一个普通的jar都是通过jdk自带的三种类加载器进行『双亲委派』加载的,
不同的内置classloader的scope 如下:


  • Bootstrap ClassLoader(加载JDK的/lib目录下的类)
  • Extension ClassLoader(加载JDK的/lib/ext目录下的类)
  • Application ClassLoader(程序自己classpath下的类)

但是spring-boot为了适配fatjar结构,只能打破『双亲委派加载原则』,通过使用spring-boot自定义的类加载器加载我们的程序依赖的Jar包。

通过对比正常和异常的service.jar包解压后的文件,基本定位到该问题就是spring-boot-loader导致的了:
在这里插入图片描述
随后我下来了spring-boot的源码,通过对比spring-boot-loader的 2.3.2 和 2.3.4(这个版本正常)版本提交记录,发现了以下问题(可以在github 的issue中搜22991):
在这里插入图片描述
到这里基本确定是JarFile 内存泄露导致的了,且spring-boot在2.3.4版本进行了修复,但值得注意的是,在JDK 11中仍然会存在问题(还好我们用的是JDK8)。

但你以为到这里就分析结束了吗?作为一名程序员,一定要有刨根问底的不懈精神!

于是,我在本地进行了问题复现:
方式也很简单,在service服务中引入 spring-boot-loader源码,通过JarLauncher类启动spring-boot项目,然后模拟大量的请求发送,观察JVM状态,通过捕捉内存快照,果然复现了问题。
在这里插入图片描述
在这里插入图片描述
通过github的issue得知,是加载了不存在的类造成的内存泄露,那我们去查查哪些类没被加载到:
https://github.com/spring-projects/spring-boot/issues/28042
在这里插入图片描述
于是,顺着思路查一下哪些类没有被加载到:
通过,调试源码发现,spring-boot不会对类加载进行异常捕获,于是我手动加上一个try cache,运行。

在这里插入图片描述
果然发现了端倪:
在这里插入图片描述
这个类是从阿里巴巴的druid连接池中调用的:
于是跟踪到druid的源代码,发现这里捕获了异常,但是又不打印异常,你说难受不难受,要我们都没发现有报错信息呢。
在这里插入图片描述
于是再往上跟踪,发现是这里调用的上述方法,这个方法是获取最后一个包发送的时间,用来判断是否需要断开与数据库的连接,为了适配不同的 MySQL 版本,这里先加载第一个类,如果加载不到就加载第二个类。

在这里插入图片描述
通过调试,我终于发现了这里的大坑:
我们的mysql驱动是6.0.3,理论上应该能找到第二个类并加载

在这里插入图片描述
但是!
6.0.3驱动包中的MysqlConnection类名称确实正确,可是包路径却不一致。

在这里插入图片描述
在这里插入图片描述
至此,终于定位到问题的根因,解决办法是把
MySQL驱动包升到8.0.21版本,同时把spring-boot-maven-plugin 升级到最新2.7.5



mysql
mysql-connector-java
8.0.21


org.springframework.boot
spring-boot-maven-plugin
2.7.5

com.hualala.report.HualalaReportServiceApplication




repackage




至此,感谢运营和运维同学的及时提醒,感谢每个后端小伙伴的辛苦付出!
希望在今后的日子里,希望我们能慎重对待每一行代码!🎉🎉🎉🎉🎉







推荐阅读
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • ArcBlock 发布 ABT 节点 1.0.31 版本更新
    2020年11月9日,ArcBlock 区块链基础平台发布了 ABT 节点开发平台的1.0.31版本更新,此次更新带来了多项功能增强与性能优化。 ... [详细]
  • Spring Boot与Graylog集成实现微服务日志聚合与分析
    本文介绍了如何在Graylog中配置输入源,并详细说明了Spring Boot项目中集成Graylog的日志聚合和分析方法,包括logback.xml的多环境配置。 ... [详细]
  • 探索将Python Spyder与GitHub连接的方法,了解当前的技术状态及未来可能的发展方向。 ... [详细]
  • 调试利器SSH隧道
    在开发微信公众号或小程序的时候,由于微信平台规则的限制,部分接口需要通过线上域名才能正常访问。但我们一般都会在本地开发,因为这能快速的看到 ... [详细]
  • 在尝试启动Java应用服务器Tomcat时,遇到了org.apache.catalina.LifecycleException异常。本文详细记录了异常的具体表现形式,并提供了有效的解决方案。 ... [详细]
  • 本文将详细介绍如何配置并整合MVP架构、Retrofit网络请求库、Dagger2依赖注入框架以及RxAndroid响应式编程库,构建高效、模块化的Android应用。 ... [详细]
  • Kubernetes Services详解
    本文深入探讨了Kubernetes中的服务(Services)概念,解释了如何通过Services实现Pods之间的稳定通信,以及如何管理没有选择器的服务。 ... [详细]
  • 2023年1月28日网络安全热点
    涵盖最新的网络安全动态,包括OpenSSH和WordPress的安全更新、VirtualBox提权漏洞、以及谷歌推出的新证书验证机制等内容。 ... [详细]
  • Spring Security基础配置详解
    本文详细介绍了Spring Security的基础配置方法,包括如何搭建Maven多模块工程以及具体的安全配置步骤,帮助开发者更好地理解和应用这一强大的安全框架。 ... [详细]
  • 在尝试使用 Android 发送 SOAP 请求时遇到错误,服务器返回 '无法处理请求' 的信息,并指出某个值不能为 null。本文探讨了可能的原因及解决方案。 ... [详细]
  • 本文探讨了在使用JavaMail发送电子邮件时,抄送功能未能正常工作的问题,并提供了详细的代码示例和解决方法。 ... [详细]
  • spring(22)JdbcTemplate
    2019独角兽企业重金招聘Python工程师标准###1.导入jar包,必须jar包:c3p0、mysql-connector、beans、con ... [详细]
  • 在Java开发中,保护代码安全是一个重要的课题。由于Java字节码容易被反编译,因此使用代码混淆工具如ProGuard变得尤为重要。本文将详细介绍如何使用ProGuard进行代码混淆,以及其基本原理和常见问题。 ... [详细]
  • 本文介绍了如何使用开源工具ChkBugReport来解析和分析Android设备的Bugreport。ChkBugReport能够将复杂的Bugreport转换为易于阅读的HTML报告,并提供详细的图表和分析结论。 ... [详细]
author-avatar
手机用户2502928053
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有