分类 Troubleshooting 相关 下的文章

如何获取 Java 应用 每个线程的 CPU usage

很多时候, 我们看到 Java 应用程序的 CPU 使用率很高, 或者是 GC 导致的, 或者是死循环导致的, 或者是执行某些特耗 CPU 的操作, 比如使用正则表达式匹配. 那么如何找这个使用率特别高的线程呢?


如何找出CPU 使用率最高的线程?

在 Linux 操作系统下, 使用 top -i 命令很容易找到CPU 使用率最高的进程, 如果使用 top -i -H 就很容易找到这个线程.

找到线程后, 如何查看它在忙什么呢?

使用 Linux 系统自带的 strace, ftrae, ltrace 等, 可以查看 OS 级别的系统调用, 函数, 库等调用, 可是无法查看用户空间的更多信息.

如何找出那些方法在消耗 CPU

通常有 2 种方法.

  1. 第一种是你大概知道那个方法耗时, 它可能是比较靠外层的方法, 你可以给他手工在方法开始和结束加一些代码, 然后计算时间差, 通过某些方式把时间差展现给你. 某些工具提供了一些面向方面(面向切面 Aspect Oriented Programing)的接口, 可以通过在线 unload 某些 class, 然后重新 load 加入新的切面的 class 来计算时间差, 比如 阿里开源的 Arthas;
  2. 通过 Sampling 的方式, 这种方法通过不断的对线程 stack 做 snapshot 然后画出火焰图. 通过火焰图, 很容易观察出那里占用了大量的 CPU 时间. 不过这里有个权衡, 多久做一个线程 stack 的 snapshot, 如果做的太多, 那么会影响CPU 的占用实际情况, 如果做的太少, 可能错过了最耗 CPU 的时间点, 或者每次正好错过这个点.

一个 Java 自带比较折中的方法.

在 Java 里, 我们可以通过程序的方式列出当前应用里面的所有正在运行的 Java 线程, 同时通过 MBean API 可以知道任何一个线程在开始之后占用的 CPU 时间. 如果我们在某个时间间隔的开始记录所有线程已经使用的时间, 在间隔的结尾再次记录, 那么就可以知道任何线程在这段时间内使用 CPU 的占用情况. 在这个间隔中间终止的线程, 不能给出正确的结果. 幸好大部分 Java 应用程序大部分使用线程池. 同时在间隔结尾处, 可以记录下线程栈的, 能大概给出一些信息. 如果一个线程长时间使用 CPU 很高, 可以做多次, 然后大概看出那部分代码比较耗 CPU.

如何具体实现

  1. 获得 ThreadMXBean
    ManagementFactory.getThreadMXBean();
  2. 获得线程 和 其它相关信息 ThreadMXBean
    首先通过 isThreadCpuTimeSupported() 方法获得是不是支持;
    可以获得死锁线程 findDeadlockedThreads()
    可以获得所有线程 ID: getAllThreadIds()
    可以获得一个线程已经使用的 CPU 时间: getThreadCpuTime(threadId)
    可以获得一个线程 Blocked 和 Waited time:
    ThreadInfo 的getBlockedTime() 和 getBlockedTime()
    ThreadInfo 的getWaitedTime() 和 getWaitedCount()
    可以通过 ThreadInfo 获得 stack -> getStackTrace()
  3. 其它要注意的地方
    ..1. 有几个 CPU -> Runtime.getRuntime().availableProcessors();
    ..2. 获得时间是 Nano time;
    ..3. 可以对使用率做百分比, 可以排序, 可以只看 Blocked 线程等.

java debug JPDA (JavaTM Platform Debugger Architecture)

Java 是在 VM 里面运行的语言, 同时要做到平台无关性, 所以它有自己的 debug 接口和实现.
官方关于 JPDA 的链接: https://docs.oracle.com/javase/6/docs/technotes/guides/jpda/
jpda.png

上面的图很容易理解, VM 具体实现和 backend 接口直接使用 JVM TI 来作为通信接口.
Debugger 和 Debuggee 之间定义了协议: JDWP (这类似于 HTTP).
Debugger 自己的和后端通信以及自己的 UI 使用 JDI(这类似与 HTML)

如何远程 debug:
从 Java 9 开始:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000 myApp
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 myApp

Java 9 之前:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication

Java 5 之前:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 myApp

参数列表:
a list of options:

  1. transport is the only fully required option. It defines which transport mechanism to use. dt_shmem only works on Windows and if both processes run on the same machine while dt_socket is compatible with all platforms and allows the processes to run on different machines
  2. server is not a mandatory option. This flag, when on, defines the way it attaches to the debugger. It either exposes the process through the address defined in the address option. Otherwise, JDWP exposes a default one
  3. suspend defines whether the JVM should suspend and wait for a debugger to attach or not
  4. address is the option containing the address, generally a port, exposed by the debuggee. It can also represent an address translated as a string of characters (like javadebug if we use server=y without providing an address on Windows)

客户端最简陋的可以使用 jdk 自带 jdb.
更多信息参考: https://www.baeldung.com/java-application-remote-debugging

Tomcat 7 NIO handling request on Linux

