Skip to content

K8s 节点镜像垃圾回收 (GC) 失败故障知识库

故障概述

故障现象

在 Kubernetes 集群日常巡检或监控告警中,发现节点频繁出现以下警告事件:

  • Event Type: Warning
  • Reason: ImageGCFailed
  • Message: unable to remove repository reference "xxx" (must force) - container xxx is using its referenced image xxx
  • 伴随现象: 节点状态在 NodeHasNoDiskPressureNodeHasDiskPressure 之间反复切换

同时可能观察到:

  • 节点磁盘使用率持续高于 85%
  • 新 Pod 调度到该节点后无法拉取镜像
  • 极端情况下触发 Pod 驱逐(Eviction)

故障等级

P2 - 重要告警

虽然此类故障通常不会直接导致业务中断(因为已有容器仍在正常运行),但存在以下风险:

  • 磁盘空间持续紧张,可能影响节点稳定性
  • 新业务部署或扩缩容时镜像拉取失败
  • 长期不处理可能演变为 P1 级故障(如触发大规模 Pod 驱逐)

影响范围

  • 直接影响: 单节点磁盘空间无法有效释放
  • 潜在影响:
    • 该节点上新 Pod 的创建和调度
    • 可能触发 kubelet 的主动驱逐机制
    • 集群整体资源利用率下降

报错原理深度分析

一、Kubernetes 镜像 GC 机制详解

1.1 为什么需要镜像 GC?

在 Kubernetes 集群中,节点会不断拉取新的容器镜像。随着时间推移:

  • 旧版本应用镜像不再使用
  • 测试镜像被废弃
  • 镜像标签更新产生历史版本

如果不进行清理,这些"悬空"镜像会持续占用磁盘空间,最终导致节点磁盘耗尽。为此,kubelet 内置了镜像垃圾回收(Garbage Collection)机制。

1.2 GC 触发条件

kubelet 通过两个阈值控制镜像 GC 行为:

参数名默认值含义
imageGCHighThresholdPercent85%当磁盘使用率超过此阈值时,开始执行 GC
imageGCLowThresholdPercent80%GC 的目标是将磁盘使用率降至该阈值以下

工作流程说明

正常状态:磁盘使用率 < 85%

某时刻磁盘使用率达到 86%(超过高阈值)

kubelet 检测到条件满足,触发镜像 GC 流程

GC 开始扫描并识别"可删除"的镜像

删除未被任何容器引用的镜像

持续检查磁盘使用率

当磁盘使用率降至 80% 以下(低阈值),GC 暂停

等待下一次触发

1.3 什么样的镜像会被 GC 删除?

kubelet 判断镜像是否可以删除的逻辑是:

核心原则: 只有未被任何容器引用的镜像才会被 GC 清理

具体判断标准:

  • 可删除: 没有任何容器(包括运行中和已停止的)引用该镜像
  • 不可删除: 至少有一个容器正在使用该镜像(即使 Pod 已经删除,但容器未清理)

这里的"引用"是指:容器的根文件系统基于该镜像创建,Docker 层面存在依赖关系。


二、GC 失败的常见原因剖析

根据实际运维经验,镜像 GC 失败主要有以下几种场景:

2.1 场景一:镜像被运行中的容器引用(本次故障类型)

典型错误信息

ImageGCFailed: unable to remove repository reference 
"dkr-registry:5000/chrony:v3.5" (must force) - 
container 7e9f6007bbee is using its referenced image 15d4fd598132

详细原理解读

这种情况通常发生在以下场景:

  1. Pod 已删除但容器残留
    • 用户执行 kubectl delete pod 后,Pod 对象被移除
    • 但由于某种原因(如节点网络分区、kubelet 异常等),对应的 Docker 容器未被清理
    • 该容器仍然持有原镜像的引用锁
  2. 长周期运行的系统容器
    • 某些系统组件(如 chrony、node-exporter 等)以 DaemonSet 形式运行
    • 升级后旧版本容器未及时清理
    • 新旧容器共存,旧容器仍占用旧镜像
  3. 容器异常僵死
    • 容器进程异常但未完全退出
    • 处于 DeadZombie 状态
    • Docker 无法正常回收其资源

