---
title: "三分钟解决 Java 进程 cpu 100% 问题"
description: "起因上午有小伙伴说某个 java 服务响应特别慢，看了一下监控，很多请求都超时了，服务是活着的，但响应非常慢。"
date: 2024-08-22T08:04:00Z
canonical: https://xiaobox.github.io/p/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/
author: 小盒子
categories: ["系统底层"]
tags: ["Java", "Docker", "架构"]
source: https://mp.weixin.qq.com/s/v67EJXK4zkd3ZSxxR2NNtw
license: CC BY-NC-SA 4.0
license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/
---

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

## 起因

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

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

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

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

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

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

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

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

## 问题解决过程

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

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

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

`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 进程的线程堆栈

`jstack -l 1 > jstack.log   `

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

### 分析 threadDump

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

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

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/001-55d83854.png)

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

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

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/002-ca3860fe.png)

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

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

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2024-08-22-san-fen-zhong-jie-jue-java-jin-cheng-cpu-100-wen-ti/003-35de550a.png)

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

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

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

## 最后

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