围绕这个图, 做几点解说(左边的虚线框表示 Tomcat):
Tomcat_Request_handling.png

  1. 从操作系统层面来说, 收到 TCP 的 SYN 之后, 放到 TCP SYN backlog 队列, 然后发送 (syn + ack) 包, 等待对方的 ack 包;
  2. SYN backlog 长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 设置, 还有文章说最终值有几个因素共同决定, 不过在 Linux Kernel 4.3 之后,这个 syn backlog 不在由 net.ipv4.tcp_max_syn_backlog 决定, 而是由 net.core.somaxconn 决定

    eric@host:~$ sysctl net.core.somaxconn
    net.core.somaxconn = 4096
    eric@host:~$ sysctl net.ipv4.tcp_max_syn_backlog
    net.ipv4.tcp_max_syn_backlog = 4096
  3. 如何查看一个监听端口的 SYN backlog 当前的队列长度?
    没有直接看当前长度的命令, 不过可以自己手工计算:

    # 查看连到当前 host 8080 端口上并且处于 sync-recv 状态的连接
    eric@host:~$ ss -n state syn-recv sport = :8080
  4. 如果 SYN backlog 已经满了, 那么操作系统层面会自动丢弃接收到的 syn 包, 那么客户端以为对端没收到, 会继续尝试.
  5. 当操作系统回复 syn+ack 之后, 对端返回 ack, 完成三次包握手, 这时, 这个连接变成 Established 的状态, 该连接从 syn backlog 进入 listen backlog;
  6. listen backlog 的长度由 listen 系统调用的参数决定, 在 Java 里由 ServerSocket(int port, int backlog)的构造函数的 backlog 参数决定. java 默认是 50. Tomcat 7 里面由 Connector 参数 acceptCount 决定, 默认是 100;
  7. 如果这个 listen backlog 满了, OS 收到对端为了完成握手的 ack 时, 选择 ignore 这个 ack, 那么客户端会以为丢包, 会继续尝试发送 ack;
  8. 如何查看一个监听端口的 listen backlog 队列的当前长度? Linux -> ss -ltn 看 Send-Q 列
    xiatian_lvsbastion200_____ssh_.png
  9. Tomcat 7 NIO 处理这些 socket 连接有 2 类 background 线程, 分别是 Acceptor 线程和 Poller 线程;
  10. Acceptor 线程 通过 serverSock.accept() 方法接受新连接, 就是 listen backlog 里面已经建立的连接;
  11. Acceptor 接受之后, 就把这个接受的 socket 送给 Poller 线程去处理;
  12. Poller 线程把 socket 封装之后放入 TaskQueue;
  13. Poller 线程还负责 从 socket 的 Channel 搬运数据到对应的 Buffer, 也就是负责 NIO 的 Selector 的处理任务;
  14. Acceptor 线程和 Poller 线程的数量分别由 Tomcat Connector 的 acceptorThreadCount 和 pollerThreadCount 决定;
  15. Tomcat 的 TaskQueue 是一个 LinkedBlockingQueue;
  16. TaskQueue队列里的 Task 由 Executor 里的线程读取并执行;
  17. TaskQueue队列里的 Task 由 Poller 放入;
  18. TaskQueue的长度默认是 Int 的最大值, 不过基本不会放这么多, 如果TaskQueue的任务和在处理的超过 maxConnection 的值, Poller 就会决绝新的任务;

参考: https://blog.cloudflare.com/syn-packet-handling-in-the-wild/

使用MAT 分析heap dump

  1. 下载及设置
    官方下载: https://www.eclipse.org/mat/
    JDK 最好用最新的 JDK, 因为最新的基本优化最多.
    根据你分析的 heap dump 的大小, 有时候需要调整 MAT 的 heap 的大小. 这个参数在 MAT 根目录的 MemoryAnalyzer.ini 文件里面. 我经常分析 30G 以下的 heap, 基本设置为 27G, 是 JVM 使用压缩指针来加速.

sjc-sreop-001.png

另外, 对于 HPROF 的 dump 来说, 我经常设置为非严格 parse, 因为有时候有点错误, 不影响分析:

sjc-sreop-002.png

文档: MAT 自带文档在 Help -> Help Contents 菜单里面, 或者[在线版本的文档][3]
  1. 分析
    正常情况下当你打开一个 heap dump 之后, 它会问你是否自动诊断内存泄漏, 如果你不是为了诊断内存泄漏, 可以取消这步.
    Histogram: 是按照类的实例数量聚集, 能很快发现包含大量实例的类. 一般情况下 char[] 或者 String 都在最上面, 这基本没有问题.
    Dominator Tree: 对于诊断内存泄漏非常有用, 如果能抓到一个对象 dominate 很多实例, 基本你找到了问题所在.
    OQL: 就像查询数据库的 SQL 语言, 非常方便的查找任何对象, 实例;
    Threads: 查看当前heap的所有线程, 对于发现某个对象是怎么被创建, 或引用的非常有帮助.
  2. 如何分析 IBM J9 dump
    参看: https://help.eclipse.org/2020-03/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html
    MAT 安装 DTFJ 插件步骤:

    1. 打开 MAT, 菜单: Help -> Install New Software ...
    2. 点 Add 按钮 添加新的 Repository.

    name: DTFJ
    Location: http://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/runtimes/tools/dtfj/
    点 Add 按钮

    1. 选中 IBM Monitoring and Diagnostic Tools
    2. 一路 next 下去, 中间接受 license, 然后重启 MAT

    更多关于如何使用 OQL: Java heap dump OQL samples - where