K8s 生产环境 10 大故障复盘!全是线上踩过的血泪坑

这篇文章记录了我这些年在 K8s 生产环境踩过的坑。每一个案例都是血泪教训,有些甚至导致了生产事故。希望通过分享这些经历,能帮助大家避免重蹈覆辙。

一、概述

1.1 背景介绍

K8s 已经成为容器编排的事实标准,但它的复杂性也带来了很多潜在的风险点。根据我的经验,生产环境出问题往往不是因为 K8s 本身有 bug,而是:

  • 配置不当
  • 资源规划不合理
  • 缺乏监控告警
  • 操作流程不规范
  • 对某些机制理解不深

1.2 文章结构

本文将按照故障严重程度,从"集群级灾难"到"应用级问题",逐一复盘 10 个真实案例:

序号故障类型影响范围严重程度
1etcd 磁盘写满整个集群P0 - 灾难
2API Server OOM整个集群P0 - 灾难
3证书过期整个集群P0 - 灾难
4节点批量 NotReady多节点P1 - 严重
5PDB 配置导致无法驱逐应用级P1 - 严重
6资源配额耗尽命名空间P2 - 中等
7滚动更新卡住应用级P2 - 中等
8ConfigMap 热更新踩坑应用级P2 - 中等
9HPA 抖动风暴应用级P2 - 中等
10镜像拉取失败雪崩多应用P2 - 中等

1.3 环境信息

组件版本说明
Kubernetes1.28 - 1.30不同案例发生在不同版本
etcd3.5.x集群数据存储
容器运行时containerd 1.7+部分老集群还在用 Docker
节点规模50 - 500 节点中大规模集群

二、集群级灾难案例

2.1 案例一:etcd 磁盘写满,集群瘫痪

故障现象

那是一个周五下午(没错,就是最经典的周五下午),监控突然开始疯狂告警:

[CRITICAL] API Server 响应超时
[CRITICAL] 多个节点状态变为 Unknown
[CRITICAL] 新 Pod 无法调度

登录 master 节点一看,kubectl 命令卡住不动,整个集群基本瘫痪了。

根因分析

# 检查 etcd 状态
systemctl status etcd
# 输出:etcd.service: Failed with result 'exit-code'
# 查看 etcd 日志
journalctl -u etcd -n 100
# 关键错误:
# "mvcc: database space exceeded"
# "etcdserver: no space"
# 检查磁盘使用
df -h /var/lib/etcd
# 输出:100% 使用率

根本原因:etcd 数据目录所在磁盘写满了。

为什么会写满?排查发现是因为:

  1. 某个 Operator 存在 bug,疯狂创建 Event 对象
  2. etcd 没有配置自动压缩(compaction)
  3. 磁盘容量规划不足,只给了 20GB

解决过程


# 1. 紧急扩容磁盘(如果是云环境)
# 或者清理其他文件腾出空间
# 2. 手动压缩 etcd
# 获取当前 revision
ETCDCTL_API=3 etcdctl endpoint status --write-out=table
# 压缩到指定 revision
ETCDCTL_API=3 etcdctl compact <revision>
# 3. 执行碎片整理
ETCDCTL_API=3 etcdctl defrag --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key
# 4. 清理过多的 Event
kubectl delete events --all -A

预防措施

# 1. 配置 etcd 自动压缩(在 etcd 启动参数中)
--auto-compaction-mode=periodic
--auto-compaction-retention=1h
# 2. 设置配额告警592# 8GB
# 3. 添加磁盘监控告警
# Prometheus 告警规则
-alert:EtcdDiskSpaceLow
expr:(etcd_mvcc_db_total_size_in_bytes/etcd_server_quota_backend_bytes)>0.8
for:5m
labels:
    severity:critical
annotations:
    summary:"etcd 磁盘空间即将耗尽"

教训:etcd 是 K8s 的心脏,一定要重点监控。建议磁盘至少预留 50GB,并配置自动压缩。