为什么不能强制删除?

从技术角度,Docker 确实提供了 docker rmi -f 强制删除选项。但在 Kubernetes 环境中,强烈不建议这样做,原因是:

  • 强制删除可能导致运行中容器文件系统损坏
  • 容器可能进入不可预测的状态
  • 最坏情况下需要重启整个节点才能恢复

因此,正确的做法是先清理容器,再让 GC 自动回收镜像

2.2 场景二:多个镜像标签指向同一镜像层

现象描述

bash
$ docker images
REPOSITORY          TAG       IMAGE ID       SIZE
dkr-registry:5000/chrony   v3.5      15d4fd598132   50MB
dkr-registry:5000/chrony   latest    15d4fd598132   50MB

可以看到,v3.5latest 两个标签实际上指向同一个镜像 ID(15d4fd598132)。

问题分析

  • Docker 的镜像存储是分层的,多个标签可以引用同一组镜像层
  • 删除其中一个标签(如 v3.5)并不会释放磁盘空间
  • 只有当所有标签都被删除,且没有容器引用时,镜像层才会被真正清理

排查方法

bash
# 查看镜像的所有标签
docker inspect 15d4fd598132 | jq '.[0].RepoTags'

2.3 场景三:镜像层被多个镜像共享

现代容器镜像采用分层存储机制,基础镜像层(如 ubuntu:20.04)可能被数十个应用镜像共享。

问题表现

  • 删除了某个应用镜像
  • 但磁盘空间几乎没有变化

原因

  • 该镜像的基础层仍被其他镜像引用
  • Docker 只会删除独占的层,共享层会保留

这种情况严格来说不算"GC 失败",而是符合预期的行为。但如果运维人员不了解这一机制,可能会误以为 GC 没有工作。


三、磁盘压力连锁反应机制

理解磁盘压力如何一步步影响集群稳定性,对于制定应对策略至关重要。

3.1 连锁反应流程图

┌─────────────────────────────────────────────────────────────┐
│  阶段 1: 磁盘使用率逐渐上升                                   │
│  - 新镜像不断拉取                                            │
│  - 旧镜像未被清理                                            │
│  - 容器日志累积                                              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  阶段 2: 触发 GC 高阈值 (>85%)                                │
│  - kubelet 检测到磁盘使用率超标                              │
│  - 启动镜像 GC 流程                                          │
│  - 尝试删除未引用的镜像                                      │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  阶段 3: GC 失败(本故障的核心)                               │
│  - 发现目标镜像被容器引用                                    │
│  - 无法安全删除,返回错误                                    │
│  - 磁盘空间未能释放                                          │
│  - 错误事件被记录并上报告警                                  │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  阶段 4: 磁盘压力状态激活                                     │
│  - 磁盘使用率持续高于阈值                                    │
│  - kubelet 将节点状态标记为 DiskPressure                     │
│  - 节点状态同步到 API Server                                 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  阶段 5: 调度器感知并规避                                     │
│  - 调度器看到节点 DiskPressure=True                          │
│  - 不再将新 Pod 调度到该节点                                 │
│  - 集群有效容量下降                                          │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  阶段 6 (最坏情况): 触发 Pod 驱逐                             │
│  - 磁盘使用率继续上升至更高级别阈值                          │
│  - kubelet 开始主动驱逐 Pod(通常是 QoS 较低的)              │
│  - 业务受到影响                                                │
└─────────────────────────────────────────────────────────────┘

3.2 关键观察点

从上述流程可以看出,阶段 3(GC 失败)是干预的最佳时机

  • 此时业务尚未受影响
  • 有充足的时间进行诊断和处理
  • 操作风险相对较低

