「Bug」Jenkins Slave 卡顿与僵尸进程
问题描述
出问题的是我们的主 Jenkins Slave,是在 Ubuntu 虚拟机里面,使用 Docker 跑了四个不同环境的 Jenkins Slave,提供 c#/golang/flutter/python 等的构建/测试环境。
而且这台服务器是不关机的,24h 提供服务。
一段时间后,这台 Jenkins Slave 虚拟机的内存就居高不下。这大概是某些构建任务会维护守护进程以加快下一次构建的速度,所以内存没释放。
大概过了一个月左右,问题就出现了:明明虚拟机提示还有 2G 的空闲内存(不包含 cached),可 ssh 登录却要花将近两三分钟,登入后 top/htop 也要黑屏将近一分钟。。
神奇的是其他的命令(ls/cd/df/du/docker)却都很正常,没有什么明显的卡顿现象。
快速解决方案
对比其他用于加速构建的 Jenkins Slave,它们每天晚上都会关机(节约用电),就一直没出过问题。
于是将其重启后,暂时解决了问题。
原因分析
首先列出卡顿的通用排查流程:
- 性能问题:CPU/RAM/IO/Network
- 僵尸进程导致 Pid 资源不足。
- 使用 ICMP-ping、tcp-ping 检查网卡
- 系统问题,检查 kernel 和 syslog
既然 CPU/RAM 都没有问题,而且每晚关机的 Slave 也没问题,这说明卡顿是在长期运行时才会发生。
排查:
- 系统负载很正常,排除性能问题。
- 内核参数:之前图方便,给所有的服务器都设了
vm.max_map_count=262144
(elasticsearch 6.8+ 需要)- 之前遇到过因为这个参数,导致 redis 长期运行后响应缓慢的问题。可能和它有关。
- Jenkins Slave 虚拟机中有 66248 个进程,但是其中 66009 个都是僵尸进程
Jenkins Slave 最明显的问题,就是僵尸进程过多了。
僵尸进程
man ps
中对 僵尸进程(状态 Z
)的解释如下:
Z defunct ("zombie") process, terminated but not reaped by its parent
在子进程已经终止,但是父进程却没有通过 wait
系统调用来收割 (reap) 子进程时,这个子进程会成为一个僵尸进程。top
命令的第二行会显示僵尸进程的个数。
查看僵尸进程的命令:ps -ef| grep defunc
,defunc 意为“死者”。
僵尸进程无法通过 kill 命令杀死(你无法杀死一个已经死掉的进程hhh),只能通过干掉它的父进程来间接的杀死它们。
系统中存在过多的僵尸进程将占用大量的操作系统进程表资源,导致系统卡顿并最终崩溃!
通过前述的 ps 命令,我们发现这些 zombies 的父进程基本都是 jenkins-agent 进程,
重启服务器时该进程也被重启,因此服务器恢复正常。但是这显然只是治标的方法,过一段时间僵尸进程又会堆积,导致服务器卡顿。
尝试寻找治根的解决方法,搜索 Jenkins zombie
发现如下 Issues、PR:
- Handling of zombie processes would be useful
- defunct sh and sleep processes when running on slave-jnlp
- Wrapper process leaves zombie when no init process present
- Start long running containers with --init
最简单的解决方案,应该就是在通过 docker 启动 jenkins-agent 时添加 --init
参数,对应的 docker-compose 参数为 docker-compose reference - init
于是在 docker-compose.yml 中添加上这个参数,解决了该问题,示例如下:
version: '3.7'
services:
android1: # 这个 key 只是它在 docker-compose 内部的一个别名,在 docker 中不可用。
container_name: android1 # 这个名字才是容器 在 docker 中的真正名称。
image: image-registry.svc.local/jenkins/slave-android:latest # 私有仓库,定制的 android 构建用基础镜像
init: true # 这个必须加上,否则僵尸进程无法回收,僵尸进程累积会导致系统卡顿
environment:
- TZ=Asia/Shanghai
command: -url http://jenkins.ci.svc.local -workDir=/home/jenkins/ <secret> android1
restart: always
networks:
- jenkins-slave