Quarkus部署于Kubernetes集群 如何规避持续产生僵尸进程的问题
Kubernetes集群Quarkus应用持续产生僵尸进程的排查与解决
问题现象
在Kubernetes集群中部署的Quarkus应用出现僵尸进程持续增长异常,增量速度为每1-2秒1个,节点累计僵尸进程峰值曾超过12000个。
按照应用代码逻辑,仅在收到特定操作指令时,才会通过Runtime调用Shell命令创建子进程。当前业务场景未触发该类进程创建逻辑,但僵尸进程仍持续产生。
相关的进程调用、回收逻辑代码如下:
String command = "helm install release xxx"; LOGGER.debug("handle Install request : command [{}]", command); waitForNormalTermination(Runtime.getRuntime().exec(command), INSTALL_TIMEOUT, TimeUnit.SECONDS, name); private void waitForNormalTermination(Process process, int timeout, TimeUnit unit, String release) throws Exception { try { if (!process.waitFor(timeout, unit)) { throw new TimeoutException("Timeout while executing " + process.info().commandLine().orElse(null)); } if (process.exitValue() != 0) { String errorStreamOutput = IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8); if (errorStreamOutput != null && errorStreamOutput.contains("release: not found")) { throw new ReleaseNotFoundException(release); } throw new Exception("Process termination was abnormal, exit value: [" + process.exitValue() + "], command:[" + process.info().commandLine().orElse(null) + "] error returned:[" + errorStreamOutput + "]"); } } finally { pr.destroy(); // 后续补充该调用用于简化进程销毁逻辑,确保方法退出前所有关联进程被销毁 } }
已开展的排查与修复操作
- 在代码中新增
pr.destroy()调用逻辑,重新构建镜像并推送至镜像仓库 - 手动删除集群中运行的异常Pod,等待Pod基于新镜像自动重建完成
- 登录应用所在的调度节点手动清理存量僵尸进程,清理前节点累计僵尸进程超12000个,清理后剩余4200个存量
- 循环执行
ps aux | grep 'Z' | wc -l观测僵尸进程增量,确认僵尸数量仍在持续增长,观测时节点统计值已达4487 - 持续跟踪应用日志,确认业务侧仅存在其他服务发起的少量周期性REST轮询调用,未触发子进程创建相关逻辑
- 编写节点侧排查脚本,统计各父进程关联的僵尸进程数量、关联容器PID,脚本内容如下:
#!/bin/bash ps -eo ppid,comm | grep "<defunct>" | awk '{print $1}' | sort | uniq -c > /tmp/zombie.file Files="/tmp/zombie.file" Lines=$(cat $Files | tr -s ' ' | cut -d ' ' -f2,3) i=0; for Line in $Lines do if [[ $i -eq 0 ]] then echo "Zombies found = $Line" i=1 else ps -f $Line i=0 fi done echo " " echo " " echo "Running docker containers are " # 该行用于筛选私有仓库下的自有容器 #docker ps | grep private-repository echo " " echo " " echo "the PID of those docker containers" for value in $(docker ps | grep private-repository | cut -d ' ' -f1); do docker inspect --format '{{ .State.Pid }}' $value done
- 将节点容器运行时从Docker切换为Containerd后,僵尸进程增长问题仍然存在,初步判断问题与Kubernetes的exec探针机制相关
标准解决方案
- 配置轻量init进程作为容器1号进程
Kubernetes容器内如果没有指定专属init进程,Java应用会直接作为容器内的1号进程。Linux内核规则下,1号进程不会自动继承回收孤儿子进程,所有退出后未被父进程调用wait()回收的子进程都会成为僵尸进程。无论是应用自身创建的子进程、还是kubelet执行exec探针派生的子进程,都会因为无法被回收持续累积。
可在应用镜像启动命令中引入tini或dumb-init作为前置init进程,负责自动回收所有退出的子进程,示例Dockerfile配置:
# 安装tini RUN apk add --no-cache tini # 将tini设为1号进程 ENTRYPOINT ["/sbin/tini", "--", "java", "-jar", "/app/quarkus-app/quarkus-run.jar"]
Quarkus框架本身也提供内置的进程适配逻辑,可在配置文件中开启自动子进程回收能力,无需额外引入外部init进程。
- 修正原有进程调用的代码缺陷
原代码中finally块的pr.destroy()存在变量不匹配问题:方法入参定义的是Process process,但finally中调用的pr属于未定义的无效变量,编译运行时该销毁逻辑根本未实际执行,所有运行完成的子进程都未被主动回收。需将代码修改为process.destroyForcibly(),同时补充进程关联的输入流、错误流的关闭逻辑,避免进程句柄泄漏。 - 优化exec探针配置
调整Liveness/Readiness探针的命令格式,避免通过sh -c包裹探针命令派生多余的shell中间进程;合理设置探针的超时时间、执行频率,减少kubelet创建的exec子进程残留。
内容的提问来源于stack exchange,提问作者Sebastien Dionne
相关产品推荐
相关产品推荐

