万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势( 三 )


从下图的wireshark界面,可看到读200M文件,共有10个GET请求:GET /goofys-bucket/test.dbf HTTP/1.1,每个GET请求读20M文件,每个GET请求读完后回复:HTTP/1.1 200 OK 。第1个GET请求到达S3gateway时间为0.2287秒,第10个GET请求到达Ozone集群时间为1.026458秒 。第1个GET请求完成时间为1.869579秒,第10个GET请求完成时间为23.640925秒 。
可见10个GET请求在1秒内全部到达Ozone集群,但每个请求耗时越来越长 。因此只需要分析后续的GET请求读同样大小的数据块,比前序GET请求多做了哪些事情即可 。

万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
最后通过分析日志和阅读代码发现,Ozone采用的第三方库commons-io采用read实现skip 。例如读第10个GET请求时,实际只需要读[180M, 200M),但commons-io实现skip前180M时,会将前180M读出来,导致第10个GET请求读完整的[0M, 200M),因此GET请求越来越慢 。优化后,性能提升一百倍 。
jstackjstack用来查询线程状态,但在极端情况下也可以用于性能优化 。在部署服务时,发现进程迅速占满所有CPU,24核的机器进程使用CPU达到2381% 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
CPU使用如此之高,无法运行arthas进行perf分析,只能采用其他策略 。首先用top -Hp pid命令打出进程pid的所有线程及每个线程的CPU消耗 。如下图,第一列PID为线程号,%CPU列代表CPU消耗,注意该图只是展示作用,该图的进程并不是使用CPU达到2381%的进程,原进程的信息当初没保存 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
然后计算出使用CPU最高的线程号的十六进制表示0x417,再用jstack -l pid > jstack.txt命令打出所有线程状态,用0x417在jstack.txt查询消耗CPU最高的线程,即下图所示ThreadPoolExecutor里的线程,该线程一直处于RUNNABLE,且队列为empty,基本确认该部分线程出了问题,因为正常的线程不会一直空转,状态会有TIMED_WAITING的时刻 。
因为线程堆栈不包含业务代码,都是JDK的源码,因此用线程堆栈搜索JDK相关问题,最终发现是JDK8的Bug:JDK-8129861,该Bug在创建大小为0的线程池时容易触发,因此在应用代码里,将大小为0的线程池修改即可 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
宕机被其他进程杀在生产环境发生过进程被清理脚本杀掉 。排查工具有两个:linux自带的auditd和systemtap 。
首先使用auditd,因为该工具简单易用,不用安装 。使用service auditd status检查服务状态,如果未启动可用service auditd restart启动 。然后使用命令:auditctl -a exit,always -F arch=b64 -S kill,监听所有的Kill信号 。如下图所示,从type=OBJ_PID行里可以看到:捕捉到的Kill信号杀的进程号opid=40442,线程名ocomm=”rocksdb:pst_st”,注意这里打出的线程名而不是进程名 。
【万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势】从type=SYSCALL行里可以看到:a1=9表示kill -9;发出kill -9的进程是exe=”/usr/bin/bash”,进程号是pid=98003 。从这些信息并不能找到相应的进程,因为脚本往往运行完就停止,生命周期非常短 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
接下来使用systemtap分析,systemtap需要安装:yum install systemtap systemtap-runtime 。先写systemtap脚本findkiller.stp,如下所示,该systemtap脚本捕捉杀进程sig_pid的KILL信号,并使用task_ancestry打印发出KILL信号进程的所有祖先进程 。
probe signal.send{if(sig_name == "SIGKILL" && sig_pid == target()) {printf("%s, %s was sent to %s (pid:%d) by %s (pid:%d) uid :%dn", ctime(gettimeofday_s()), sig_name, pid_name , sig_pid, execname(), pid(), uid());printf("parent of sender: %s(%d)n", pexecname(), ppid());printf("task_ancestry:%sn", task_ancestry(pid2task(pid()), 1));}}然后stap -p4 findkiller.stp生成ko文件:stap_XX.ko,有的机器需要将ko文件补上签名才能运行 。然后运行:nohup staprun -x 98120 stap_XX.ko >nohup.out 2>&1 &,此处的98120即为脚本中的target() 。
捕捉结果如下,从图里可以看出发出KILL命令的进程是通过crond启动的,也就是说定时任务运行了某些脚本杀了进程 。但仍然不知道定时任务启动了哪个脚本杀了进程 。
万字详文:Java内存泄漏、性能优化、宕机死锁的N种姿势

文章插图
 
接下来再用auditd排查,使用命令:auditctl -a exit,always -F arch=b64 -S execve捕捉所有的系统调用,结果如下,最后一行是捕捉到杀进程opid=20286的信号,从图中可看出kill信号附近出现的都是/data/tools/clean命令 。


推荐阅读