問題描述
出問題的是我們的主 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