三分钟解决 Java 进程 cpu 100% 问题

起因

上午有小伙伴说某个 java 服务响应特别慢,看了一下监控,很多请求都超时了,服务是活着的,但响应非常慢。

由于服务是多容器负载均衡调度部署的,又花了一点时间定位到具体的宿主机。仔细查下来发现,并不是部署该服务的所有容器都有问题,仅仅是某一个有问题。

具体的问题是某 java 进程的 cpu 占用率 打满了,是 100% 。

这里要跟大家说的是,如果某关键服务的所有副本全部出问题了,那么第一时间并不是揪着问题不放,跟那儿使劲查。而是先尝试恢复服务,如果系统在架构上的弹性和容错做的比较好,可能不会造成 “雪崩”,导致全系统异常,无法使用。但是注意,我说的是“关键服务”,就算你隔离、熔断做的再好,关键服务不能用了,整个系统还是不能用,对用户来说就是 “挂了” ,比如你出地铁想扫个共享单车,扫码没问题,但就是开不了锁(开锁服务出问题了)。

对于我们今天遇到的问题来说,还好,没那么严重,观察下来只是某服务的某副本出了问题。这里我根据系统的具体情况和观察到的情况做了个决定:先不回滚服务,一边观察一边收集事故现场信息,同时让一位同事准备好,如果事故进一步恶化就立刻回滚。

其实就算 “回滚” 也是有问题的,因为如果要考虑周全的话,仔细一想就会有一些问题,比如:

  • 回滚单个服务,看起来可行,但实际上问题多多,因为一般情况下,一次 “上线”,会涉及很多服务,相关的上下游都可能会有所牵涉,你如果只单单的回滚了出问题的那个服务 ,其他相关服务可还在最新状态呢,到时候又会引发新的问题。除非你对这个服务了如指掌,非常清楚迭代的内容,也能够判断出回滚的影响可控,否则只回滚单个服务,在大型分布式系统中是有风险的。
  • 版本回滚,这也是一些公司的常规做法。尤其是出了比较严重的问题,那么一次上线的全部内容都要完整地回滚,而不仅仅是回滚出问题的那个服务。但这也比较麻烦,因为首先 “全部回滚” 的时长可能比较长,时间一长对用户的影响就比较大。另外,回滚后还要处理从上线到回滚完成这段时间以来产生的 “脏数据”。总之复杂度高了,要考虑的问题也多。

好了,关于回滚,我这里就点到为指。总而言之,还是要根据你系统的具体情况来决定。谨慎、灵活地处置。

问题解决过程

上文我说问题只要服务的某个副本上出了问题,你可能会觉得不对,代码都一样的,怎么可能就只有一个副本有问题呢?

所以先解释下原因,最后事故处理完我们分析代码发现,原来是某个特殊的用户在某个特殊的条件下触下了某个 bug。那部分代码确实只会在这么特殊的条件下才会运行,而且这个动作的执行频率相当低。所以就是这么巧。

我们进入正题,首先在宿主机上看到某 java 进程的 CPU 占用率是 100% 。由于我们所有的 java 进程全部是 docker 容器启动的,所以要找到具体是宿主机的哪个 docker 容器,这里我用了这个命令:

整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

perl

代码解读

复制代码

docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Id}}' | grep 2880

假设在宿主机上的这个 java 进程的 pid 为 2880。这行命令就是找出 pid 对应的具体是哪个 docker 容器。详解说明一下:

  • docker ps 是用来列出当前运行中的 Docker 容器的命令。 -q 选项告诉 docker ps 仅输出每个容器的 ID,而不是完整的容器信息列表。
  • |(管道操作符):管道操作符用于将前一个命令的输出作为下一个命令的输入。这里,它将 docker ps -q 命令输出的容器 ID 列表传递给下一个命令。
  • xargs docker inspect --format '{{.State.Pid}}, {{.Id}}':xargs 是一个将输入数据转换为其他命令行参数的工具。在这里,它将 docker ps -q 输出的容器 ID 列表转换为 docker inspect 命令的参数。
  • docker inspect 是用于获取有关 Docker 容器的详细信息的命令。--format '{{.State.Pid}}, {{.Id}}' 是一个格式化选项,它指定了 docker inspect 命令的输出格式。这里使用了 Go 模板语法来指定只输出每个容器的进程 ID(PID)和容器 ID。
  • |(管道操作符):再次使用管道操作符,将 docker inspect 命令的输出传递给下一个命令。
  • grep 2880:grep 是一个文本搜索工具,用于搜索包含特定模式的字符串。2880 是要搜索的字符串,代表一个进程 ID(PID)。这个命令会从输入中筛选出包含数字 2880 的行。

将所有部分结合起来,这行命令的作用是:首先,列出所有运行中的 Docker 容器的 ID。 然后,对每个容器 ID 执行 docker inspect,以获取每个容器的 PID 和 ID,并以特定格式输出。最后,从这些输出中筛选出 PID 为 2880 的容器,并显示其 PID 和 ID

到这里我们知道了具体是哪个容器,然后进入到该容器中,查看了一下 cpu 的使用情况,确实很高,接着使用以下命令导出 java 进程的线程堆栈

 

c

代码解读

复制代码

jstack -l 1 > jstack.log

java 进程的 pid 为 1 ,所以上面命令中 1 就是 java 进程的进程 pid

分析 threadDump

用 jstack 命令导出 java 进程的线程堆栈快照后就可以进行分析了。

这里我们使用在线的工具网站来分析:fastthread.io/

我个人觉得是最好用的一个。你可能会问有没有本地的好用的工具。我找了一圈,有是有,但很一般,还不如人家这个在线网站呢。

接着我们反 jstack.log 打个包上传上去。很快就会得到分析结果,会出一个关于这个 Thread Dump 的全方面分析报告,方便你定位相关问题。

如果有异常会有红号感叹号提示。

除了人家提示的明显异常,我一般重点会看有没有死锁,以及哪些线程占用的 CPU 较高:

剩下的就凭借你对系统的了解和经验作出判断了。

具体到我们的情况,我们分析了占 CPU 最高的两个线程,全部跟我们自己写的代码有关。具体来说有一个线程的代码执行的是一个递归操作。出问题的就是这部分代码。

找到原因后,很快我们就修复了 bug。

最后

要说简单也简单,如果你的思路是对的,整个处理过程不超过 3 分钟。但如果你的思路有问题,找到问题的时间就可能会无限长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值