问题描述

当 Kubernetes 集群中出现以下症状时,可能存在僵尸容器问题:

症状识别

  1. Pod 事件中出现大量 FailedKillPod 错误

    error killing pod: failed to "KillContainer" with KillContainerError: 
    "rpc error: code = DeadlineExceeded desc = context deadline exceeded"
    
  2. Pod metrics 异常

    • CPU 显示异常高的值(如 14635 cores)
    • 内存或 CPU 显示为 0
    • kubectl top pods 返回不正常数据
  3. 节点资源占用异常

    • 内存使用率持续在 75-80% 以上
    • 存在多个月前的旧进程仍在运行
  4. 容器状态异常

    crictl ps | grep <pod-name>
    # 显示多个相同 Pod 的容器,创建时间相差很久(如几个月)
    

诊断步骤

1. 检查 Pod 状态和事件

# 查看 Pod 状态
kubectl get pods -n <namespace> -o wide

# 查看最近的事件
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | tail -50

# 查看特定 Pod 的详细信息
kubectl describe pod <pod-name> -n <namespace>

2. 检查节点上的容器

在每个节点上执行:

# 列出所有容器
crictl ps | grep <pod-name>

# 检查是否有旧容器(注意 CREATED 列)
# 正常情况:所有容器的创建时间应该相近
# 异常情况:存在 "X months ago" 的旧容器

3. 检查进程状态

# 查看相关进程
ps aux | grep <application-name> | grep -v grep

# 检查是否有僵尸进程(状态为 Z)
ps aux | grep defunct

4. 检查系统资源

# 检查内存使用
free -h

# 检查节点资源
kubectl top nodes

# 检查 Pod 资源(可能失败)
kubectl top pods -n <namespace>

解决方案

方案概述

僵尸容器的清理需要在每个受影响的节点上执行以下步骤:

  1. 识别并杀死旧容器的进程
  2. 停止 containerd
  3. 清理容器元数据
  4. 重启 containerd

完整清理脚本

将以下脚本保存为 cleanup-zombie-containers.sh

#!/bin/bash
# Kubernetes 僵尸容器清理脚本
# 使用方法:sudo ./cleanup-zombie-containers.sh <old-container-id>

set -e

if [ "$EUID" -ne 0 ]; then 
    echo "请使用 root 权限运行此脚本"
    exit 1
fi

if [ -z "$1" ]; then
    echo "使用方法: $0 <container-id>"
    echo "示例: $0 c6cfbe677343b"
    exit 1
fi

OLD_CONTAINER_ID="$1"

echo "=========================================="
echo "开始清理僵尸容器: $OLD_CONTAINER_ID"
echo "=========================================="

# 1. 查找容器的进程 ID
echo ""
echo "=== 步骤 1: 查找容器进程 ==="
PID=$(crictl inspect "$OLD_CONTAINER_ID" 2>/dev/null | grep '"pid"' | head -1 | grep -oP ':\s*\K\d+')

if [ -n "$PID" ] && [ "$PID" != "1" ]; then
    echo "找到容器进程 PID: $PID"
    
    # 杀死主进程和子进程
    echo "杀死进程及其子进程..."
    pkill -9 -P "$PID" 2>/dev/null || true
    kill -9 "$PID" 2>/dev/null || true
    sleep 2
else
    echo "未找到有效的进程 ID 或 PID 为 1(跳过)"
fi

# 2. 查找并杀死旧的应用进程(根据日期)
echo ""
echo "=== 步骤 2: 清理旧的应用进程 ==="
echo "查找 4 个月前(May)启动的进程..."
OLD_PIDS=$(ps aux | grep -E "May14|May" | grep -v grep | awk '{print $2}')
if [ -n "$OLD_PIDS" ]; then
    echo "找到旧进程: $OLD_PIDS"
    echo "$OLD_PIDS" | xargs -r kill -9 2>/dev/null || true
    sleep 2
else
    echo "未找到旧进程"
fi

# 3. 查找 Pod Sandbox ID
echo ""
echo "=== 步骤 3: 查找 Pod Sandbox ==="
SANDBOX_ID=$(crictl inspect "$OLD_CONTAINER_ID" 2>/dev/null | grep '"sandboxID"' | grep -oP ':\s*"\K[^"]+')
echo "Pod Sandbox ID: $SANDBOX_ID"

# 4. 杀死 containerd-shim 进程
if [ -n "$SANDBOX_ID" ]; then
    echo ""
    echo "=== 步骤 4: 清理 containerd-shim ==="
    SHIM_PIDS=$(ps aux | grep containerd-shim | grep "$SANDBOX_ID" | grep -v grep | awk '{print $2}')
    if [ -n "$SHIM_PIDS" ]; then
        echo "找到 containerd-shim 进程: $SHIM_PIDS"
        echo "$SHIM_PIDS" | xargs -r kill -9 2>/dev/null || true
        sleep 2
    fi
fi

# 5. 停止 containerd
echo ""
echo "=== 步骤 5: 停止 containerd ==="
systemctl stop containerd
sleep 5