一旦进入阶段 5 或 6,就需要紧急响应,且可能需要牺牲部分业务来恢复节点健康。


完整排查步骤(实战版)

下面是一套经过生产环境验证的标准化排查流程。建议按顺序执行,每一步都确认清楚后再进入下一步。

Step 1: 确认 Docker 数据目录位置

⚠️ 重要提醒:很多运维同学习惯性地直接执行 df -h /var/lib/docker,这是错误的做法!

Docker 的数据目录可以通过配置文件自定义,不同环境的配置可能不同。盲目假设会导致误判。

1.1 查看 Docker 配置文件

Docker 的主配置文件位于 /etc/docker/daemon.json,其中 data-root(旧版本叫 graph)参数指定了数据目录位置。

bash
# 查看配置文件内容
cat /etc/docker/daemon.json

典型配置示例

json
{
  "registry-mirrors": ["https://docker.mirror.example.com"],
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  },
  "data-root": "/data/docker"
}

注意最后一行 "data-root": "/data/docker",这说明 Docker 的数据实际存储在 /data/docker,而不是默认的 /var/lib/docker

1.2 如果配置文件不存在或为空

有些环境可能没有 daemon.json 文件,此时 Docker 使用默认配置,数据目录为 /var/lib/docker

可以用以下命令确认:

bash
# 查看 Docker 服务启动参数
systemctl show docker | grep ExecStart

# 或者使用 docker info 查看
docker info | grep "Docker Root Dir"

输出示例:

Docker Root Dir: /data/docker

1.3 确认真实的数据目录路径

综合以上信息,我们才能确定要检查的磁盘分区。假设我们得到的是 /data/docker,那么后续所有操作都要基于这个路径。


Step 2: 评估磁盘使用情况

确认真实的数据目录后,现在可以检查磁盘使用状况了。

2.1 查看磁盘分区和使用率

bash
# 假设数据目录是 /data/docker
df -h /data/docker

输出示例:

Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb1       500G  450G   50G  90% /data

关键指标解读

  • Use% 达到 90%,已经超过 GC 高阈值(85%),这解释了为什么会触发 GC
  • Avail 只剩 50G,确实比较紧张

2.2 分析 Docker 各组件空间占用

bash
# 查看 Docker 各类型资源的总体占用
docker system df

输出示例:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          156       45        120GB     80GB (66%)
Containers      89        45        2.5GB     1.2GB (48%)
Local Volumes   23        12        15GB      8GB (53%)
Build Cache     -         -         5.2GB     5.2GB

分析要点

  • Images 行显示有 120GB 镜像,其中 80GB 理论上可回收(但可能因为本次故障无法实际回收)
  • Containers 行显示有 44 个非活跃容器(89-45),占用 1.2GB,这部分是可以安全清理的
  • Local Volumes 也有 8GB 可回收

2.3 深入分析各目录占用

