前言

记得大三下学期背八股准备面试的时候,什么vmstatpidstatmatjstatjstackJProfiler(付费),背得滚瓜烂熟,当时也用阿里云买的Linux学生机上手过,但如今系统换成Mac,排查一个CPU占用高的问题,却在第一步卡住了。。。

原因是有些Linux系统命令在Mac上没有替代品,于是只能跳过一些筛选命令,靠肉眼去筛选了,好在应用的线程不多,基本dump线程快照下来看一遍就抓住主干所在了。


问题发现

  • 最近在维护一个「云真机平台Sonic」,在调试的过程中发现一旦进入一个手机的远控,CPU的占用100%,当远控两个手机时,CPU就到200%,如下面视频所示:

  • 很明显,每远控一台手机就消耗一个逻辑核的算力,假如我这台电脑是4个核4线程,那么只要同时远控4台手机,那么电脑的散热就直接呼呼响了,万一使用的用户电脑烧掉了,我可承担不起,于是立马就开始排查了= =、

开始排查

Mac 命令阉割问题

  • 如果你曾经在公司的线上 or 测试环境尝试排查这种问题,那么我相信你第一步一定是先执行top -Hp <pid>,但很遗憾的是在mac上执行这个这个命令,你只会得到没什么卵用的帮助提示,如下图所示👇🏻:
  • image.png
  • 在Mac上top -Hp <pid>是没有任何替代命令的,除非你自己写个代码去获取,但对于临时排查问题来说,这实在太麻烦了。

  • 之后,我就想:害,这条命令不能用就算了,试着打印下线程id,于是执行printf '%x\n' 37109👇🏻:
  • image.png
  • 哦!看起来成功得到了一个线程id -> 90f5了,那么用jstack命令能不能获取到线程栈呢?于是我执行jstack 37109 | grep -A 20 90f5👇🏻:
  • image.png
  • 🐭🐭我啊,空欢喜一场🤣

这里可能有人就要问了:

  • Q:你为啥不打个jar,然后拿一台云上Linux调试呢?
  • A:那是因为这个项目的特殊性,部署这个服务的电脑必须连上真机才能被另一个server端远控,我总不能跑到云服务商去插手机。
  • Q:那为啥不用虚拟机呢?
  • A:我手上目前只有一台Mac,况且因为对于不同设备的远控,目前的策略是随机找一个可用的端口,如果用虚拟机实际上还要处理端口映射,很是麻烦。

靠jstack枚举

  • 既然用jstack过滤线程ID的法子不好使了,那就只能过滤别的字段了。
  • 一开始的思路是,出现CPU高负载,极大概率是因为你业务代码造成的,jdk源码、框架的逻辑可以先不管,于是我就尝试先远控一台手机,然后用jstack过滤含有应用包名的线程,即执行命令jstack 37109 | grep -A 20 com.sonic.agent > thread_info.txt
  • image.png
  • 之后我们打开dump出来的文件,查找cpu=关键字,很容易发现一个数据大得离谱的数据(jstack输出参考)👇🏻:
  • image.png
  • 此时问题已经相对明显了,从线程堆栈中我们可以获得这样的信息:在OutputSocketThread线程中,Thread.isAlive函数的执行频率非常高,那么接下来就是到代码中看看到底是什么原因导致这个函数频率出现异常了。

优化代码

  • 在上面排查出那个线程导致问题后,接下来我们找到OutputSocketThread的代码,发现以下这一段逻辑:
  • image.png
  • 很容易就可以发现,当sendImg.isAlive()返回值一直为true时,不管sendImg.getDataQueue()的返回值是否为空,线程都会一直不停的循环,抽象一下就相当于while(true){continue;},也难怪刚好消耗了一个逻辑核的算力。

  • 那么我们要怎么优化这段逻辑呢?
  • 首先我们要明确我们的需求,我们期望sendImg.getDataQueue()中有数据时,线程才动起来;而没数据的时候不需要动。说白了就是我们需要一个消费订阅者的模型,而非不断轮训。
  • 说到订阅-消费这就巧了,Java中的BlockingQueue正好就满足了这个模型,而我们的返回值现在刚好又是Queue,也就是说,我们只要更改下原有的实现类就可以完整优化了,核心改动如下:
  • image.png

  • 优化完成,让我们来重启代码,看看现在远控两台手机CPU的状况如何(最后点下设置是为了表示代码更改后远控功能还是正常的)👇🏻:

  • 可见CPU的内心毫无波动,远控两台手机CPU占用从之前的200%降到20%不到。

小结

  • Mac上的排查方式可能会与Linux有些许差异,比如在Mac上Top命令经过阉割,想用printf "%x\n <pid>也没用,这时候思路要有个小切换,即想办法用jstack过滤其它维度的关键信息,比如我认为问题大概率出在业务代码上,那么就可以通过过滤包名获取关键堆栈信息。
  • 对于一些需要不断循环读取数据的场景,应该明确代码的暂停条件,不能让其造成cpu空转损耗性能。比如当前不断读取二进制流的场景,当CPU的指令调度周期远短于数据生产的周期时,就应该用生产-订阅模型,等有数据了才动起来消费,不让CPU白白空转。