记一次Kill <Pid> Java进程无法退出的问题处理

前言

这几天手上的一个Java应用在流水线部署的时候总是会偶尔失败。流水线的基本过程是先使用kill <PID>来杀死当前的Java进程,然后不断检查进程优雅杀死后继而重新启动JVM。

不携带-9信号量,使用kill -9 <PID>去强制杀死进程的原因就是怕正在进行的一些业务突然中断丢失,所以使用上面的方式来杀死进程,Spring也会优雅的关闭。直到所有的线程完成使命后退出线程。

流水线脚本

下面是一段关于杀死进程的shell脚本,执行kill命令后,去轮询看是否进程退出,在轮询过程中退出了的话就会return,否则就会卡住,流水线就会执行失败。

shell 复制代码
stop() {
  # 过滤 java & 含该应用名的,且排除grep进程和执行deploy.sh 产生的的进程,打印出进程ID
  pid=$(ps -ef | grep java | grep ${APP_NAME} | grep external-data-integration | grep -v grep | grep -v 'deploy.sh' | awk '{print$2}')

  # 没有该进程
  if [[ ! $pid ]]; then
    echo -e "\r no ${APP_NAME} process"
    return
  fi

  echo "stop ${APP_NAME} process"
  times=60
  for e in $(seq 60); do
    COSTTIME=$(($times - $e))
    pid=$(ps -ef | grep java | grep ${APP_NAME} | grep -v grep | grep -v 'deploy.sh' | awk '{print$2}')

    # 杀掉进程
    if [[ $pid ]]; then
      kill $pid
      echo -e "\r        -- stopping java lasts $(expr $COSTTIME) seconds."
    else
      echo -e "\rjava process has exited"
      break
    fi
    sleep 1s
  done
  echo ""
}

流水线日志

正常的流水线日志应该是这样的,正常杀死进程后就启动项目。

排查步骤和思路

当问题发生后,去机器上查看进程情况,使用jps命令可以找到机器上的Java进程,就会发现我们的进程并没有消失。所以接下来就是去查看究竟是什么情况导致的问题。

一般来说,导致进程未消失的原因就是进程在等待磁盘I/O、网络连接或者其他资源(比如数据库连接,数据库连接基本上也是网络连接)。虽说是Java进程在阻塞,但实际上Java运行的最小单位是线程,如果是线程在阻塞,那么自然进程就是无法自然退出的。

查看线程

所以接下来应该去看究竟哪些线程,依旧在kill之后还死活不结束。正常来说,一个线程或者线程池在kill进程后,Spring环境关闭,不会再有新的业务进来了,随着时间前进,慢慢就执行完成了。如果一直没执行完,那么基本上就是发生了阻塞。

一旦有线程发生了上述说的磁盘和网络IO的阻塞,就会一直等待。

接下来就是使用 jstack <pid> 查看 Java 进程的线程堆栈,因为该命令会将线程和堆栈信息都打印到控制台中,不方便查看,所以最好输出到一个文件中下载到自己的电脑上查看:

shell 复制代码
jstack <pid> > jstack.log

它的内容大概长这个样子:

找到阻塞线程

紧接着我们就去打印出来的文件中搜索一些阻塞信息,其中网络阻塞是最常见的,Java中实现网络连接就是Socke套接字,TCP连接,所以根据IO,Socket关键字去搜索,就可能会发生蛛丝马迹。

这个socketRead0本地方法(native method)会一直在这等着完成某个连接,看下面的堆栈信息会发现这是来自mongodb数据库的连接。

有可能就是因为Spring关闭后,连接池关闭了,连接都没了,所以就一直导致某个想要和mongodb进行交互的线程一直等待阻塞。

当然问题解决的过程肯定不是一帆风顺的,我也是倒着骑毛驴,先是查看了大量的线程信息,然后发现这个,然后才想到了网络IO阻塞是最可能的!

如何验证就是这个线程导致的进程无法退出呢?杀死该进程,然后看进程情况。

杀死线程确认问题

找到上文中打印出来的线程的nid,如上图的线程的nid就是0x5964,紧接着通过

shell 复制代码
ps -eLo pid,lwp,cmd | grep <nid>

会输出一行信息,其中有个lwp id,记住这个id。

接下来我们就先使用进程id进入到进程中,再来杀死这个进程。首先用 gdb 附加到进程:

shell 复制代码
gdb -p <pid>

打印出所有线程id:

bash 复制代码
info threads

会输出类似于:

shell 复制代码
2 Thread 0x7f9c1406b000 (LWP 1005)  0x00007f9c1a6bdbf6 in poll ()

的格式的很多条信息,通过LWP id进行寻找,锁定这一条,最前面的那个数字就是线程在进程中的编号num。

然后执行下面两条命令就能杀死该线程了:

shell 复制代码
thread <num> # 进入该线程
call pthread_kill(<LWP_ID>, 9) # 杀死该线程,9表示强制杀死的信号量,是固定值,和 kill -9中的9一样

杀掉该线程后,退出gdb,命令行中再执行jps命令,发现进程果然退出消失了!所以断定就是该问题导致的。

问题解决

找到了问题所在:因Spring环境关闭,连接池关闭,与mongodb的TCP连接中断后,SocketRead0方法一直在等待IO响应所以导致线程阻塞。解决问题的方法就是增加与mongodb连接中的socket的超时时间设置。

yaml 复制代码
  data:
    mongodb:
      host: xxx
      port: xxx
      database: xxx
      option:
        # 关闭连接前的最大等待时间(毫秒)
        max-wait-time: 5000
        # 服务器选择超时
        server-selection-timeout: 5000
        # Socket读写超时
        socket-timeout: 5000
        # 心跳检测频率
        heartbeat-frequency: 5000

总结

事出有妖必有因,不能着急,静下心来去慢慢分析解决问题即可。并且一次次的亲手去排查也总是能获得或者巩固一些知识。

不怕出问题,就怕不出问题一直没机会成长,或者出了问题压根就不想和没有面对疑难杂症的勇气。

相关推荐
徐小黑ACG7 分钟前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
战族狼魂3 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
xyliiiiiL4 小时前
ZGC初步了解
java·jvm·算法
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch5 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
bobz9655 小时前
k8s 怎么提供虚拟机更好
后端
bobz9656 小时前
nova compute 如何创建 ovs 端口
后端
天天向上杰6 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
用键盘当武器的秋刀鱼6 小时前
springBoot统一响应类型3.5.1版本
java·spring boot·后端