2020年11月

诊断由于低效的代码引起的 CPU 突然增高

临近大促, 有同事发现某个应用在某些时候 CPU 的使用率很是怪异, 经常在某个水平上持续一段时间. 如下图:
cpu.png

图形看上去像叠罗汉一样, 看上去还很美观 (以后将会有专门的一篇讲各种奇怪的监控图形). 这里来解释一下为什么会出现这种情况: 其实这个是因为这个机器(虚拟机) 有 4 个 CPU 核心, 一旦有一个 CPU 被使用到 100%, 并且持续一段时间, 就会升到将近 25%, 保持一段直线. 若同时有第二个 CPU 被使用到 100%, 那么就保持到将近 50%, 以此类推, 最高到接近 100%. 同样原因, 如果有一个 CPU 从煎熬中解脱, 那么将掉 25% 左右.

一开始, 某个同事做了一段时间的 CPU profiling, 从 profiling 的结果看, CPU 花费在了这段代码上面:

"DefaultThreadPool-24" daemon prio=10 tid=0x00007fa85040e000 nid=0xfee9 runnable [0x00007fa849d4b000]
   java.lang.Thread.State: RUNNABLE
    at java.util.HashMap.get(HashMap.java:421)
    at java.util.Collections$UnmodifiableMap.get(Collections.java:1357)
    at java.util.AbstractMap.equals(AbstractMap.java:460)
    at java.util.Collections$UnmodifiableMap.equals(Collections.java:1394)
    at java.util.ArrayList.indexOf(ArrayList.java:303)
...  省略 ...

当时推测, 是不是又是并发使用 HashMap 导致的死循环?
仔细分析一下, 其实不是, 如果是死循环, 这个线程将永远不会退出来, 除非线程死掉, 可是我们看到某个机器的 CPU 会自动降下来. 于是他又推测, 是不是 HashMap 里面一个 bucket 里面太多了? 如果一个 bucket 里面太多, HashMap 将会根据元素的增多增加 bucket, 所以这也不是.

另外, 通过使用 htop 查看进程的使用情况, 也能看到其实燃烧 CPU 的线程过段时间就变掉.

那么, 如果某个代码某段时间内疯狂循环, 也能把 CPU 加热. 于是, 我们做了 heap dump 查看是不是这种情况. 从 heap dump 看, 上面 stack trace 里面的 ArrayList 有 95733 个元素, 而每个元素都是 HashMap, 每个 HashMap 大概有 10 个元素.
burnCPU.png

这种情况下, 会不会很耗 CPU 呢? 答案是当然: 首先我们看 ArrayList.indexOf() 方法怎么做? 它会遍历每个元素去调用他们的 equals() 方法. 平均对比元素个数为 N/2.

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

在看 HashMap 的 equals() 方法: 虽然做了很多快速判断, 但是我们的 case 里大概率还是要每个元素的 key 和 value 都做一次 equals() 对比.

public boolean equals(Object o) {
        if (o == this)
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

从上面的代码来看, 由于ArrayList 里大量的数据加上每个元素都是 HashMap,最终要花费大量的CPU 时间去做对比, 导致 CPU 在某个时间段过热.

使用 tcpkill 杀掉 tcp 连接

debian/Ubuntu 系列安装 tcpkill

sudo apt install dsniff -y

redhat/CentOS 系列安装 tcpkill

yum -y install dsniff -y

杀掉连接

sudo tcpkill -i eth0 -9 port 50185
sudo tcpkill ip host 10.168.1.1
sudo tcpkill -9 port 32145

查看连接

ss -t

解决 iPhone 连接 MAC 一直断断续续的问题

想把 iPhone 的照片导入到 Mac 上面, 插上之后, 一直连了断, 断了连. 充电的标志一下有, 一下无. 如果打开 photos.app 会突然看到一下连上, 马上断掉, 然后重新连上, 再次断掉.

一开始怀疑是充电线的问题, 因为有的充电线只有充电的线路, 没有数据线路. 但是这根线本来就是为了传数据买的, 并且时断时续, 跟线怎么摆放, 是不是弯曲没有任何关系, 说明很有可能不是线的问题.

于是去 google 了一下, 发现其他人也遇到类似的问题. 其中有一个官方问答里面提到了 usbd 的进程. 于是打开了 Mac 的 monitor, 确实看到了 usbd 进程. 于是强制退出这个进程, 然后重新插上 iPhone, 然后 iPhone 提示是不是信任这个设备, 点击确定, 问题解决.

Activity_Monitor__All_Processes_.png

如果上面这种方式还是无法解决, 那么先把手机拔下来, 然后打开命令行, 重复执行下面的命令每10秒钟执行一次, 执行10几次,然后再插入 iPhone, 在继续执行10几次, 就应该可以了

sudo killall -STOP -c usbd

kill.png

由于频繁 JAXBContext.newInstance 导致应用程序变慢

最近有个应用开始接受一种新的service请求, 请求是由一个 batch 应用触发, 在短时间内产生大量请求, 导致整个应用多个方面出现问题, 比如平均延迟增加, CPU 几乎 100%, GC overhead 在 60% 上下, tomcat 允许的服务线程数达到上限, error 大量增多, timeout 增多.

从已有的数据看, GC overhead 占用 60% 左右, 说明还有 40% 是其它代码占用的. 并且 GC overhead 最终是由引用代码引起的, 从 GC 日志看, heap 总是可以回收, 只是回收慢一些, 说明如果修改了代码的问题, GC 问题大概率会自动修复. 一般由于 transaction time 增加, 会导致停留在内存的短暂数据增加, 导致 GC overhead 升高. 此时, CMS, G1 等 GC 算法之前积累的经验(何时开始回收的时间点) 不适用新的情况.

通过 async-profiler 做出 CPU 的使用火焰图, 发现占用 CPU 40% 的代码绝大部分都是在处理 javax/xml/bind/JAXBContext.newInstance 的事情. 如下:
JAXBContextNewInstance.png

关于为什么 JAXBContext.newInstance 会导致 CPU 比较高的问题, 其实互联网上已经有很多这样的问题: https://www.ibm.com/support/pages/jaxbcontext-initialization-takes-long-time

引用这篇文章里面的:

JAXB context (javax.xml.bind.JAXBContext) object instantiation is a resource intensive operation. JAXB Context instantiation involves the pre-load and pre-creation of contexts (called the pre-caching process) of all packages and classes associated with the context, and then all of the packages and classes which are statically (directly and indirectly) referenced from those. Performance latency will correlate with the number of classes which are passed during JAXB creation during this pre-caching process.

一般对于这种 xxxContext 对象, 都是某个过程, 某个环境只有一个的对象, 对于这个问题当中, 每个请求都尝试创建一个 Request level 的 JAXBContext, 导致应用出现问题.