# 6. 清理状态文件
echo ""
echo "=== 步骤 6: 清理容器元数据 ==="
if [ -n "$SANDBOX_ID" ]; then
    echo "清理 Sandbox 相关文件..."
    rm -rf /run/containerd/runc/k8s.io/"$SANDBOX_ID"* 2>/dev/null || true
    rm -rf /run/containerd/io.containerd.grpc.v1.cri/sandboxes/"$SANDBOX_ID"* 2>/dev/null || true
fi

echo "清理容器相关文件..."
rm -rf /run/containerd/io.containerd.runtime.v2.task/k8s.io/"$OLD_CONTAINER_ID"* 2>/dev/null || true
rm -rf /run/containerd/io.containerd.grpc.v1.cri/containers/"$OLD_CONTAINER_ID"* 2>/dev/null || true

# 7. 重启 containerd
echo ""
echo "=== 步骤 7: 重启 containerd ==="
systemctl start containerd
echo "等待 30 秒让 containerd 完全启动..."
sleep 30

# 8. 验证结果
echo ""
echo "=== 步骤 8: 验证清理结果 ==="
echo "当前容器列表:"
crictl ps 2>/dev/null | grep -v "CONTAINER" | head -5

REMAINING=$(crictl ps 2>/dev/null | grep "$OLD_CONTAINER_ID" || true)
if [ -z "$REMAINING" ]; then
    echo ""
    echo "✓ 僵尸容器已成功清理!"
else
    echo ""
    echo "✗ 警告:容器仍然存在,可能需要重启节点"
fi

echo ""
echo "=========================================="
echo "清理完成!"
echo "=========================================="

使用步骤

步骤 1: 识别僵尸容器

在每个节点上执行:

# 查找旧容器(注意 "months ago")
crictl ps | grep <app-name>

# 记录旧容器的 ID(通常是前几个字符就够了)
# 示例输出:
# c6cfbe677343b  emqx:5.8.6  4 months ago  Running  emqx  ...
步骤 2: 执行清理
# 赋予脚本执行权限
chmod +x cleanup-zombie-containers.sh

# 执行清理(替换为实际的容器 ID)
sudo ./cleanup-zombie-containers.sh c6cfbe677343b
步骤 3: 验证清理结果
# 检查容器列表(不应该有 "months ago" 的容器)
crictl ps | grep <app-name>

# 检查进程
ps aux | grep <app-name> | grep -v grep

# 检查是否还有旧进程
ps aux | grep May | grep <app-name>
步骤 4: 在所有受影响节点重复

对集群中所有有僵尸容器的节点重复上述步骤。

步骤 5: 验证 Kubernetes 状态

回到 master 节点执行:

# 检查 Pod 状态(应该都在 Running)
kubectl get pods -n <namespace> -o wide

# 等待 3-5 分钟让 Pod 完全启动
sleep 180

# 检查 Pod 是否全部就绪
kubectl get pods -n <namespace>

# 检查是否还有错误事件
kubectl get events -n <namespace> --sort-by='.lastTimestamp' | grep -i failed | tail -10

# 检查资源使用(应该恢复正常)
kubectl top pods -n <namespace>

快速清理命令(紧急情况)

如果脚本无法使用,可以手动执行以下命令:

# 1. 找到旧容器 ID
OLD_CONTAINER_ID="c6cfbe677343b"  # 替换为实际的容器 ID

# 2. 杀死容器进程
PID=$(crictl inspect $OLD_CONTAINER_ID 2>/dev/null | grep '"pid"' | head -1 | grep -oP ':\s*\K\d+')
[ -n "$PID" ] && kill -9 $PID

# 3. 杀死旧应用进程
ps aux | grep <app-name> | grep May | awk '{print $2}' | xargs -r kill -9

# 4. 找到 Sandbox ID 并杀死 shim
SANDBOX_ID=$(crictl inspect $OLD_CONTAINER_ID 2>/dev/null | grep '"sandboxID"' | grep -oP ':\s*"\K[^"]+')
ps aux | grep containerd-shim | grep "$SANDBOX_ID" | awk '{print $2}' | xargs -r kill -9

# 5. 停止 containerd
systemctl stop containerd
sleep 5

# 6. 清理元数据
rm -rf /run/containerd/runc/k8s.io/${SANDBOX_ID}*
rm -rf /run/containerd/io.containerd.runtime.v2.task/k8s.io/${OLD_CONTAINER_ID}*
rm -rf /run/containerd/io.containerd.grpc.v1.cri/sandboxes/${SANDBOX_ID}*
rm -rf /run/containerd/io.containerd.grpc.v1.cri/containers/${OLD_CONTAINER_ID}*

# 7. 重启 containerd
systemctl start containerd
sleep 30

# 8. 验证
crictl ps | grep <app-name>

终极方案:重启节点

如果上述方法都无法清理僵尸容器,可以重启节点:

# 在 master 节点执行
# 1. 标记节点为不可调度
kubectl cordon <node-name>

