Harbor容器僵尸进程数告警——docker-compose.override.yml加init参数修复
记录时间:2026-09-19
环境:CentOS 7.x + Docker / docker-compose,Harbor v2.14.0,生产环境双节点(10.x.x.47、10.x.x.48)说明:笔记中的 IP、域名、业务域标识均已做脱敏处理,实际值以现场为准。
一、问题现象
最近监控里 <业务域>-prod-xc 的两台 Harbor 节点频繁触发「Node 进程数过多」告警,告警规则长这样:
- alert: Node 进程数过多
expr: |
node_processes_state{component="node-exporter",state="Z"} > 10
for: 5m
labels:
severity: warning
看曲线,僵尸进程数长期在 5~12 之间晃,中位数 6~8,偶尔冲到 12 就持续 5 分钟触发告警。
我的第一反应是:这个阈值 10 是不是设低了?Harbor 上跑的组件多,有几个僵尸好像也正常。
二、排查过程
2.1 先看形态,判断是真实泄漏还是阈值误报
我把曲线形态看了一遍:是缓慢爬升到 12 附近又回落,常年大于 0,没有突发尖峰的样子。
僵尸进程(state=Z)只有在父进程没有 wait() 回收时才会存在。数量长期不为零,说明确实有进程在一直漏。偶发尖峰才可能是阈值问题,这种形态不像。所以先不动告警,找源头。
2.2 定位僵尸的父进程
在 Harbor 节点上跑这两条:
# 列出所有僵尸进程,重点是 PPID 和存活时长
ps -eo pid,ppid,stat,etime,comm | awk '$3 ~ /Z/'
# 拿到各容器的主进程 PID,用来和上面的 PPID 对号入座
docker ps -q | xargs docker inspect --format '{{.Name}} {{.State.Pid}}' | sort
僵尸输出(PID 已保留,节点/容器信息已脱敏):
211416 986186 Z 27-09:19:45 timeout <defunct>
333189 986186 Z 6-09:13:19 timeout <defunct>
883930 986186 Z 10-09:15:01 timeout <defunct>
1496174 986186 Z 52-09:24:53 timeout <defunct>
1548038 986173 Z 09:09:38 timeout <defunct>
1548082 33034 Z 09:09:37 timeout <defunct>
2128458 986173 Z 4-09:11:50 mount <defunct>
2686326 986186 Z 15-09:17:09 timeout <defunct>
3258362 986173 Z 19-09:18:14 timeout <defunct>
3522433 4175388 Z 73-21:57:14 wsrep_sst_xtrab <defunct>
3882394 986173 Z 16-09:17:23 timeout <defunct>
3926964 33034 Z 2-09:10:52 timeout <defunct>
拿容器 PID 对了一遍,父进程分布很集中:
| 父 PID | 归属 | 僵尸子进程 | 数量 | 存活时长 |
|---|---|---|---|---|
| 986186 | trivy-adapter 容器 | timeout <defunct> |
5 | 6~52 天 |
| 986173 | registryctl 容器 | timeout / mount <defunct> |
4 | 4~19 天 |
| 33034 | harbor-exporter 容器 | timeout <defunct> |
2 | 2~9 天 |
| 4175388 | 宿主机 MySQL(Galera) | wsrep_sst_xtrab <defunct> |
1 | 73 天 |
僵尸的父进程就是这三个 Harbor 容器的 PID 1。存活 52 天那个说明僵尸只增不减,只是增长慢,所以看起来常年维持在 10 上下。
最后一个 wsrep_sst_xtrab 是 73 天前 Galera 做 SST 全量同步时留下的,父进程 mysqld 没回收。就一个僵尸,不值得为它重启生产库,我决定放着不管。
2.3 为什么这三个容器会漏
用 docker top 看一眼容器内的进程树就明白了,容器内 PID 1 直接就是业务二进制:
10000 2721964 2721824 /home/scanner/bin/scanner-trivy
10000 2721913 2721797 /home/harbor/harbor_registryctl -c /etc/registryctl/config.yml
10000 2722288 2722163 /harbor/harbor_exporter
这三个都是 Go 写的程序。Go 程序作为 PID 1 时不会去 wait() 回收孤儿子进程,子进程(这里是 timeout、mount)退出后就变成僵尸一直挂着。这是容器里跑 Go 二进制的通病,不是 Harbor 配置写错了。
三、根因分析
容器 PID 1 是 Go 业务进程,不具备 init 进程的孤儿回收能力。
顺带把 harbor.yml 里的 trivy 段排除掉,这几个参数和子进程回收完全不在一个层面:
| 参数 | 值 | 作用 | 与僵尸进程的关系 |
|---|---|---|---|
ignore_unfixed |
false | 是否忽略无修复版本的漏洞 | 无关 |
skip_update |
true | 不联网更新漏洞库 | 无关 |
offline_scan |
true | 离线扫描(用本地漏洞库) | 无关 |
security_check |
vuln | 扫描类型(vuln / secret) | 无关 |
insecure |
false | 是否跳过 TLS 校验 | 无关 |
四、解决方案
4.1 尝试 1(自己推演后否决):直接改 docker-compose.yml 加 init
我最初想的是直接在 docker-compose.yml 里给三个服务加 init: true。同事提醒了一句:compose 文件是 harbor.yml 经 prepare 生成的。
我推演了一下确认他说得对:以后升级 Harbor 或重跑 ./install.sh,docker-compose.yml 会被重新生成覆盖,改动就丢了。这个方案不可靠,放弃。
4.2 确认 harbor.yml 没有这个入口
既然要求”只改 harbor.yml”,我先确认它到底能不能干这事:
grep -in "init" harbor.yml.tmpl | head -20
harbor.yml 只描述 Harbor 自身的配置(hostname、端口、数据目录、trivy 开关、外部 DB/Redis 等),没有容器级 init 参数入口。这条路走不通,Harbor 安装包压根没暴露这个开关。
4.3 最终方案:docker-compose.override.yml
docker compose 会自动合并同目录下的 docker-compose.override.yml,而且 prepare / install.sh 只会重写 docker-compose.yml,不会碰 override 文件。这样既满足了”不改主 compose 文件”的约束,又能长期生效。
# 修复 Harbor 容器 PID 1(Go 二进制)不回收子进程导致 timeout/mount 僵尸进程堆积。
# 说明:harbor.yml 不支持配置容器级 init 参数;本文件由 docker compose 自动合并,
# 且不会被 prepare / install.sh 覆盖。
services:
trivy-adapter:
init: true
registryctl:
init: true
exporter:
init: true
注意:服务名和容器名不一致,写错不会报错但也不会生效。Harbor v2.14 的对应关系见第六节表格。
然后重建容器:
cd /opt/docker/harbor # 实际安装目录以现场为准
docker-compose up -d
注意:
up -d只有在检测到配置变化时才重建容器。如果执行后HostConfig.Init仍是 false,补一条docker-compose up -d --force-recreate trivy-adapter registryctl exporter。
五、验证
# 1. init 参数是否生效
docker inspect --format '{{.Name}} init={{.HostConfig.Init}}' trivy-adapter registryctl harbor-exporter
# 2. PID 1 是否换成 docker-init
docker top trivy-adapter | head -3
docker top registryctl | head -3
docker top harbor-exporter | head -3
# 3. 僵尸是否清零
ps -eo pid,ppid,stat,etime,comm | awk '$3 ~ /Z/'
# 4. 服务健康
docker-compose ps
实际输出(已脱敏):
/trivy-adapter init=true
/registryctl init=true
/harbor-exporter init=true
UID PID PPID CMD
10000 2721824 2721682 /sbin/docker-init -- /home/scanner/entrypoint.sh
10000 2721964 2721824 /home/scanner/bin/scanner-trivy
10000 2721797 2721699 /sbin/docker-init -- /home/harbor/start.sh
10000 2721913 2721797 /home/harbor/harbor_registryctl -c /etc/registryctl/config.yml
10000 2722163 2722107 /sbin/docker-init -- /harbor/entrypoint.sh
10000 2722288 2722163 /harbor/harbor_exporter
3522433 4175388 Z 73-22:05:40 wsrep_sst_xtrab <defunct>
四条都对上了:
init=true,说明容器确实被重建过(HostConfig.Init只有新建容器才会带上)- PID 1 变成
/sbin/docker-init,业务进程降为它的子进程,此后子进程退出会被立即回收 - 僵尸从 12 个降到 1 个,只剩 mysqld 那个 73 天的 SST 残留
docker-compose ps10 个容器全部Up (healthy)
六、注意事项
override 文件不会随 Harbor 升级失效,但升级后要重新确认服务名有没有变,用
docker-compose config | grep -B3 "init: true"复核一次。服务名 ≠ 容器名,写错的话 compose 不报错但配置不生效。Harbor v2.14 的映射关系:
| compose 服务名 | 容器名 |
|---|---|
core |
harbor-core |
postgresql |
harbor-db |
proxy |
nginx |
exporter |
harbor-exporter |
jobservice |
harbor-jobservice |
portal |
harbor-portal |
log |
harbor-log |
trivy-adapter |
trivy-adapter |
registryctl / registry |
registryctl / registry |
重建容器等于重启,trivy-adapter 会有短暂不可用(漏洞扫描任务中断重试),registryctl 的 GC 定时任务会重置。建议低峰执行。
两台节点都要改,两台都在告警范围,另一台执行同样的操作。
告警阈值 10 不要再动。修完之后它完全合理,调高只是把真实问题静音。如果短期没法重建容器,才考虑临时上调并在 annotation 里注明原因。
残留的那个
wsrep_sst_xtrab僵尸不要处理。为 1 个僵尸重启生产库不划算。如果漏洞扫描功能实际没在用,理论上
--without-trivy重装能直接消掉 5 个僵尸的主要来源,但代价是重装加失去扫描能力,我一般不建议。