提交 c9292f3a 编写于 作者: 沉默王二's avatar 沉默王二 💬

jvm 增加两篇实战

上级 6b53c2e5
......@@ -220,6 +220,8 @@
- [图解Java的垃圾回收机制](docs/jvm/tujie-gc.md)
- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](docs/jvm/problem-tools.md)
- [Java即时编译(JIT)器原理解析及实践](docs/jvm/jit.md)
- [一次内存溢出排查优化实战](docs/jvm/oom.md)
- [一次生产CPU 100% 排查优化实践](docs/jvm/cpu-percent-100.md)
- [JVM 核心知识点总结](docs/jvm/zongjie.md)
......
......@@ -272,6 +272,8 @@ export const sidebarConfig = defineSidebarConfig({
"tujie-gc",
"problem-tools",
"jit",
"oom",
"cpu-percent-100",
"zongjie",
],
},
......
......@@ -231,6 +231,8 @@ headerDepth: 1
- [图解Java的垃圾回收机制](jvm/tujie-gc.md)
- [Java问题诊断和排查工具(查看JVM参数、内存使用情况及分析)](jvm/problem-tools.md)
- [Java即时编译(JIT)器原理解析及实践](jvm/jit.md)
- [一次内存溢出排查优化实战](jvm/oom.md)
- [一次生产CPU 100% 排查优化实践](jvm/cpu-percent-100.md)
- [JVM 核心知识点总结](jvm/zongjie.md)
## Java企业级开发
......
---
category:
- Java核心
- JVM
tag:
- Java
---
# 一次生产CPU 100% 排查优化实践
## 前言
最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。
## 定位问题
拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 `ps` 命令拿到了应用的 `PID`
接着使用 `top -Hp pid` 将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-e9b35104-fce9-40ea-ae91-8bbb7fd8aa96.jpg)
果然某些线程的 CPU 使用率非常高。
为了方便定位问题我立马使用 `jstack pid > pid.log` 将线程栈 `dump` 到日志文件中。
我在上面 100% 的线程中随机选了一个 `pid=194283` 转换为 16 进制(2f6eb)后在线程快照中查询:
> 因为线程快照中线程 ID 都是16进制存放。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-f8b051d5-f28d-481e-a0b2-e97151797e3b.jpg)
发现这是 `Disruptor` 的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 [OOM]():[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/)
没想到又来一出。
为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。
[http://fastthread.io/](http://fastthread.io/)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-d6c9bc1c-9600-47f2-9ff1-d0c9bd8ef849.jpg)
其中有一项菜单展示了所有消耗 CPU 的线程,我仔细看了下发现几乎都是和上面的堆栈一样。
也就是说都是 `Disruptor` 队列的堆栈,同时都在执行 `java.lang.Thread.yield` 函数。
众所周知 `yield` 函数会让当前线程让出 `CPU` 资源,再让其他线程来竞争。
根据刚才的线程快照发现处于 `RUNNABLE` 状态并且都在执行 `yield` 函数的线程大概有 30几个。
因此初步判断为大量线程执行 `yield` 函数之后互相竞争导致 CPU 使用率增高,而通过对堆栈发现是和使用 `Disruptor` 有关。
## 解决问题
而后我查看了代码,发现是根据每一个业务场景在内部都会使用 2 个 `Disruptor` 队列来解耦。
假设现在有 7 个业务类型,那就等于是创建 `2*7=14``Disruptor` 队列,同时每个队列有一个消费者,也就是总共有 14 个消费者(生产环境更多)。
同时发现配置的消费等待策略为 `YieldingWaitStrategy` 这种等待策略确实会执行 yield 来让出 CPU。
代码如下:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-49840c0d-2c10-4bcb-80c6-1df7553ddb6c.jpg)
> 初步看来和这个等待策略有很大的关系。
### 本地模拟
为了验证,我在本地创建了 15 个 `Disruptor` 队列同时结合监控观察 CPU 的使用情况。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-7f3b2fa6-6505-4b67-9f42-0170a236832b.jpg)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-d597089d-54e0-49ef-a0f9-41798e84de48.jpg)
创建了 15 个 `Disruptor` 队列,同时每个队列都用线程池来往 `Disruptor队列` 里面发送 100W 条数据。
消费程序仅仅只是打印一下。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-97b88b4d-2d81-47ab-9beb-830ac122c282.jpg)
跑了一段时间发现 CPU 使用率确实很高。
---
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-c0ee1da2-29af-4581-b0d8-97f6250401e7.jpg)
同时 `dump` 线程发现和生产的现象也是一致的:消费线程都处于 `RUNNABLE` 状态,同时都在执行 `yield`
通过查询 `Disruptor` 官方文档发现:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-de904a90-8b59-4333-82f5-9ec94a6525a0.jpg)
> YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用`自旋 + yield`的方式来提高性能。
> 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。
---
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-3faf6f7e-0d2c-4cfe-8e3a-07e15601485d.jpg)
同时查阅到其他的等待策略 `BlockingWaitStrategy` (也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。
于是在和之前同样的条件下将等待策略换为 `BlockingWaitStrategy`
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-12912ce3-a702-4bb2-a19b-816c22f7d43a.jpg)
---
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-b4aad83e-af9d-48fc-bcd0-ad2a42588179.jpg)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-56dc1513-8f10-422f-bb2a-ae5dcfb8413f.jpg)
和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。
### 优化解决
看样子将等待策略换为 `BlockingWaitStrategy` 可以减缓 CPU 的使用,
但留意到官方对 `YieldingWaitStrategy` 的描述里谈道:
当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。
而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 `Disruptor` 队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 `YieldingWaitStrategy`)。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-b1cbc2c2-828a-46e8-ba14-86cd0fa660c6.jpg)
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/cpu-percent-100-f8fb7682-a61a-407d-923c-890a16bce109.jpg)
跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。
## 总结
所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 `Disruptor` 队列。
由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。
所以我们的调整方式如下:
- 为了快速缓解这个问题,先将等待策略换为 `BlockingWaitStrategy`,可以有效降低 CPU 的使用率(业务上也还能接受)。
- 第二步就需要将应用拆分(上文模拟的一个 `Disruptor` 队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。
当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。
创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。
所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。
本文的演示代码已上传至 GitHub:
[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor)
**你的点赞与分享是对我最大的支持**
原文链接:[https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md](https://github.com/crossoverJie/JCSprout/blob/master/docs/jvm/cpu-percent-100.md)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
---
category:
- Java核心
- JVM
tag:
- Java
---
# 一次内存溢出排查优化实战
## 前言
`OutOfMemoryError` 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。
本文以最近碰到的一次线上内存溢出的定位、解决问题的方式展开;希望能对碰到类似问题的同学带来思路和帮助。
主要从`表现-->排查-->定位-->解决` 四个步骤来分析和解决问题。
## 表象
最近我们生产上的一个应用不断的爆出内存溢出,并且随着业务量的增长出现的频次越来越高。
该程序的业务逻辑非常简单,就是从 Kafka 中将数据消费下来然后批量的做持久化操作。
而现象则是随着 Kafka 的消息越多,出现的异常的频次就越快。由于当时还有其他工作所以只能让运维做重启,并且监控好堆内存以及 GC 情况。
> 重启大法虽好,可是依然不能根本解决问题。
## 排查
于是我们想根据运维之前收集到的内存数据、GC 日志尝试判断哪里出现问题。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-81051388-0c35-4de6-a3d9-4f546ef4bfec.jpg)
结果发现老年代的内存使用就算是发生 GC 也一直居高不下,而且随着时间推移也越来越高。
结合 jstat 的日志发现就算是发生了 FGC 老年代也已经回收不了,内存已经到顶。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-e79d4da0-fbb1-4918-a8d8-e29d2d64323b.jpg)
甚至有几台应用 FGC 达到了上百次,时间也高的可怕。
这说明应用的内存使用肯定是有问题的,有许多赖皮对象始终回收不掉。
## 定位
由于生产上的内存 dump 文件非常大,达到了几十G。也是由于我们的内存设置太大有关。
所以导致想使用 MAT 分析需要花费大量时间。
因此我们便想是否可以在本地复现,这样就要好定位的多。
为了尽快的复现问题,我将本地应用最大堆内存设置为 150M。
然后在消费 Kafka 那里 Mock 为一个 while 循环一直不断的生成数据。
同时当应用启动之后利用 VisualVM 连上应用实时监控内存、GC 的使用情况。
结果跑了 10 几分钟内存使用并没有什么问题。根据图中可以看出,每产生一次 GC 内存都能有效的回收,所以这样并没有复现问题。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-4cf05af0-924f-406b-a8a4-5aa885e38cea.jpg)
没法复现问题就很难定位了。于是我们 review 代码,发现生产的逻辑和我们用 while 循环 Mock 数据还不太一样。
查看生产的日志发现每次从 Kafka 中取出的都是几百条数据,而我们 Mock 时每次只能产生**一条**
为了尽可能的模拟生产情况便在服务器上跑着一个生产者程序,一直源源不断的向 Kafka 中发送数据。
果然不出意外只跑了一分多钟内存就顶不住了,观察左图发现 GC 的频次非常高,但是内存的回收却是相形见拙。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-a6d6c9cd-e79c-4a76-ba97-032cfefefd5f.jpg)
同时后台也开始打印内存溢出了,这样便复现出问题。
## 解决
从目前的表现来看就是内存中有许多对象一直存在强引用关系导致得不到回收。
于是便想看看到底是什么对象占用了这么多的内存,利用 VisualVM 的 HeapDump 功能可以立即 dump 出当前应用的内存情况。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-49b47ca3-b3e2-49f7-85c9-23f7a3ef6f93.jpg)
结果发现 `com.lmax.disruptor.RingBuffer` 类型的对象占用了将近 50% 的内存。
看到这个包自然就想到了 `Disruptor` 环形队列。
再次 review 代码发现:从 Kafka 里取出的 700 条数据是直接往 Disruptor 里丢的。
这里也就能说明为什么第一次模拟数据没复现问题了。
模拟的时候是一个对象放进队列里,而生产的情况是 700 条数据放进队列里。这个数据量是 700 倍的差距。
而 Disruptor 作为一个环形队列,再对象没有被覆盖之前是一直存在的。
我也做了一个实验,证明确实如此。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-dee49da6-905a-4085-b82e-41e136d422e8.jpg)
我设置队列大小为 8 ,从 0~9 往里面写 10 条数据,当写到 8 的时候就会把之前 0 的位置覆盖掉,后面的以此类推(类似于 HashMap 的取模定位)。
所以在生产上假设我们的队列大小是 1024,那么随着系统的运行最终肯定会导致 1024 个位置上装满了对象,而且每个位置是 700 个!
于是查看了生产上 Disruptor 的 RingBuffer 配置,结果是:`1024*1024`
这个数量级就非常吓人了。
为了验证是否是这个问题,我在本地将该值换为 2 ,一个最小值试试。
同样的 128M 内存,也是通过 Kafka 一直源源不断的取出数据。通过监控如下:
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/jvm/oom-5529781f-1f68-47a7-a3d2-04eba9e9d52e.jpg)
跑了 20 几分钟系统一切正常,每当一次 GC 都能回收大部分内存,最终呈现锯齿状。
这样问题就找到了,不过生产上这个值具体设置多少还得根据业务情况测试才能知道,但原有的 1024*1024 是绝对不能再使用了。
## 总结
虽然到了最后也就改了一行代码(还没改,直接修改配置),但这排查过程我觉得是有意义的。
也会让大部分觉得 JVM 这样的黑盒难以下手的同学有一个直观的感受。
`同时也得感叹 Disruptor 东西虽好,也不能乱用哦!`
相关演示代码查看:
[https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor](https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor)
**你的点赞与转发是最大的支持。**
原文链接:https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
......@@ -344,4 +344,4 @@ Java程序问题分析:jmap 分析堆内存、jstack 分析线程栈等,见
原文链接:https://www.cnblogs.com/z-sm/p/6745375.html
<img src="http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png" width="700px">
<img src="http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png">
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册