2.2 案例二:API Server OOM,集群失联

故障现象

某天早上 9 点,业务高峰期,突然收到告警:

[CRITICAL] kube-apiserver 进程重启
[CRITICAL] 大量 Pod 状态 Unknown
[WARNING] kubectl 命令响应缓慢

查看监控发现 API Server 内存使用率飙升到 100%,然后被 OOM Killer 干掉了。

根因分析

# 查看 API Server 日志
journalctl -u kube-apiserver -n 200 | grep -i "oom\|memory\|killed"
# 查看系统日志
dmesg | grep -i "out of memory"
# 输出:Out of memory: Killed process 12345 (kube-apiserver)
# 检查 API Server 内存配置
ps aux | grep kube-apiserver
# 发现没有设置内存限制

根本原因:有人写了一个脚本,用 kubectl get pods -A -o json 获取所有 Pod 的完整信息,而且是每 10 秒执行一次。集群有 5000+ Pod,每次请求返回的 JSON 数据量巨大,API Server 内存被撑爆了。

更坑的是,这个脚本还是在多个节点上并行运行的…

解决过程

# 1. 找出问题请求
# 开启 API Server 审计日志
# 在 kube-apiserver 配置中添加:
--audit-log-path=/var/log/kubernetes/audit.log
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
# 2. 分析审计日志,找出大请求
cat /var/log/kubernetes/audit.log | jq 'select(.responseStatus.code == 200) | {user: .user.username, verb: .verb, resource: .objectRef.resource}' | sort | uniq -c | sort -rn | head -20
# 3. 限制 API Server 资源
# 对于 kubeadm 部署的集群,修改 /etc/kubernetes/manifests/kube-apiserver.yaml
resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "2"
    memory: "4Gi"# 根据实际情况调整
# 4. 配置请求限流
--max-requests-inflight=400
--max-mutating-requests-inflight=200

预防措施


# 1. API 优先级和公平性配置(APF)
apiVersion:flowcontrol.apiserver.k8s.io/v1beta3
kind:FlowSchema
metadata:
name:restrict-list-all
spec:
priorityLevelConfiguration:
    name:low-priority
matchingPrecedence:1000
rules:
-subjects:
    -kind:ServiceAccount
      serviceAccount:
        name:"*"
        namespace:"*"
    resourceRules:
    -verbs:["list","watch"]
      apiGroups:["*"]
      resources:["pods","events"]
      namespaces:["*"]
      
# 2. 监控 API Server 内存
# Prometheus 告警规则
- alert: APIServerHighMemory
  expr: process_resident_memory_bytes{job="kube-apiserver"} > 3e9
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "API Server 内存使用过高"

教训:永远不要在生产环境无限制地 list 所有资源。使用 label selector、field selector 或分页查询。


2.3 案例三:证书过期,一夜回到解放前

故障现象

周一早上来上班,发现所有 kubectl 命令都报错:

Unable to connect to the server: x509: certificate has expired or is not yet valid

整个集群完全无法操作,业务虽然还在跑(已有的 Pod),但无法进行任何变更。

根因分析

# 检查证书有效期
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates
# 输出:
# notBefore=Jan 15 00:00:00 2025 GMT
# notAfter=Jan 15 00:00:00 2026 GMT  # 已过期!
# 检查所有证书
kubeadm certs check-expiration

根本原因:kubeadm 创建的证书默认有效期是 1 年,我们忘记续期了。更惨的是,这个集群是一年前搭建的,当时没有设置证书过期告警…

解决过程