bash
# 查看 Docker 数据目录下各子目录的大小
du -sh /data/docker/*

# 或者更详细的排序
du -ah /data/docker | sort -rh | head -20

常见目录说明:

  • containers/: 容器相关数据
  • image/: 镜像元数据和层
  • volumes/: 数据卷
  • overlay2/(或其他存储驱动目录): 实际的文件系统层

Step 3: 定位问题容器

从 Step 1 收集到的告警事件中,我们已经知道了关键信息:

  • 问题镜像:dkr-registry:5000/chrony:v3.5
  • 占用容器 ID:7e9f6007bbee

现在需要深入了解这个容器的情况。

3.1 查看容器基本信息

bash
CONTAINER_ID="7e9f6007bbee"

# 查看容器详细信息
docker inspect $CONTAINER_ID

重点关注以下字段:

json
{
  "Config": {
    "Image": "dkr-registry:5000/chrony:v3.5",
    "Labels": {
      "io.kubernetes.container.name": "chrony",
      "io.kubernetes.pod.name": "chrony-daemonset-abc123",
      "io.kubernetes.pod.namespace": "kube-system"
    }
  },
  "State": {
    "Status": "running",
    "Running": true,
    "StartedAt": "2026-02-15T08:30:00Z"
  },
  "Created": "2026-02-15T08:29:55Z"
}

关键信息提取

  • 容器所属 Pod:chrony-daemonset-abc123
  • 命名空间:kube-system
  • 容器名称:chrony
  • 运行状态:running(正在运行)
  • 启动时间:2 月 15 日,已运行约 25 天

3.2 查找对应的 Kubernetes Pod

bash
# 根据 Pod 名称查找
kubectl get pod chrony-daemonset-abc123 -n kube-system -o wide

# 或者通过标签搜索
kubectl get pods -n kube-system -l app=chrony -o wide

可能的情况

情况 A:Pod 仍然存在

  • 说明这是当前正在运行的合法 Pod
  • 不能简单删除容器,需要考虑其他方式

情况 B:Pod 已被删除

  • 说明这是一个"孤儿容器"
  • 可以安全删除

3.3 评估容器的重要性

chrony 是时间同步服务,通常是集群基础设施的一部分。在决定如何处理之前,需要确认:

bash
# 查看容器日志
docker logs --tail 100 $CONTAINER_ID

# 查看容器资源使用
docker stats $CONTAINER_ID --no-stream

# 检查是否有其他 chrony 实例在运行
kubectl get pods -n kube-system -l app=chrony

如果发现有多个 chrony Pod 在其他节点正常运行,且该容器的 Pod 已经被新版本的 Pod 替代,那么可以安全清理。


Step 4: 检查镜像引用关系

为了全面理解问题,还需要分析问题镜像的引用情况。

bash
# 查看所有包含 chrony 的镜像
docker images | grep chrony

# 查看特定镜像的详细信息
docker inspect dkr-registry:5000/chrony:v3.5 | jq '.[0].RepoTags'

如果输出显示多个标签:

json
[
  "dkr-registry:5000/chrony:v3.5",
  "dkr-registry:5000/chrony:latest"
]

说明这两个标签指向同一个镜像,只删除一个标签不会释放空间。


解决方案(生产环境验证版)

根据实际情况和风险评估,提供以下几种解决方案。请严格按照推荐顺序考虑

方案一:清理磁盘空间(首选方案)⭐

这是最安全、最推荐的方案。核心思路是:不直接处理问题镜像,而是清理其他可释放的空间,让磁盘使用率降到 GC 阈值以下

适用场景

  • 问题容器是重要的系统组件,不能随意删除
  • 业务高峰期,需要最小化风险
  • 有其他可清理的空间(如旧日志、悬空卷等)

操作步骤

1. 清理已停止的容器

这些容器已经不再运行,但它们的历史数据仍占用空间。

bash
# 查看所有已停止的容器
docker ps -a | grep Exit

# 统计数量
docker ps -a | grep Exit | wc -l

# 删除所有已停止的容器
docker container prune -f

# 验证
docker ps -a | wc -l

预期效果:根据 Step 2 的分析,这一步可能释放 1-2GB 空间。

2. 清理悬空镜像(dangling images)

悬空镜像是指没有被任何容器引用、也没有标签指向的镜像层。

bash
# 查看悬空镜像
docker images -f dangling=true

# 删除悬空镜像
docker image prune -f

注意:这不会删除有标签的镜像,只清理无主的镜像层。

3. 清理未使用的数据卷(谨慎操作)

数据卷可能包含持久化数据,清理前务必确认!

bash
# 查看未使用的卷(没有被任何容器挂载)
docker volume ls -f dangling=true

# 逐个检查后再删除
for vol in $(docker volume ls -f dangling=true -q); do
  echo "=== Volume: $vol ==="
  docker volume inspect $vol | jq '.[0].Mountpoint'
  # 确认无误后再删除
  # docker volume rm $vol
done

# 确认安全后批量删除
docker volume prune -f

⚠️ 警告:某些应用可能将重要数据存放在卷中,即使容器已删除。务必先检查卷的内容!

4. 清理容器日志

容器日志文件可能非常大,特别是没有配置日志轮转的环境。

bash
# 查找大的日志文件
find /data/docker/containers -name "*.log" -size +100M

# 查看具体是哪个容器的日志
ls -lh /data/docker/containers/<container_id>/<container_id>-json.log

# 清空日志(不要删除文件,而是清空内容)
truncate -s 0 /data/docker/containers/<container_id>/<container_id>-json.log

更好的做法:配置 Docker 日志轮转,防止未来再次出现此问题。

编辑 /etc/docker/daemon.json

json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}

然后重启 Docker:

bash
systemctl restart docker
5. 清理构建缓存(如果有)

如果节点上执行过 docker build,可能会有构建缓存。

bash
# 查看构建缓存
docker builder df

# 清理构建缓存
docker builder prune -f
6. 验证磁盘空间释放
bash
# 重新检查磁盘使用率
df -h /data/docker

# 检查是否仍高于阈值
# 理想情况:降至 80% 以下

如果磁盘使用率成功降至 80% 以下,kubelet 会自动停止 GC,告警也会消失。


方案二:磁盘扩容(根本解决方案)⭐⭐

如果清理后空间仍然紧张,或者预计未来业务增长会持续消耗空间,建议进行磁盘扩容。这是从根本上解决问题的方案。

适用场景

  • 清理后可用空间仍然不足
  • 业务持续增长,磁盘需求不断增加
  • 当前磁盘规划不合理(如初始分配过小)

扩容方式选择

方式 A:在线扩容(推荐,业务无感知)

如果底层存储支持(如云盘、LVM),可以在线扩容。

云环境示例(以百度云为例)

  1. 在云平台控制台扩大云盘容量
  2. 登录节点扩展文件系统
bash
# 查看磁盘变化
lsblk

# 假设 /dev/vdb 从 500G 扩展到 1000G
# 扩展分区(如果需要)
growpart /dev/vdb 1

# 扩展文件系统(根据文件系统类型选择命令)
# ext4
resize2fs /dev/vdb1

# xfs
xfs_growfs /data

验证

bash
df -h /data/docker
方式 B:添加新磁盘并迁移

如果无法在线扩容,可以添加新磁盘并迁移 Docker 数据目录。

步骤

  1. 添加新磁盘并挂载
bash
# 假设新磁盘为 /dev/vdc
mkfs.xfs /dev/vdc
mount /dev/vdc /data2
  1. 停止 Docker 服务
bash
systemctl stop docker
  1. 迁移数据
bash
rsync -avz /data/docker/ /data2/docker/
  1. 修改 Docker 配置

编辑 /etc/docker/daemon.json

json
{
  "data-root": "/data2/docker"
}
  1. 启动 Docker 并验证
bash
systemctl start docker
docker info | grep "Docker Root Dir"
docker ps  # 确认所有容器正常启动
  1. (可选)清理旧数据

确认一切正常后,可以卸载并清理旧磁盘。


方案三:调整 GC 阈值(临时缓解方案)

如果短期内无法扩容,且可清理空间有限,可以调整 GC 阈值,让 kubelet 更积极地回收空间。

适用场景

  • 磁盘空间确实紧张,但暂时无法扩容
  • 作为临时措施,为扩容争取时间
  • 节点上镜像更新频繁,需要更积极的 GC

操作步骤

1. 查看当前 kubelet 配置

kubelet 的配置方式因安装方式而异:

方式 A:通过配置文件

bash
# 查找 kubelet 配置文件
find /etc/kubernetes -name "kubelet-config.yaml"
# 或
find /etc/kubernetes -name "config.yaml"

方式 B:通过 systemd 配置

bash
# 查看 kubelet 启动参数
systemctl cat kubelet

方式 C:通过 KubeletConfiguration CR(K8s 1.11+)

bash
kubectl get kubeletconfiguration -o yaml
2. 修改 GC 阈值

在配置文件中找到或添加以下参数:

yaml
imageGCHighThresholdPercent: 80
imageGCLowThresholdPercent: 70

或者如果是命令行参数形式:

bash
--image-gc-high-threshold=80 --image-gc-low-threshold=70

参数说明

  • imageGCHighThresholdPercent: 80:磁盘使用率达到 80% 就开始 GC(原来是 85%)
  • imageGCLowThresholdPercent: 70:GC 目标是降到 70% 以下(原来是 80%)
3. 重启 kubelet
bash
# 重新加载配置
systemctl daemon-reload

# 重启 kubelet
systemctl restart kubelet

# 验证状态
systemctl status kubelet
4. 观察效果
bash
# 查看节点事件
kubectl describe node <node-name> | grep -A 5 "Events"

# 监控磁盘使用率变化
watch -n 5 'df -h /data/docker'

⚠️ 注意事项

  1. GC 频率增加可能影响性能
    • 更频繁的 GC 会消耗更多 CPU 和 I/O
    • 在镜像拉取频繁的环境中尤为明显
  2. 这不是长期解决方案
    • 只是延缓问题爆发的时间
    • 根本解决还是需要扩容或清理
  3. 建议在低峰期操作
    • 避免在业务高峰期调整
    • 给 kubelet 一些时间适应新配置

预防措施与最佳实践

一、建立定期清理机制

1. 部署自动化清理脚本

创建脚本 /usr/local/bin/k8s-node-cleanup.sh

bash
#!/bin/bash
#
# K8s 节点定期清理脚本
# 功能:清理已停止容器、悬空镜像、旧日志
#

set -e

LOG_FILE="/var/log/k8s-cleanup.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

log() {
  echo "[$TIMESTAMP] $1" | tee -a $LOG_FILE
}

log "=== 开始节点清理 ==="

# 1. 清理已停止的容器
STOPPED_COUNT=$(docker ps -a | grep Exit | wc -l)
if [ $STOPPED_COUNT -gt 0 ]; then
  log "发现 $STOPPED_COUNT 个已停止的容器,开始清理..."
  docker container prune -f
  log "已停止容器清理完成"
else
  log "无需清理已停止容器"
fi

# 2. 清理悬空镜像
DANGLING_COUNT=$(docker images -f dangling=true -q | wc -l)
if [ $DANGLING_COUNT -gt 0 ]; then
  log "发现 $DANGLING_COUNT 个悬空镜像,开始清理..."
  docker image prune -f
  log "悬空镜像清理完成"
else
  log "无需清理悬空镜像"
fi

# 3. 清理构建缓存
BUILD_CACHE=$(docker builder du | tail -1)
log "构建缓存占用:$BUILD_CACHE"
docker builder prune -f
log "构建缓存清理完成"

# 4. 检查磁盘使用率
DISK_USAGE=$(df /data/docker | tail -1 | awk '{print $5}')
log "当前磁盘使用率:$DISK_USAGE"

# 5. 记录清理结果
log "=== 清理完成 ==="
log ""

# 如果磁盘使用率仍然很高,发送告警
THRESHOLD=80
USAGE_NUM=${DISK_USAGE%\%}
if [ $USAGE_NUM -gt $THRESHOLD ]; then
  log "⚠️ 警告:磁盘使用率 ${DISK_USAGE} 仍高于阈值 ${THRESHOLD}%"
  # 这里可以集成告警通知,如发送如流消息、邮件等
fi

赋予执行权限:

bash
chmod +x /usr/local/bin/k8s-node-cleanup.sh

2. 配置定时任务

编辑 crontab:

bash
crontab -e

添加以下内容(每周日凌晨 2 点执行):

0 2 * * 0 /usr/local/bin/k8s-node-cleanup.sh

3. 监控清理效果

可以在 Grafana 中创建仪表板,跟踪以下指标:

  • 节点磁盘使用率趋势
  • 镜像数量变化
  • 容器数量变化
  • 清理脚本执行日志

二、优化镜像管理策略

1. 使用具体的版本号标签

❌ 不推荐

bash
docker pull dkr-registry:5000/myapp:latest

✅ 推荐

bash
docker pull dkr-registry:5000/myapp:v1.2.3

原因:

  • latest 标签会不断指向新版本,旧版本的镜像成为"悬空"状态
  • 具体版本号便于追溯和回滚
  • 更容易实施镜像保留策略

2. 实施镜像保留策略

对于 CI/CD 流水线构建的镜像,建议:

  • 只保留最近 N 个版本(如最近 5 个)
  • 定期清理超过一定时间的旧版本
  • 使用镜像仓库的生命周期管理功能

示例(Harbor 仓库):

  • 配置保留策略:保留最近 10 个 Tag
  • 启用自动清理:每天凌晨清理过期镜像

3. 多环境镜像隔离

  • 开发环境镜像使用独立仓库或命名空间
  • 生产环境镜像严格管控,禁止随意推送
  • 定期审计各环境镜像使用情况

三、完善监控告警体系

1. Prometheus 告警规则

在 Prometheus 中添加以下告警规则:

yaml
groups:
- name: k8s-node-storage
  rules:
  # 磁盘使用率告警
  - alert: NodeDiskUsageHigh
    expr: (node_filesystem_size_bytes{mountpoint="/data"} - node_filesystem_avail_bytes{mountpoint="/data"}) / node_filesystem_size_bytes{mountpoint="/data"} * 100 > 80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "节点 {{ $labels.instance }} 磁盘使用率过高"
      description: "磁盘使用率 {{ $value }}%,已超过 80% 阈值"

  - alert: NodeDiskUsageCritical
    expr: (node_filesystem_size_bytes{mountpoint="/data"} - node_filesystem_avail_bytes{mountpoint="/data"}) / node_filesystem_size_bytes{mountpoint="/data"} * 100 > 90
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "节点 {{ $labels.instance }} 磁盘使用率严重过高"
      description: "磁盘使用率 {{ $value }}%,已超过 90% 临界值"

  # 镜像 GC 失败告警
  - alert: KubeletImageGCFailed
    expr: increase(kubelet_image_gc_failed_total[5m]) > 5
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "节点 {{ $labels.node }} 镜像 GC 频繁失败"
      description: "过去 5 分钟内发生 {{ $value }} 次 GC 失败"

  # 节点磁盘压力状态告警
  - alert: NodeHasDiskPressure
    expr: kube_node_status_condition{condition="DiskPressure",status="true"} == 1
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "节点 {{ $labels.node }} 处于磁盘压力状态"
      description: "节点可能开始驱逐 Pod"

2. 告警通知配置

根据告警级别配置不同的通知渠道:

告警级别通知方式通知对象响应时间要求
Warning如流 + 邮件值班人员1 小时内
Critical电话 + 如流 + 邮件值班 + 负责人15 分钟内

四、容量规划与预测

1. 建立容量基线

定期(如每月)统计以下数据:

指标当前值警戒线行动线
磁盘使用率-70%80%
镜像总数-100 个150 个
容器总数-50 个80 个
停止容器数-10 个30 个
数据卷数量-20 个40 个

2. 趋势分析与预测

基于历史数据,预测未来容量需求:

bash
# 示例:计算过去 30 天磁盘增长趋势
# 假设有监控系统可以提供历史数据

# 平均每日增长 = (当前使用量 - 30 天前使用量) / 30
# 预计耗尽天数 = (总容量 - 当前使用量) / 平均每日增长

如果预计 3 个月内会达到警戒线,应提前规划扩容。

3. 制定扩容计划

根据业务发展规划:

  • 新业务上线带来的镜像增长
  • 现有业务的自然增长
  • 季节性波动(如大促期间)

提前预留 30-50% 的缓冲空间。


故障复盘模板

故障基本信息

项目内容
故障时间2026-03-11 17:00 - 17:30
故障等级P2
影响节点p0-lkg-data14
发现方式监控告警
处理人历飞雨

故障时间线

时间事件备注
17:00监控系统触发 ImageGCFailed 告警首次告警
17:05值班人员确认告警,开始排查-
17:10定位为容器占用镜像导致 GC 失败容器 ID: 7e9f6007bbee
17:15确认该容器为 chrony 系统组件不能直接删除
17:20执行磁盘清理操作清理已停止容器和悬空镜像
17:25磁盘使用率降至 78%GC 恢复正常
17:30告警消除,故障恢复-

根因分析

直接原因

节点上存在一个运行中的 chrony 容器(ID: 7e9f6007bbee),该容器引用了旧版本镜像 dkr-registry:5000/chrony:v3.5。当磁盘使用率超过 85% 触发 GC 时,由于该镜像被容器引用而无法删除,导致 GC 失败。

根本原因

  1. 缺乏定期清理机制:节点上没有部署自动化清理脚本,已停止的容器和悬空镜像长期累积
  2. 监控告警不完善:缺少对磁盘使用率趋势的预警,等到触发 GC 失败时才发现问题
  3. 镜像管理不规范:chrony DaemonSet 升级后,旧版本容器未被及时清理

影响评估

  • 业务影响:无直接业务影响
  • 潜在风险:如不及时处理,可能触发 Pod 驱逐
  • 持续时间:30 分钟
  • 影响范围:单节点

改进措施

序号措施负责人预计完成时间状态
1部署节点定期清理脚本陈欢2026-03-15待办
2优化 Prometheus 告警规则陈欢2026-03-12待办
3更新 chrony DaemonSet 配置陈欢2026-03-12待办
4制定镜像管理规范运维团队2026-03-20待办
5建立容量规划机制运维团队2026-03-25待办

经验教训

  1. 预防胜于治疗:定期清理比故障后紧急处理更安全、成本更低
  2. 监控要前置:不要等到 GC 失败才告警,应该在磁盘使用率达到 70% 时就预警
  3. 自动化是关键:人工清理容易遗漏,应该用脚本固化最佳实践
  4. 文档要及时更新:将本次故障的处理过程整理成知识库,方便团队学习

附录:常用命令速查表

节点状态查询

bash
# 查看节点详细信息
kubectl describe node <node-name>

# 查看节点事件
kubectl get events --field-selector involvedObject.name=<node-name> --sort-by='.lastTimestamp'

# 查看节点状态条件
kubectl get node <node-name> -o jsonpath='{.status.conditions}' | jq

Docker 空间管理

bash
# 查看 Docker 系统总览
docker system df

# 查看详细空间使用
docker system df -v

# 清理已停止容器
docker container prune -f

# 清理悬空镜像
docker image prune -f

# 清理未使用卷
docker volume prune -f

# 清理构建缓存
docker builder prune -f

# 一键清理所有(谨慎使用)
docker system prune -a --volumes

磁盘分析

bash
# 查看磁盘使用率
df -h /data/docker

# 查看目录大小
du -sh /data/docker/*

# 查找大文件
find /data/docker -type f -size +100M -exec ls -lh {} \;

# 按大小排序目录
du -ah /data/docker | sort -rh | head -20

容器和镜像查询

bash
# 查看所有容器(包括已停止)
docker ps -a

# 查看已停止容器
docker ps -a | grep Exit

# 查看容器详细信息
docker inspect <container-id>

# 查看镜像列表
docker images

# 查看镜像详细信息
docker inspect <image-id>

# 查找引用某镜像的容器
docker ps -a --filter ancestor=<image-name>

kubelet 配置查询

bash
# 查看 kubelet 启动参数
systemctl cat kubelet

# 查看 kubelet 配置文件
cat /var/lib/kubelet/config.yaml

# 通过 API 查询(K8s 1.11+)
kubectl get kubeletconfiguration -o yaml