Harbor容器僵尸进程数告警——docker-compose.override.yml加init参数修复

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() 回收孤儿子进程,子进程(这里是 timeoutmount)退出后就变成僵尸一直挂着。这是容器里跑 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.ymlprepare 生成的。

我推演了一下确认他说得对:以后升级 Harbor 或重跑 ./install.shdocker-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>

四条都对上了:

  1. init=true,说明容器确实被重建过(HostConfig.Init 只有新建容器才会带上)
  2. PID 1 变成 /sbin/docker-init,业务进程降为它的子进程,此后子进程退出会被立即回收
  3. 僵尸从 12 个降到 1 个,只剩 mysqld 那个 73 天的 SST 残留
  4. docker-compose ps 10 个容器全部 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 个僵尸的主要来源,但代价是重装加失去扫描能力,我一般不建议。

七、参考资料

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