# 2. 驱逐节点上的 Pod
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --force --grace-period=0

# 3. 在目标节点上重启
reboot

# 4. 重启后,恢复节点调度
kubectl uncordon <node-name>

# 5. 验证节点状态
kubectl get nodes
kubectl get pods -n <namespace> -o wide

预防措施

1. 调整 Kubelet 超时配置

编辑 /var/lib/kubelet/config.yaml

runtimeRequestTimeout: 5m  # 从默认 2m 增加到 5m

重启 kubelet:

systemctl restart kubelet

2. 配置应用优雅关闭

在部署配置中添加:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 120  # 给应用足够时间关闭
      containers:
      - name: <container-name>
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                # 应用特定的关闭命令
                <app-stop-command>
                sleep 10

3. 合理配置资源限制

resources:
  requests:
    cpu: "1"
    memory: "2Gi"
  limits:
    cpu: "4"
    memory: "8Gi"

避免设置过高的资源限制导致节点内存压力过大。

4. 避免频繁 apply/delete

# ✗ 不推荐
kubectl delete -f app.yaml
kubectl apply -f app.yaml

# ✓ 推荐使用滚动更新
kubectl rollout restart deployment/<deployment-name>

# ✓ 或使用 patch
kubectl patch deployment <deployment-name> -p '{"spec":{"template":{"metadata":{"annotations":{"restarted-at":"'$(date +%s)'"}}}}}'

5. 监控和告警

设置监控告警:

  • 节点内存使用率 > 80%
  • Pod 重启次数异常
  • 出现 FailedKillPod 事件

常见问题 (FAQ)

Q1: 为什么会出现僵尸容器?

原因:

  • 频繁执行 kubectl apply/delete 导致容器无法正常终止
  • 应用关闭时间过长,超过 terminationGracePeriod
  • 系统资源不足(内存、磁盘)
  • Containerd/Docker 运行时故障

Q2: 清理僵尸容器会影响正在运行的服务吗?

影响:

  • ✅ 清理旧容器不影响新容器
  • ⚠️ 重启 containerd 会导致节点上所有容器短暂重启(30-60秒)
  • ✅ Kubernetes 会自动恢复 Pod
  • ⚠️ 建议在低峰期操作

Q3: 如何确认容器是"僵尸容器"?

判断标准:

  1. 创建时间很久(几个月前)
  2. 对应的 Pod 状态异常或已不存在
  3. 无法通过 crictl stop/rm 删除
  4. 进程处于僵尸状态(Z 或 defunct)

Q4: 清理后 Pod 一直重启怎么办?

排查步骤:

  1. 检查应用日志:kubectl logs <pod-name> -n <namespace>
  2. 检查节点资源:kubectl top nodes
  3. 检查存储卷:PVC 是否正常挂载
  4. 检查网络:Pod IP 是否分配正常

Q5: metrics-server 不可用怎么办?

解决方法:

# 重启 metrics-server
kubectl rollout restart deployment metrics-server -n kube-system

# 等待恢复
kubectl get pods -n kube-system | grep metrics-server

总结

关键步骤回顾

  1. 诊断:识别僵尸容器和异常进程
  2. 清理:杀死进程 → 停止 containerd → 清理元数据 → 重启 containerd
  3. 验证:确认容器和进程已清理,Pod 恢复正常
  4. 预防:调整配置,避免再次发生

操作检查清单

  • 备份重要数据和配置
  • 识别所有受影响的节点
  • 记录当前 Pod 状态
  • 逐节点执行清理(不要同时清理所有节点)
  • 验证每个节点清理成功
  • 检查 Pod 恢复状态
  • 验证应用功能正常
  • 配置预防措施

注意事项

⚠️ 重要警告:

  • 清理操作需要 root 权限
  • 建议在低峰期执行
  • 一次只清理一个节点
  • 确保有完整备份
  • 操作前通知相关团队

附录

A. 相关命令速查

# 容器管理
crictl ps                          # 列出容器
crictl inspect <container-id>      # 查看容器详情
crictl stop <container-id>         # 停止容器
crictl rm <container-id>           # 删除容器

# Pod 管理
kubectl get pods -n <ns> -o wide   # 查看 Pod
kubectl describe pod <pod> -n <ns> # Pod 详情
kubectl logs <pod> -n <ns>         # 查看日志
kubectl delete pod <pod> -n <ns> --force --grace-period=0  # 强制删除

# 节点管理
kubectl cordon <node>              # 标记不可调度
kubectl drain <node>               # 驱逐 Pod
kubectl uncordon <node>            # 恢复调度

# 系统服务
systemctl status containerd        # 查看状态
systemctl restart containerd       # 重启服务
journalctl -u containerd -n 100    # 查看日志

B. 日志位置

# Containerd 日志
/var/log/containerd.log
journalctl -u containerd

# Kubelet 日志
/var/log/kubelet.log
journalctl -u kubelet

# Pod 日志
/var/log/pods/<namespace>_<pod-name>_<uid>/<container-name>/

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