# 1. 备份现有证书
cp -r /etc/kubernetes/pki /etc/kubernetes/pki.bak
# 2. 续期所有证书
kubeadm certs renew all
# 3. 重启控制平面组件
# 对于 static pod 方式部署的组件
mv /etc/kubernetes/manifests/*.yaml /tmp/
sleep 30
mv /tmp/*.yaml /etc/kubernetes/manifests/
# 4. 更新 kubeconfig
kubeadm kubeconfig user --client-name=admin --org=system:masters > /etc/kubernetes/admin.conf
cp /etc/kubernetes/admin.conf ~/.kube/config
# 5. 验证
kubectl get nodes

预防措施

# 1. 设置证书过期监控
# 使用 kube-prometheus-stack 自带的证书监控
# 或者自定义脚本
#!/bin/bash
# check-cert-expiry.sh
CERT_PATH="/etc/kubernetes/pki/apiserver.crt"
EXPIRY_DATE=$(openssl x509 -in$CERT_PATH -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then    
echo"WARNING: Certificate expires in $DAYS_LEFT days"    
# 发送告警
fi
# 2. Prometheus 告警规则
-alert:KubernetesCertificateExpiration
expr:apiserver_client_certificate_expiration_seconds_count{job="kube-apiserver"}>0andhistogram_quantile(0.01,rate(apiserver_client_certificate_expiration_seconds_bucket{job="kube-apiserver"}[5m]))<604800
for:5m
labels:
    severity:critical
annotations:
    summary:"Kubernetes 证书即将过期"

教训:部署集群后第一件事就是设置证书过期告警!建议在证书过期前 30 天就开始告警。


三、节点级严重故障

3.1 案例四:节点批量 NotReady,业务雪崩

故障现象

某天下午,监控大屏突然一片红:

[CRITICAL] Node node-01 状态变为 NotReady
[CRITICAL] Node node-02 状态变为 NotReady
[CRITICAL] Node node-03 状态变为 NotReady
... (持续告警)

5 分钟内,集群 30% 的节点都变成了 NotReady,大量 Pod 被驱逐重建,业务出现严重抖动。

根因分析

# 查看节点状态
kubectl get nodes
kubectl describe node node-01
# 关键信息:
# Conditions:
#   Ready   False   KubeletNotReady   container runtime is down
# 登录问题节点
ssh node-01
systemctl status containerd
# 输出:containerd.service: Failed
# 查看 containerd 日志
journalctl -u containerd -n 100
# 发现大量 "too many open files" 错误

根本原因:containerd 进程打开的文件描述符超过了系统限制,导致 containerd 崩溃。而 containerd 崩溃后,kubelet 无法与容器运行时通信,节点就变成 NotReady 了。

为什么会打开这么多文件?排查发现是某个应用疯狂写日志,每秒产生上千个日志文件…

解决过程

# 1. 临时提高文件描述符限制
ulimit
 -n 1048576
# 2. 永久修改系统限制
cat >> /etc/security/limits.conf << EOF
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
EOF
# 3. 修改 containerd 服务配置
mkdir -p /etc/systemd/system/containerd.service.d/
cat > /etc/systemd/system/containerd.service.d/limits.conf << EOF
[Service]
LimitNOFILE=1048576
LimitNPROC=1048576
EOF
# 4. 重启服务
systemctl daemon-reload
systemctl restart containerd
# 5. 清理问题应用的日志
find /var/
log
/pods -name 
"*.log"
 -size +100M -delete

预防措施

# 1. 监控文件描述符使用
# node_exporter 自带此指标
node_filefd_allocated / node_filefd_maximum > 0.8
# 2. 配置日志轮转
# 在 kubelet 配置中
--container-log-max-size=100Mi
--container-log-max-files=5

教训:系统级资源限制(ulimit)很容易被忽视,但一旦触发就是灾难性的。建议在节点初始化时就配置好。


3.2 案例五:PDB 配置不当,节点无法维护

故障现象

计划对集群节点进行内核升级,需要逐个驱逐节点上的 Pod。执行 kubectl drain 时卡住了:

kubectl drain node-05 --ignore-daemonsets --delete-emptydir-data
# 输出:
# evicting pod default/web-app-xxx
# error when evicting pods/"web-app-xxx" -n "default" (will retry after 5s):
# Cannot evict pod as it would violate the pod's disruption budget.

等了半小时还是这样,节点维护工作完全无法进行。

根因分析

# 查看 PDB 配置
kubectl get pdb -A
kubectl describe pdb web-app-pdb
# 输出:
# Min Available: 3
# Current: 3
# Desired: 3
# Allowed Disruptions: 0  # 问题在这里!

根本原因:PDB(Pod Disruption Budget)配置了 minAvailable: 3,而 Deployment 的副本数也是 3。这意味着任何时候都不允许少于 3 个 Pod,所以一个都驱逐不了。

解决过程

# 方案一:临时增加副本数
kubectl scale deployment web-app --replicas=4
# 等待新 Pod Ready 后再 drain
# 方案二:临时调整 PDB
kubectl patch pdb web-app-pdb -p 
'{"spec":{"minAvailable":2}}'
# 方案三:删除 PDB(不推荐,有风险)
kubectl delete pdb web-app-pdb

教训:PDB 的 minAvailable 不要设置成和副本数相等!建议使用百分比或 maxUnavailable。


四、应用级常见问题

4.1 案例六:资源配额耗尽,新 Pod 无法创建

故障现象

开发同学反馈新部署的应用一直处于 Pending 状态:

kubectl get pods
# NAME                      READY   STATUS    RESTARTS   AGE
# new-app-xxx               0/1     Pending   0          30m
kubectl describe pod new-app-xxx
# Events:
#   Warning  FailedScheduling  pod didn't trigger scale-up:
#   1 Insufficient cpu, 1 Insufficient memory

根因分析

# 检查命名空间配额
kubectl describe resourcequota -n dev
# 输出:
# Name:            dev-quota
# Resource         Used    Hard
# --------         ----    ----
# limits.cpu       8       8      # 已用完!
# limits.memory    16Gi    16Gi   # 已用完!
# requests.cpu     4       4
# requests.memory  8Gi     8Gi

根本原因:命名空间的资源配额已经用完了,但没有人注意到。

解决过程

# 1. 查看当前资源使用情况
kubectl top pods -n dev
# 2. 找出资源占用大户
kubectl get pods -n dev -o custom-columns=\
NAME:.metadata.name,\
CPU_REQ:.spec.containers[*].resources.requests.cpu,\
MEM_REQ:.spec.containers[*].resources.requests.memory
# 3. 清理不需要的资源或扩大配额
kubectl patch resourcequota dev-quota -n dev \
  -p 
'{"spec":{"hard":{"limits.cpu":"16","limits.memory":"32Gi"}}}'

教训:配置 ResourceQuota 后一定要配套监控告警,在使用率达到 80% 时就应该告警。


4.2 案例七:滚动更新卡住,新旧版本并存

故障现象

发布新版本后,Deployment 一直卡在更新中:

kubectl rollout status deployment/api-server
# Waiting for deployment "api-server" rollout to finish: 2 out of 3 new replicas have been updated...

根因分析

kubectl get pods -l app=api-server
# NAME                          READY   STATUS             RESTARTS   AGE
# api-server-old-xxx            1/1     Running            0          2d
# api-server-new-xxx            0/1     CrashLoopBackOff   5          10m
# api-server-new-yyy            0/1     CrashLoopBackOff   5          10m
kubectl describe pod api-server-new-xxx
# Events:
#   Warning  Unhealthy  Readiness probe failed: HTTP probe failed with statuscode: 500

根本原因:新版本代码有 bug,健康检查一直失败。由于默认的滚动更新策略,旧 Pod 不会被删除,导致更新卡住。

解决过程

# 1. 回滚到上一版本
kubectl rollout undo deployment/api-server
# 2. 查看回滚状态
kubectl rollout status deployment/api-server
# 3. 修复代码后重新发布

预防措施

# 配置合理的更新策略和超时
apiVersion:apps/v1
kind:Deployment
spec:
progressDeadlineSeconds:600# 10分钟超时
strategy:
    type:RollingUpdate
    rollingUpdate:
      maxSurge:1
      maxUnavailable:0

教训:发布前一定要在测试环境验证健康检查配置!


4.3 案例八:ConfigMap 热更新踩坑

故障现象

更新了 ConfigMap 中的配置,但应用没有生效:

kubectl edit configmap app-config
# 修改了数据库连接字符串
# 但应用还是连接旧的数据库
kubectl logs app-xxx | grep 
"database"
# 输出还是旧的连接地址

根因分析

根本原因:ConfigMap 更新后,已经运行的 Pod 不会自动重新加载配置。这是 K8s 的设计行为,不是 bug。

有两种挂载方式,行为不同:

  • 环境变量方式:永远不会自动更新
  • Volume 挂载方式:会自动更新文件,但应用需要自己监听文件变化

解决方案

# 方案一:重启 Pod(最简单)
kubectl rollout restart deployment/app
# 方案二:使用 Reloader 自动重启
# 安装 stakater/Reloader
kubectl apply -f https://raw.githubusercontent.com/stakater/Reloader/master/deployments/kubernetes/reloader.yaml
# 在 Deployment 上添加注解
kubectl annotate deployment app reloader.stakater.com/auto=
"true"

教训:ConfigMap 更新不会自动触发 Pod 重启,需要额外机制处理。


4.4 案例九:HPA 抖动风暴

故障现象

配置了 HPA 后,Pod 数量疯狂波动:

10:00 - 副本数: 310:01 - 副本数: 1010:02 - 副本数: 310:03 - 副本数: 8...

根因分析

kubectl describe hpa web-app# 发现 CPU 使用率在阈值附近波动# 每次扩容后负载下降,然后缩容,负载又上升...

根本原因:HPA 配置的阈值太敏感,加上默认的扩缩容行为,导致抖动。

解决方案

apiVersion: autoscaling/v2
kind:HorizontalPodAutoscaler
spec:
behavior:
    scaleDown:
      stabilizationWindowSeconds:300# 缩容稳定窗口
      policies:
      -type:Percent
        value:10
        periodSeconds:60
    scaleUp:
      stabilizationWindowSeconds:60
      policies:
      -type:Percent
        value:100
        periodSeconds:15

教训:HPA 一定要配置 behavior 字段,控制扩缩容速度。


4.5 案例十:镜像拉取失败雪崩

故障现象

某天早上,大量 Pod 启动失败:

kubectl get pods
# NAME                    READY   STATUS             RESTARTS   AGE
# app-a-xxx               0/1     ImagePullBackOff   0          5m
# app-b-xxx               0/1     ImagePullBackOff   0          5m
# app-c-xxx               0/1     ErrImagePull       0          3m

根因分析

kubectl describe pod app
-a
-xxx
# Events:
#   Warning  Failed   Failed to pull image "registry.example.com/app:v1":
#   rpc error: code = Unknown desc = failed to pull and unpack image:
#   unexpected status code 503 Service Unavailable

根本原因:内部镜像仓库挂了,所有需要拉取镜像的 Pod 都失败了。

解决方案

# 1. 配置镜像拉取策略,减少不必要的拉取
spec:
containers:
-name:app
    image:registry.example.com/app:v1
    imagePullPolicy:IfNotPresent# 本地有就不拉取
# 2. 配置多镜像仓库备份
# 使用 Harbor 的镜像复制功能

教训:镜像仓库是单点故障,必须做高可用或备份。


五、总结

案例核心教训
etcd 磁盘满配置自动压缩,监控磁盘使用
API Server OOM限制大查询,配置 APF
证书过期设置过期告警,定期续期
节点 NotReady配置系统资源限制
PDB 配置错误使用百分比或 maxUnavailable
资源配额耗尽监控配额使用率
滚动更新卡住测试环境验证健康检查
ConfigMap 不生效使用 Reloader 或手动重启
HPA 抖动配置 behavior 稳定窗口
镜像拉取失败镜像仓库高可用

更多推荐