一次生产 CPU 100% 排查优化实践
前言
到了年底果然都不太平,最近又收到了运维报警:表示有些服务器负载非常高,让我们定位问题。
还真是想什么来什么,前些天还故意把某些服务器的负载提高(没错,老板让我写个 BUG!),不过还好是不同的环境互相没有影响。
定位问题
拿到问题后首先去服务器上看了看,发现运行的只有我们的 Java 应用。于是先用 ps
命令拿到了应用的 PID
。
接着使用 top -Hp pid
将这个进程的线程显示出来。输入大写的 P 可以将线程按照 CPU 使用比例排序,于是得到以下结果。
果然某些线程的 CPU 使用率非常高。
为了方便定位问题我立马使用 jstack pid > pid.log
将线程栈 dump
到日志文件中。
我在上面 100% 的线程中随机选了一个 pid=194283
转换为 16 进制(2f6eb)后在线程快照中查询:
因为线程快照中线程 ID 都是16进制存放。
发现这是 Disruptor
的一个堆栈,前段时间正好解决过一个由于 Disruptor 队列引起的一次 OOM:强如 Disruptor 也发生内存溢出?
没想到又来一出。
为了更加直观的查看线程的状态信息,我将快照信息上传到专门分析的平台上。
其中有一项菜单展示了所有消耗 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。
代码如下:
初步看来和这个等待策略有很大的关系。
本地模拟
为了验证,我在本地创建了 15 个 Disruptor
队列同时结合监控观察 CPU 的使用情况。
创建了 15 个 Disruptor
队列,同时每个队列都用线程池来往 Disruptor队列
里面发送 100W 条数据。
消费程序仅仅只是打印一下。
跑了一段时间发现 CPU 使用率确实很高。
同时 dump
线程发现和生产的现象也是一致的:消费线程都处于 RUNNABLE
状态,同时都在执行 yield
。
通过查询 Disruptor
官方文档发现:
YieldingWaitStrategy 是一种充分压榨 CPU 的策略,使用
自旋 + yield
的方式来提高性能。 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。
同时查阅到其他的等待策略 BlockingWaitStrategy
(也是默认的策略),它使用的是锁的机制,对 CPU 的使用率不高。
于是在和之前同样的条件下将等待策略换为 BlockingWaitStrategy
。
和刚才的 CPU 对比会发现到后面使用率的会有明显的降低;同时 dump 线程后会发现大部分线程都处于 waiting 状态。
优化解决
看样子将等待策略换为 BlockingWaitStrategy
可以减缓 CPU 的使用,
但留意到官方对 YieldingWaitStrategy
的描述里谈道: 当消费线程(Event Handler threads)的数量小于 CPU 核心数时推荐使用该策略。
而现有的使用场景很明显消费线程数已经大大的超过了核心 CPU 数了,因为我的使用方式是一个 Disruptor
队列一个消费者,所以我将队列调整为只有 1 个再试试(策略依然是 YieldingWaitStrategy
)。
跑了一分钟,发现 CPU 的使用率一直都比较平稳而且不高。
总结
所以排查到此可以有一个结论了,想要根本解决这个问题需要将我们现有的业务拆分;现在是一个应用里同时处理了 N 个业务,每个业务都会使用好几个 Disruptor
队列。
由于是在一台服务器上运行,所以 CPU 资源都是共享的,这就会导致 CPU 的使用率居高不下。
所以我们的调整方式如下:
- 为了快速缓解这个问题,先将等待策略换为
BlockingWaitStrategy
,可以有效降低 CPU 的使用率(业务上也还能接受)。 - 第二步就需要将应用拆分(上文模拟的一个
Disruptor
队列),一个应用处理一种业务类型;然后分别单独部署,这样也可以互相隔离互不影响。
当然还有其他的一些优化,因为这也是一个老系统了,这次 dump 线程居然发现创建了 800+ 的线程。
创建线程池的方式也是核心线程数、最大线程数是一样的,导致一些空闲的线程也得不到回收;这样会有很多无意义的资源消耗。
所以也会结合业务将创建线程池的方式调整一下,将线程数降下来,尽量的物尽其用。
本文的演示代码已上传至 GitHub:
https://github.com/crossoverJie/JCSprout
你的点赞与分享是对我最大的支持
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
源码分析 Mybatis 的 foreach 为什么会出现性能问题
背景 最近在做一个类似于综合报表之类的东西,需要查询所有的记录(数据库记录有限制),大概有1W条记录,该报表需要三个表的数据,也就是根据这 1W 个 ID 去执行查询三次数据库,其中,有一条查询 SQL 是自己写,其他两条是根据别人提供的接口进行查询,刚开始的时候,没有多想,直接使用 in 进行查询,使用 Mybatis 的 foreach 语句;项目中使用的是 jsonrpc 来请求数据,在测试的时候,发现老是请求不到数据,日志抛出的是 jsonrpc 超时异常,继续查看日志发现,是被阻塞在上面的三条SQL查询中。 在以前分析 Mybatis 的源码的时候,了解到,Mybatis 的 foreach 会有性能问题,所以改了下 SQL,直接在代码中拼接SQL,然后在 Mybatis 中直接使用 # 来获取,替换class 测试了下,果然一下子就能查询出数据。 前提 这里先不考虑使用 in 好不好,如何去优化 in,如何使用exists 或 inner join 进行代替等,这里就只是考虑使用了 in 语句,且使用了 Mybatis 的 foreach 语句进行优化,其实 foreach...
- 下一篇
Docker 异常总结
提起容器,大家可能首先想到的是 Docker,Docker 已经当之无愧的成为容器界巨头。如果你使用 Kubernetes 作为私有云的解决方案,Docker 也是首选的容器解决方案。虽然 Docker 很优秀,但 Docker 并不是完美的,甚至存在很多问题。下面介绍我们下在生产环境中遇到的关于 Docker 的一些问题及排查过程。避免大家再踩坑。 异常一 docker ps 无响应, Node 节点表现为 NotReady。 运行信息 $ docker -v $ Docker version 17.03.2-ce, build f5ec1e2 $ docker-containerd -v $ containerd version 0.2.3 commit:4ab9917febca54791c5f071a9d1f404867857fcc $ docker-runc -v $ runc version 1.0.0-rc2 $ commit: 54296cf40ad8143b62dbcaa1d90e520a2136ddfe $ spec: 1.0.0-rc2-dev 启用 Docker ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2全家桶,快速入门学习开发网站教程