鲲鹏社区首页
中文
注册
一次java进程消失的定位过程

一次java进程消失的定位过程

java

发表于 2025/04/18

0

作者|丁浩源


问题背景

压测环境加大对业务的并发压力,业务的java进程直接消失不见了


应用信息

软件名称

版本

说明

open JDK

1.8.0_391

jdk

SpringBoot

2.5.3

web框架

XGBoost4j

1.5.1

XGBoost的java实现

应用信息比较简单,主要介绍下XGBoost ,XGBoost 是一个经过优化的分布式梯度提升库,旨在实现高效性、灵活性和可移植性。它在梯度提升框架下实现了多种机器学习算法。XGBoost 提供了并行树提升(也称为梯度提升决策树 GBDT 或梯度提升机 GBM),能够快速且精准地解决许多数据科学问题。客户业务主要是使用XGBoost进行预测分类。

问题现象

共有四台压力机,当起到3到4台压力机的java的进程消失。 并且压测的tps只有2000左右,远低于基线值的4500。

问题现象分析

 1.  寻找coredump

    正常来讲java进程异常退出之后会生成hs_err_pid文件即java的coredump文件,但是很遗憾我们找遍了整个文件目录也没有找到这个文件。查阅资料后发现,常见的还有就是操作系统将程序杀死然后会记录在操作系统日志里面:/var/log/message 会记录杀死进程的日志,但是我们也没有找到相关的记录。目前看分析进入了瓶颈,因为找不到任何的信息,程序就这样莫名其妙消失了。

2. Devkit分析程序运行情况

    安装好devkit,  怀疑异常退出是底层so库的迁移不兼容问题,因此启用软件迁移评估工具进行分析,项目的所有jar包进行扫描,没有so需要调整的问题,因此排除底层so文件不兼容的影响。

启动压测,开始java性能分析后, 在devkit的锁分析图中我们发现:

许多的http-nio线程等待 Booster类的一个锁, 这个Booster类属于xgboost4j。

    Xgboost的predict方法导致了比较多的请求线程的阻塞。这点可以解释为什么性能较差,tps低。

    在客户基线环境(类生产环境)上用jstack查看线程情况后,发现没有线程block的情况(其实就是类生产的负载比较低),当时就以为是锁的问题导致了程序退出和性能差。

   分析源码后发现,Xgboost的predict方法是带了同步锁的:

    于是思路就变成了解决锁竞争的问题,为了验证无锁的版本性能是否会提升,我们之间将synchronized去掉之后重新压测(当然这个操作不能上生产,只是为了验证),验证结果为TPS达到了7000,CPU占用高达70%,因为去掉锁性能提升明显所以方向基本确定。

   在xgboost4j的官网发现2.1.13的提供了一个无锁版本的方法,如果使用这个版本则需要需要代码,改动较大。

3.  为什么x86无锁

     使用无锁版本可以规避这个问题,但是如果把问题的根源归结给锁,那么就有个问题,为什么x86没有锁?

得出这个x86没有锁的结论是基于x86类生产环境使用jstack查看的,怀疑是x86类生产环境的压力不够,于是搭建一个x86的测试环境编译我们进行对比压测。

搭建出来之后压测发现,x86的tps也只有4500, 低于客户之前说的1w tps。 然后用jstack查看线程情况发现也是存在大量的线程block,于是推翻看了x86没有锁的结论。

那么现在问题来了,既然两个环境都有锁,那程序崩溃的问题就不是锁引起的,那么即使使用无锁的版本,可能同等压力下不会崩溃,但是极限情况下还是有可能崩溃。

4.  正面定位为什么进程消失

    当前的规避方案没有解释进程为什么会消失,讨论后猜测程序可能是自己退出的,就是可能是程序自己因为某种情况调用了exit,自行退出,然后操作系统认为这是一种正常行为所以没有任何的异常日志。

 
 使用gdb调试java进程,在exit, 和_exit打上断点:
  gdb attach {进程pid}                
  b exit                          
  b _exit                         
  handle SIGSEGV nostop pass             
  c
如程序捕捉到退出函数,敲bt ,打印下调用栈
在四台压力机的情况下,如期出现异常,并捕获到如下堆栈:
(gdb) bt
#0 0x0000fffc5b6495a8 in exit () from /usr/lib64/libc.so.6
#1 0x0000ffb452529b84 in ?? () from /usr/lib64/libgomp.so.1
#2 0x0000ffb452529c10 in ?? () from /usr/lib64/libgomp.so.1
#3 0x0000ffb452536d88 in ?? () from /usr/lib64/libgomp.so.1
#4 0x0000ffb45252e4a0 in GOMP_parallel () from /usr/lib64/libgomp.so.1
#5 0x0000ffb4528116e0 in void xgboost::common::ParallelFor<unsigned long.......

可以发现程序在xgboost内部调用了openmp的包,然后走到了exit方法。 因此基本可以确认是xgboost内部使用的openmp模块因为某些原因走到了exit里面导致程序退出。

问题修复

如上分析,在编译xgboost4j的jar包是直接将user openmp选项关掉:

重新构建jar后,替换环境上的lib里面的xgboost4j的jar包。

重新压测后发现tps达到了1w, 远超基线值的4500. 进过长时间压测之后发现没有崩溃,问题得到解决。

关于该场景下关闭openmp性能反而提升的想法:

1. 该场景属于高并发场景,并且外部的predict方法还加上synchronized,锁竞争严重,更多的线程会激化这种竞争

2. 去掉openmp之后,发现单次预测的时间本身极短,1ms以下,说明单次预测的计算量并不大,用多线程实现,在线程创建,切换上的消耗反而大于单纯的计算,得不偿失。


总结思考

  这次经历扩展了对java进程异常消失的认识,程序还存在自行调用exit的可能。要熟练使用gdb等工具,之前以为gdb调试不了java,后面要加强这方面的认识。

本页内容