前言
这几天手上的一个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
总结
事出有妖必有因,不能着急,静下心来去慢慢分析解决问题即可。并且一次次的亲手去排查也总是能获得或者巩固一些知识。
不怕出问题,就怕不出问题一直没机会成长,或者出了问题压根就不想和没有面对疑难杂症的勇气。