一、概述
1.1 背景介绍
K8s集群在测试环境跑得好好的,一上生产就各种花式故障——etcd磁盘写满集群瘫痪、OOMKill连锁反应打崩整个命名空间、滚动更新配错一个参数导致服务全量中断。这些问题不是理论推演,是真金白银的生产事故,每一个都对应着一次P0/P1级别的故障复盘。
K8s的复杂度在于它是一个分布式系统的分布式系统。etcd是分布式KV存储、kube-apiserver是无状态API网关、kubelet是节点Agent、CNI/CSI/CRI各种插件各管一摊。任何一个环节出问题,都可能引发级联故障。更麻烦的是,很多问题在小规模集群上根本不会暴露,节点数一过50、Pod数一过2000,各种边界条件就开始冒头。
这篇文章整理了10个在生产环境运维中高频出现的故障场景,每个问题都按照 现象 -> 原因分析 -> 排查路径 -> 解决方案 的结构展开,附带可直接复用的配置和命令。
1.2 技术特点
- 故障驱动:每个问题都来自真实生产场景,不是实验室里造出来的
- 全链路覆盖:从etcd存储层到应用层,从网络到安全,覆盖K8s核心组件
- 可复现可验证:所有排查命令和修复方案都经过K8s 1.32环境验证
- 防御性配置:不只是修复问题,更重要的是如何在问题发生前做好防护
1.3 适用场景
- 场景一:生产集群运维,需要快速定位和修复常见故障
- 场景二:集群初始化阶段,需要提前规避已知的配置陷阱
- 场景三:SRE团队建设,需要建立标准化的故障排查Runbook
- 场景四:K8s升级前的风险评估,需要了解各版本的行为差异
1.4 环境要求
| 组件 |
版本要求 |
说明 |
| Kubernetes |
1.32+ |
2026年主流生产版本 |
| etcd |
3.5.17+ |
集群状态存储 |
| containerd |
2.0+ |
容器运行时 |
| 操作系统 |
Ubuntu 24.04 LTS |
内核6.8+,cgroup v2默认启用 |
| Cilium |
1.17+ |
推荐CNI插件 |
| cert-manager |
1.17+ |
证书自动管理 |
二、详细步骤
2.1 问题一:etcd磁盘性能不足导致集群不可用
2.1.1 现象
kube-apiserver响应变慢,kubectl 命令超时,最终所有API请求返回 context deadline exceeded。集群内的Pod调度停滞,已运行的Pod不受影响但无法进行任何变更操作。etcd日志中大量出现 took too long 和 slow fdatasync 警告。
2.1.2 原因分析
etcd是K8s的“大脑”,所有集群状态都存在etcd里。etcd使用WAL(Write-Ahead Log)保证数据一致性,每次写操作都要先写WAL再fsync到磁盘。如果磁盘I/O延迟超过etcd的心跳间隔(默认100ms),就会触发leader选举,频繁选举直接导致集群不可用。
常见的磁盘性能不足原因:
- etcd和其他I/O密集型服务共享磁盘(比如跑在同一块SATA HDD上)
- 云厂商的普通云盘IOPS不够(通常需要 > 3000 IOPS)
- 虚拟化环境下的I/O调度器配置不当
- etcd数据目录和WAL目录没有分离
2.1.3 排查路径
# 检查 etcd 的 fsync 延迟(核心指标)
etcdctl endpoint status --write-out=table
# 查看 etcd 慢请求日志
journalctl -u etcd | grep "slow fdatasync" | tail -20
# 检查磁盘 I/O 延迟
# WAL fsync 延迟应该 < 10ms,超过 50ms 就很危险
iostat -x 1 5
# 用 fio 测试磁盘实际性能
fio --rw=write --ioengine=sync --fdatasync=1 --directory=/var/lib/etcd \
--size=22m --bs=2300 --name=etcd-benchmark
# 检查 etcd 的 Prometheus 指标
# etcd_disk_wal_fsync_duration_seconds 的 p99 应该 < 10ms
# etcd_disk_backend_commit_duration_seconds 的 p99 应该 < 25ms
curl -s http://localhost:2379/metrics | grep etcd_disk_wal_fsync_duration
2.1.4 解决方案
# 方案一:使用高性能 SSD(推荐 NVMe)
# 云环境下选择 IOPS >= 3000 的 SSD 云盘
# AWS: gp3 (配置 3000+ IOPS) 或 io2
# 阿里云: ESSD PL1 或更高
# 方案二:分离 WAL 目录到独立磁盘
# 修改 etcd 启动参数
--wal-dir=/mnt/etcd-wal # 独立的高速 SSD
# 方案三:调整 I/O 调度器(适用于 SSD)
echo none > /sys/block/nvme0n1/queue/scheduler
# 方案四:给 etcd 进程设置 I/O 优先级
ionice -c2 -n0 -p $(pgrep etcd)
# 方案五:定期压缩和碎片整理
# etcd 数据库大小默认限制 2GB,超过会拒绝写入
etcdctl compact $(etcdctl endpoint status --write-out=json | jq '.[0].Status.header.revision')
etcdctl defrag --endpoints=https://127.0.0.1:2379
etcdctl alarm disarm
关键指标阈值:
| 指标 |
健康值 |
警告值 |
危险值 |
| WAL fsync p99 |
< 10ms |
10-50ms |
> 50ms |
| Backend commit p99 |
< 25ms |
25-100ms |
> 100ms |
| DB 大小 |
< 4GB |
4-6GB |
> 6GB |
| Leader 选举次数/h |
0 |
1-2 |
> 3 |
2.2 问题二:资源 Limit 设置不当引发 OOMKill 连锁反应
2.2.1 现象
某个Pod被OOMKill,Deployment控制器立即拉起新Pod,新Pod启动时内存飙升又被Kill,反复重启。更糟糕的是,同节点上的其他Pod因为内存压力也开始被驱逐,形成雪崩效应。kubectl get pods 看到一片 CrashLoopBackOff 和 Evicted。
2.2.2 原因分析
K8s的资源管理分Request和Limit两层:
- Request:调度器用来决定Pod放在哪个节点,是“最低保障”
- Limit:kubelet用cgroup强制限制的上限,超过就OOMKill
常见的错误配置模式:
- Limit设得太低:Java应用的JVM堆 + 非堆 + 线程栈 + NIO DirectBuffer加起来超过Limit
- Request远小于Limit(过度超卖):节点实际内存不够,触发节点级OOM
- 没设Limit:BestEffort QoS,节点内存紧张时第一个被干掉
- 忽略了init container的资源消耗:init container和主容器的Request取最大值
2.2.3 排查路径
# 查看 Pod 被 OOMKill 的详细信息
kubectl describe pod <pod-name> -n <namespace> | grep -A 5 "Last State"
# 查看节点内存压力
kubectl describe node <node-name> | grep -A 10 "Conditions"
# 查看节点上所有 Pod 的资源使用情况
kubectl top pods -n <namespace> --sort-by=memory
# 查看被驱逐的 Pod
kubectl get pods -n <namespace> --field-selector=status.phase=Failed \
-o jsonpath='{range .items}{.metadata.name}{"\t"}{.status.reason}{"\n"}{end}'
# 查看 cgroup 内存限制和实际使用(节点上执行)
# cgroup v2 路径
cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice/memory.max
cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice/memory.current
# 查看 OOM 事件
dmesg | grep -i "oom\|killed process" | tail -20
2.2.4 解决方案
# 正确的资源配置模板
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-example
spec:
template:
spec:
containers:
- name: app
resources:
requests:
memory: "512Mi" # 基于实际 p50 使用量
cpu: "250m"
limits:
memory: "1Gi" # Request 的 1.5-2 倍,留足余量
cpu: "1000m" # CPU 建议不设 Limit 或设较大值
# Java 应用特别注意:JVM 参数要和 cgroup 限制对齐
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport"
# 配置 LimitRange 防止"裸奔" Pod
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- default: # 默认 Limit
memory: "512Mi"
cpu: "500m"
defaultRequest: # 默认 Request
memory: "256Mi"
cpu: "100m"
max: # 单个容器最大值
memory: "4Gi"
cpu: "4000m"
min: # 单个容器最小值
memory: "64Mi"
cpu: "50m"
type: Container
# 配置 ResourceQuota 防止命名空间资源耗尽
apiVersion: v1
kind: ResourceQuota
metadata:
name: namespace-quota
namespace: production
spec:
hard:
requests.memory: "32Gi"
limits.memory: "64Gi"
requests.cpu: "16"
limits.cpu: "32"
pods: "100"
CPU Limit的争议:2026年的主流实践是 不设CPU Limit或设一个很大的值。原因是CPU是可压缩资源,超过Limit不会被Kill,只会被throttle。而CFS throttle会导致延迟毛刺,对延迟敏感的服务影响很大。只设CPU Request保证调度公平性就够了。
2.3 问题三:滚动更新配置错误导致服务中断
2.3.1 现象
执行 kubectl apply 更新Deployment后,服务出现短暂或持续的不可用。监控上看到请求成功率骤降,部分请求返回502/503。旧Pod已经被终止,新Pod还没Ready,中间出现了可用实例数为0的窗口期。
2.3.2 原因分析
滚动更新的核心参数是 maxSurge 和 maxUnavailable,配合 readinessProbe 和 terminationGracePeriodSeconds 一起工作。任何一个配错都可能导致服务中断:
- maxUnavailable设太大:比如replicas=3且maxUnavailable=2,更新时只剩1个Pod扛流量
- readinessProbe缺失或配置不当:新Pod还没准备好就被加入Service Endpoints
- terminationGracePeriodSeconds太短:旧Pod还在处理请求就被强制Kill
- preStop hook缺失:Pod收到SIGTERM后立即停止接收请求,但Endpoints还没更新,流量还在往这个Pod发
- 启动时间过长:应用启动需要60秒,但initialDelaySeconds只设了5秒
2.3.3 排查路径
# 查看 Deployment 的滚动更新策略
kubectl get deployment <name> -o jsonpath='{.spec.strategy}' | jq .
# 查看更新过程中的事件
kubectl rollout status deployment/<name> -n <namespace>
kubectl describe deployment <name> -n <namespace> | grep -A 20 "Events"
# 查看 ReplicaSet 的变化过程
kubectl get rs -n <namespace> -l app=<name> --sort-by=.metadata.creationTimestamp
# 检查 Endpoints 变化(关键:看是否有空窗期)
kubectl get endpoints <service-name> -n <namespace> -w
# 查看 Pod 的 readiness 状态变化
kubectl get pods -n <namespace> -l app=<name> -w
2.3.4 解决方案
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多多出 1 个 Pod
maxUnavailable: 0 # 不允许任何 Pod 不可用(零停机的关键)
template:
spec:
terminationGracePeriodSeconds: 60 # 给足优雅关闭时间
containers:
- name: web
# readinessProbe:决定 Pod 何时接收流量
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # 根据应用实际启动时间调整
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
# livenessProbe:决定 Pod 何时被重启
# 注意:livenessProbe 不要和 readinessProbe 用同一个端点
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 30 # 必须大于应用最长启动时间
periodSeconds: 10
failureThreshold: 3
# startupProbe:K8s 1.20+ 推荐用于慢启动应用
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30 # 30 * 2s = 60s 启动窗口
periodSeconds: 2
lifecycle:
preStop:
exec:
# 关键:等待 Endpoints 控制器移除本 Pod
# 没有这个 sleep,会出现流量发到已经开始关闭的 Pod
command: ["sh", "-c", "sleep 10"]
零停机更新的核心公式:maxUnavailable=0 + 正确的 readinessProbe + preStop sleep > Endpoints传播延迟(通常5-10秒)。
2.4 问题四:PDB缺失导致节点维护时服务雪崩
2.4.1 现象
执行 kubectl drain 进行节点维护时,节点上的所有Pod被同时驱逐。如果某个服务的多个副本恰好都调度在这个节点上,服务直接不可用。更极端的情况:集群自动伸缩器(Cluster Autoscaler)缩容时自动drain节点,半夜三点服务挂了,没人知道为什么。
2.4.2 原因分析
kubectl drain 默认行为是驱逐节点上所有非DaemonSet的Pod。如果没有PodDisruptionBudget(PDB),drain操作不会检查服务的可用副本数,直接一锅端。
PDB的作用是告诉K8s:“这个服务至少要保持N个副本可用”或“最多允许N个副本同时不可用”。drain操作会尊重PDB的约束,逐个驱逐Pod而不是一次性全部干掉。
没有PDB的常见后果:
- 节点维护时服务中断
- Cluster Autoscaler缩容导致服务不可用
- 节点故障自动修复(如AWS ASG替换实例)时的级联影响
2.4.3 排查路径
# 检查集群中哪些 Deployment 没有对应的 PDB
kubectl get deployments -A -o json | jq -r '.items[] | select(.spec.replicas > 1) | "\(.metadata.namespace)/\(.metadata.name)"' > /tmp/deployments.txt
kubectl get pdb -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.spec.selector.matchLabels)"' > /tmp/pdbs.txt
# 对比两个列表,找出缺失 PDB 的 Deployment
# 查看现有 PDB 的状态
kubectl get pdb -A
kubectl describe pdb <pdb-name> -n <namespace>
# 模拟 drain 操作(不实际执行)
kubectl drain <node-name> --dry-run=client --ignore-daemonsets --delete-emptydir-data
# 检查 Pod 的反亲和性配置(避免同一服务的 Pod 扎堆在一个节点)
kubectl get pod -n <namespace> -l app=<name> -o wide
2.4.4 解决方案
# PDB 配置:保证至少 N 个副本可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-app-pdb
namespace: production
spec:
minAvailable: 2 # 至少保持 2 个副本可用
# 或者用 maxUnavailable: 1 # 最多允许 1 个副本不可用
selector:
matchLabels:
app: web-app
# 配合 Pod 反亲和性,避免副本扎堆
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
template:
spec:
affinity:
podAntiAffinity:
# 强制反亲和:同一服务的 Pod 不能在同一节点
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["web-app"]
topologyKey: kubernetes.io/hostname
# 如果节点数不够,用软反亲和降级
# preferredDuringSchedulingIgnoredDuringExecution:
# - weight: 100
# podAffinityTerm:
# labelSelector:
# matchExpressions:
# - key: app
# operator: In
# values: ["web-app"]
# topologyKey: kubernetes.io/hostname
PDB配置原则:minAvailable 或 maxUnavailable 的值要根据服务的实际副本数来定。3副本的服务设 minAvailable: 2 或 maxUnavailable: 1。单副本服务设PDB没有意义(drain会卡住永远完不成)。
2.5 问题五:RBAC权限过大引发安全事故
2.5.1 现象
开发人员使用CI/CD的ServiceAccount意外删除了生产命名空间的核心ConfigMap,导致依赖该ConfigMap的所有Pod重启后无法启动。排查发现该ServiceAccount绑定了 cluster-admin 角色——一个拥有集群所有资源所有操作权限的超级角色。
2.5.2 原因分析
K8s的RBAC模型由四个对象组成:Role、ClusterRole、RoleBinding、ClusterRoleBinding。权限过大的常见原因:
- 图省事绑cluster-admin:开发初期为了方便,给所有ServiceAccount都绑了cluster-admin
- ClusterRoleBinding滥用:本来只需要某个命名空间的权限,却用了ClusterRoleBinding授予全集群权限
- 通配符权限:
resources: ["*"] 和 verbs: ["*"] 一把梭
- ServiceAccount Token泄露:默认挂载的Token被应用日志打印或被攻击者获取
- 没有定期审计:权限只增不减,离职员工的权限没有回收
2.5.3 排查路径
# 查看集群中所有 ClusterRoleBinding 到 cluster-admin 的绑定
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name + " -> " + (.subjects[]? | .kind + "/" + .name)'
# 查看某个 ServiceAccount 的实际权限
kubectl auth can-i --list --as=system:serviceaccount:<namespace>:<sa-name>
# 查看哪些 Pod 挂载了 ServiceAccount Token
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.automountServiceAccountToken != false) | "\(.metadata.namespace)/\(.metadata.name) -> \(.spec.serviceAccountName)"'
# 审计日志中查找危险操作
# 需要开启 API Server 审计日志
grep '"verb":"delete"' /var/log/kubernetes/audit.log | jq -r '.user.username + " deleted " + .objectRef.resource + "/" + .objectRef.name'
2.5.4 解决方案
# 最小权限 Role 示例:CI/CD 部署专用
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cicd-deployer
namespace: production
rules:
# 只允许操作 Deployment 和 Service
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch"]
# 只允许读取 ConfigMap 和 Secret,不允许删除
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
# 允许查看 Pod 状态(排查部署问题)
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cicd-deployer-binding
namespace: production
subjects:
- kind: ServiceAccount
name: cicd-sa
namespace: production
roleRef:
kind: Role
name: cicd-deployer
apiGroup: rbac.authorization.k8s.io
# 禁止自动挂载 ServiceAccount Token(K8s 1.24+ 默认不再自动创建 Secret)
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
automountServiceAccountToken: false # 除非应用确实需要调用 K8s API
RBAC黄金法则:永远从零权限开始,按需添加。用 Role + RoleBinding(命名空间级)而不是 ClusterRole + ClusterRoleBinding(集群级),除非确实需要跨命名空间操作。
2.6 问题六:ConfigMap/Secret热更新踩坑
2.6.1 现象
修改了ConfigMap的内容,kubectl get configmap 确认已经更新,但应用的行为完全没变。等了半天发现Pod里的配置文件还是旧的。更隐蔽的情况:用 subPath 挂载的ConfigMap永远不会自动更新,排查了几个小时才发现这个“特性”。
2.6.2 原因分析
ConfigMap/Secret挂载到Pod的更新机制有几个关键细节:
- Volume挂载方式:以Volume形式挂载的ConfigMap,kubelet会定期同步更新(默认同步周期60秒 + 一定的缓存TTL),但不是实时的
- subPath挂载不更新:使用
subPath 挂载的文件 永远不会自动更新,这是K8s的设计行为,不是Bug
- 环境变量方式不更新:通过
envFrom 或 env.valueFrom 引用的ConfigMap/Secret,Pod运行期间不会更新,必须重启Pod
- 应用不感知文件变化:即使文件更新了,应用如果没有watch文件变化的逻辑,也不会重新加载配置
- Immutable ConfigMap/Secret:K8s 1.21+支持设置
immutable: true,设置后无法修改,需要创建新的ConfigMap并更新Pod引用
2.6.3 排查路径
# 确认 ConfigMap 是否已更新
kubectl get configmap <name> -n <namespace> -o yaml
# 检查 Pod 内的实际文件内容
kubectl exec <pod-name> -n <namespace> -- cat /path/to/config/file
# 检查挂载方式是否使用了 subPath
kubectl get pod <pod-name> -n <namespace> -o json | \
jq '.spec.containers[].volumeMounts[] | select(.subPath != null)'
# 检查 ConfigMap 是否设置了 immutable
kubectl get configmap <name> -n <namespace> -o jsonpath='{.immutable}'
# 查看 kubelet 的同步周期配置
# 默认 --sync-frequency=1m
ps aux | grep kubelet | grep sync-frequency
2.6.4 解决方案
# 方案一:避免 subPath,使用目录挂载(推荐)
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
volumeMounts:
# 正确:挂载整个目录,支持自动更新
- name: config-volume
mountPath: /etc/app/config
# 错误:subPath 挂载不会自动更新
# - name: config-volume
# mountPath: /etc/app/config/app.yaml
# subPath: app.yaml
volumes:
- name: config-volume
configMap:
name: app-config
# 方案二:使用 configmap hash 注解触发滚动更新
# 在 CI/CD 流水线中计算 ConfigMap 的 hash 并写入 Pod 注解
apiVersion: apps/v1
kind: Deployment
spec:
template:
metadata:
annotations:
# ConfigMap 内容变化时 hash 值变化,触发 Pod 滚动更新
checksum/config: "sha256-of-configmap-content"
# 方案三:手动触发滚动重启(最简单粗暴)
kubectl rollout restart deployment/<name> -n <namespace>
# 方案四:使用 Reloader 自动监听 ConfigMap 变化并重启 Pod
# 安装 stakater/Reloader
# helm install reloader stakater/reloader -n kube-system
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
reloader.stakater.com/auto: "true" # 自动监听关联的 ConfigMap/Secret
最佳实践:对于需要热更新的配置,应用层面实现文件watch机制(如Go的fsnotify、Java的Spring Cloud Config)。对于不支持热更新的应用,使用Reloader或CI/CD流水线中的hash注解方案。
2.7 问题七:节点NotReady排查(kubelet/容器运行时问题)
2.7.1 现象
kubectl get nodes 显示某个节点状态为 NotReady,该节点上的Pod在 node.kubernetes.io/not-ready 容忍时间(默认300秒)后被驱逐到其他节点。如果多个节点同时NotReady,可能触发大规模Pod迁移,进而压垮剩余健康节点。
2.7.2 原因分析
节点NotReady的本质是kubelet停止向API Server上报心跳。kube-controller-manager中的NodeLifecycleController在超过 node-monitor-grace-period(默认40秒)没收到心跳后,将节点标记为NotReady。
常见原因按频率排序:
- kubelet进程挂了:OOM、证书过期、配置错误
- containerd/CRI运行时故障:containerd进程卡死、shim进程泄漏
- 系统资源耗尽:磁盘满(特别是/var/lib/kubelet和/var/lib/containerd)、inode耗尽、内存不足
- 内核问题:内核Bug导致的soft lockup、cgroup泄漏
- 网络问题:节点到API Server的网络中断
- PLEG(Pod Lifecycle Event Generator)超时:containerd响应慢导致PLEG健康检查失败
2.7.3 排查路径
# 第一步:查看节点状态和 Conditions
kubectl describe node <node-name> | grep -A 20 "Conditions"
# 第二步:SSH 到节点,检查 kubelet 状态
systemctl status kubelet
journalctl -u kubelet --since "30 minutes ago" | tail -100
# 第三步:检查容器运行时
systemctl status containerd
crictl info
crictl ps -a | head -20
# 第四步:检查系统资源
df -h # 磁盘空间
df -i # inode 使用
free -h # 内存
dmesg | tail -50 # 内核日志
# 第五步:检查 PLEG 状态
journalctl -u kubelet | grep "PLEG" | tail -20
# 第六步:检查证书是否过期
openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -dates
# 第七步:检查到 API Server 的连通性
curl -k https://<apiserver-ip>:6443/healthz
2.7.4 解决方案
# 场景一:kubelet OOM 被 Kill
# 检查 kubelet 内存使用
journalctl -u kubelet | grep "oom\|killed"
# 增加 kubelet 的系统预留
# 修改 /var/lib/kubelet/config.yaml
# systemReserved:
# memory: "1Gi"
# cpu: "500m"
# kubeReserved:
# memory: "1Gi"
# cpu: "500m"
systemctl restart kubelet
# 场景二:containerd 卡死
# 检查 containerd shim 进程数量
ps aux | grep containerd-shim | wc -l
# 如果 shim 进程数量异常多,可能存在泄漏
# 重启 containerd(会导致节点上所有容器重启)
systemctl restart containerd
# 场景三:磁盘空间不足
# 清理已退出的容器和未使用的镜像
crictl rmi --prune
# 清理 kubelet 的日志
journalctl --vacuum-size=500M
# 检查大文件
du -sh /var/lib/containerd/*
du -sh /var/log/*
# 场景四:证书过期
# K8s 1.32 的 kubelet 支持自动轮换证书
# 确认 kubelet 配置中启用了证书轮换
# rotateCertificates: true
# 手动触发证书更新
kubeadm certs renew all
systemctl restart kubelet
预防措施:配置节点级别的监控告警,在磁盘使用率 > 80%、内存使用率 > 90%、kubelet进程消失时立即告警。设置合理的 evictionHard 阈值,让kubelet在资源紧张时主动驱逐低优先级Pod而不是自己挂掉。
2.8 问题八:DNS解析超时(ndots配置问题)
2.8.1 现象
应用访问外部域名(如 api.example.com)时偶发超时,延迟从正常的几毫秒飙升到5-15秒。抓包发现DNS查询被发送了多次,先查询 api.example.com.default.svc.cluster.local、api.example.com.svc.cluster.local、api.example.com.cluster.local,全部失败后才查询 api.example.com。
2.8.2 原因分析
K8s默认给Pod的 /etc/resolv.conf 配置了 ndots:5,含义是:如果域名中的点号数量少于5个,就先在search域中搜索。
# Pod 内默认的 /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
api.example.com 只有2个点,少于5个,所以DNS解析器会依次尝试:
api.example.com.default.svc.cluster.local -- 失败
api.example.com.svc.cluster.local -- 失败
api.example.com.cluster.local -- 失败
api.example.com -- 成功
每次失败的查询都要等待超时,4次查询变成了8次(A记录和AAAA记录各一次),延迟直接翻了好几倍。在CoreDNS负载高的时候,这个问题会被放大。
2.8.3 排查路径
# 查看 Pod 的 DNS 配置
kubectl exec <pod-name> -n <namespace> -- cat /etc/resolv.conf
# 在 Pod 内测试 DNS 解析时间
kubectl exec <pod-name> -n <namespace> -- time nslookup api.example.com
# 抓包分析 DNS 查询过程
kubectl exec <pod-name> -n <namespace> -- tcpdump -i eth0 port 53 -nn -c 20
# 查看 CoreDNS 的负载和延迟
kubectl top pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
# 检查 CoreDNS 的 Prometheus 指标
# coredns_dns_request_duration_seconds 的 p99
# coredns_dns_responses_total 中 NXDOMAIN 的比例
2.8.4 解决方案
# 方案一:在 Pod 级别调整 ndots(推荐)
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
dnsConfig:
options:
- name: ndots
value: "2" # 降低到 2,大部分外部域名不再走 search 域
- name: single-request-reopen
value: "" # 避免 A 和 AAAA 查询使用同一个 socket 导致的竞争
containers:
- name: app
# ...
# 方案二:外部域名使用 FQDN(末尾加点)
# 在应用配置中,外部域名统一使用 FQDN 格式
# api.example.com. (注意末尾的点)
# 这样 DNS 解析器会直接查询,不走 search 域
# 方案三:优化 CoreDNS 配置
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
# 启用 DNS 缓存,减少上游查询
cache 30 {
success 9984 30
denial 9984 5
}
# NodeLocal DNSCache 配合使用效果更好
forward . /etc/resolv.conf {
max_concurrent 1000
}
loop
reload
loadbalance
}
# 方案四:部署 NodeLocal DNSCache(强烈推荐)
# 在每个节点上运行 DNS 缓存,避免所有 DNS 查询都打到 CoreDNS
# K8s 1.32 中 NodeLocal DNSCache 已经非常成熟
kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
ndots调优原则:如果应用主要访问外部服务,设 ndots: 2。如果应用主要访问集群内服务(通过短名称如 my-service),保持默认 ndots: 5。混合场景下设 ndots: 2 并在访问集群内服务时使用完整的 my-service.namespace.svc.cluster.local。
2.9 问题九:Ingress证书过期导致全站不可用
2.9.1 现象
用户访问HTTPS站点时浏览器弹出证书过期警告,部分客户端直接拒绝连接。如果Ingress Controller配置了强制HTTPS重定向,HTTP也无法正常访问——所有流量都被重定向到一个证书过期的HTTPS端点。监控告警显示5xx错误率飙升到100%。
2.9.2 原因分析
TLS证书有明确的有效期(Let's Encrypt是90天,商业证书通常1年)。证书过期的常见原因:
- 没有自动续期机制:手动申请的证书,到期了没人记得续
- cert-manager续期失败:ACME challenge失败(DNS记录没配对、HTTP-01验证路径被WAF拦截)但没有告警
- 证书更新了但Ingress Controller没重载:Nginx Ingress Controller的SSL证书热更新有时会失败
- 通配符证书覆盖不全:
*.example.com 不覆盖 example.com(裸域名)和 *.sub.example.com(多级子域名)
- Secret被误删:存放证书的Secret被清理脚本或人为操作删除
2.9.3 排查路径
# 检查 Ingress 关联的 TLS Secret
kubectl get ingress -n <namespace> -o json | \
jq -r '.items[] | .metadata.name + ": " + (.spec.tls[]?.secretName // "NO TLS")'
# 查看证书的过期时间
kubectl get secret <tls-secret> -n <namespace> -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -dates -subject
# 批量检查所有命名空间的 TLS 证书过期时间
for ns in $(kubectl get ns -o jsonpath='{.items.metadata.name}'); do
for secret in $(kubectl get secrets -n $ns --field-selector type=kubernetes.io/tls -o jsonpath='{.items.metadata.name}'); do
expiry=$(kubectl get secret $secret -n $ns -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -enddate 2>/dev/null)
echo "$ns/$secret: $expiry"
done
done
# 检查 cert-manager 的 Certificate 资源状态
kubectl get certificates -A
kubectl describe certificate <name> -n <namespace>
# 查看 cert-manager 的续期日志
kubectl logs -n cert-manager -l app=cert-manager --tail=100 | grep -i "error\|renew\|fail"
2.9.4 解决方案
# 使用 cert-manager 自动管理证书(推荐方案)
# 1. 创建 ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
# HTTP-01 验证(最简单,适合公网可达的服务)
- http01:
ingress:
ingressClassName: nginx
# DNS-01 验证(适合内网服务和通配符证书)
# - dns01:
# cloudflare:
# email: ops@example.com
# apiTokenSecretRef:
# name: cloudflare-api-token
# key: api-token
# 2. 在 Ingress 中声明证书需求
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-app
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
- "*.app.example.com"
secretName: app-example-com-tls # cert-manager 自动创建和续期
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80
# 3. 配置证书过期告警(Prometheus 规则)
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cert-expiry-alerts
namespace: monitoring
spec:
groups:
- name: cert-manager
rules:
- alert: CertificateExpiringSoon
expr: certmanager_certificate_expiration_timestamp_seconds-time()<7*24*3600
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} in {{ $labels.namespace }} expires in less than 7 days"
- alert: CertificateExpiryCritical
expr: certmanager_certificate_expiration_timestamp_seconds-time()<24*3600
for: 10m
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} in {{ $labels.namespace }} expires in less than 24 hours"
证书管理铁律:生产环境必须使用cert-manager或等效的自动化工具管理证书。手动管理证书在规模化场景下必然翻车,不是会不会的问题,是什么时候的问题。
2.10 问题十:存储卷挂载失败(CSI驱动问题)
2.10.1 现象
Pod一直卡在 ContainerCreating 状态,kubectl describe pod 显示 FailedMount 或 FailedAttachVolume 事件。常见的错误信息包括:
AttachVolume.Attach failed: rpc error: code = Internal
MountVolume.MountDevice failed: rpc error: code = DeadlineExceeded
Unable to attach or mount volumes: unmounted volumes=[data], unattached volumes=[data]
Multi-Attach error for volume "pvc-xxx" Volume is already exclusively attached to one node
2.10.2 原因分析
K8s的存储架构分三层:Provision(创建卷)-> Attach(挂载到节点)-> Mount(挂载到Pod)。CSI(Container Storage Interface)驱动负责实现这三个阶段的具体操作。
常见的存储卷挂载失败原因:
- Multi-Attach冲突:RWO(ReadWriteOnce)类型的PV只能挂载到一个节点。Pod被调度到新节点时,旧节点上的VolumeAttachment还没释放,新节点挂载失败
- CSI驱动Pod异常:CSI Node Plugin(DaemonSet)或CSI Controller(Deployment)挂了
- 云厂商API限流:大量Pod同时创建时,云盘API被限流导致Attach超时
- 存储后端故障:NFS服务器不可达、Ceph集群降级、云盘服务异常
- PV/PVC状态不匹配:PVC处于Pending状态(StorageClass不存在或配额不足)
- 文件系统损坏:非正常卸载导致文件系统需要fsck修复
2.10.3 排查路径
# 第一步:查看 Pod 事件
kubectl describe pod <pod-name> -n <namespace> | grep -A 10 "Events"
# 第二步:检查 PVC 状态
kubectl get pvc -n <namespace>
kubectl describe pvc <pvc-name> -n <namespace>
# 第三步:检查 PV 状态
kubectl get pv | grep <pvc-name>
kubectl describe pv <pv-name>
# 第四步:检查 VolumeAttachment(关键)
kubectl get volumeattachment | grep <pv-name>
# 如果看到 attached=true 但 Pod 在另一个节点,说明是 Multi-Attach 问题
# 第五步:检查 CSI 驱动状态
kubectl get pods -n kube-system -l app=csi-driver
kubectl logs -n kube-system <csi-node-pod> -c <driver-container> --tail=50
# 第六步:检查节点上的挂载情况
# SSH 到节点执行
mount | grep <pv-name>
ls -la /var/lib/kubelet/plugins/
ls -la /var/lib/kubelet/pods/<pod-uid>/volumes/
# 第七步:检查 StorageClass
kubectl get storageclass
kubectl describe storageclass <sc-name>
2.10.4 解决方案
# 场景一:Multi-Attach 冲突
# 强制删除旧的 VolumeAttachment(谨慎操作,确认旧 Pod 已不存在)
kubectl delete volumeattachment <va-name>
# 如果旧 Pod 卡在 Terminating,强制删除
kubectl delete pod <old-pod> -n <namespace> --grace-period=0 --force
# 场景二:避免 Multi-Attach,使用 StatefulSet + volumeClaimTemplates
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: database
spec:
serviceName: database
replicas: 3
template:
spec:
containers:
- name: db
volumeMounts:
- name: data
mountPath: /var/lib/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
# 场景三:配置合理的 StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # 或对应云厂商的 CSI 驱动
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
reclaimPolicy: Retain # 生产环境用 Retain,防止误删数据
allowVolumeExpansion: true # 允许在线扩容
volumeBindingMode: WaitForFirstConsumer # 延迟绑定,避免跨 AZ 调度问题
mountOptions:
- noatime # 减少不必要的元数据写入
# 场景四:CSI 驱动故障恢复
# 重启 CSI Node Plugin
kubectl rollout restart daemonset/<csi-node-ds> -n kube-system
# 重启 CSI Controller
kubectl rollout restart deployment/<csi-controller> -n kube-system
# 检查 CSI 驱动注册状态
kubectl get csinodes
kubectl describe csinode <node-name>
存储卷排查口诀:先看PVC状态(Pending/Bound),再看VolumeAttachment(有没有残留),然后看CSI驱动日志(有没有报错),最后看节点挂载(mount命令确认)。
三、示例代码和配置
3.1 完整配置示例
3.1.1 生产级Deployment模板
把前面提到的所有防护措施整合到一个完整的云原生Deployment配置中:
# 文件路径:manifests/deployment-production-template.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
labels:
app: web-app
version: v1.0.0
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
version: v1.0.0
annotations:
checksum/config: "PLACEHOLDER_FOR_CI_CD"
spec:
serviceAccountName: web-app-sa
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: ["web-app"]
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web-app
dnsConfig:
options:
- name: ndots
value: "2"
- name: single-request-reopen
value: ""
containers:
- name: web
image: registry.example.com/web-app:v1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
# CPU Limit 不设或设较大值
startupProbe:
httpGet:
path: /healthz
port: http
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe:
httpGet:
path: /livez
port: http
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
volumeMounts:
- name: config
mountPath: /etc/app/config
readOnly: true
- name: tmp
mountPath: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: config
configMap:
name: web-app-config
- name: tmp
emptyDir: {}
3.1.2 配套的PDB和ServiceAccount
# PDB
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-app-pdb
namespace: production
spec:
maxUnavailable: 1
selector:
matchLabels:
app: web-app
---
# ServiceAccount(最小权限)
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-app-sa
namespace: production
automountServiceAccountToken: false
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 集群初始化阶段的防御性配置
集群搭建完成后、业务上线前,有一批配置必须提前做好。等出了事再补,代价是停机时间和事故报告。
# 1. 每个命名空间必须有 LimitRange 和 ResourceQuota
# 防止"裸奔" Pod 和资源无限制消耗
# 参考 2.2 节的配置模板
# 2. 每个多副本 Deployment 必须有 PDB
# 参考 2.4 节的配置模板
# 3. 默认 NetworkPolicy:deny-all + 按需放行
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
# 4. 开启 API Server 审计日志
# kube-apiserver 启动参数
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-log-maxbackup=10
--audit-log-maxsize=100
# 5. 配置 etcd 自动压缩
# etcd 启动参数
--auto-compaction-mode=periodic
--auto-compaction-retention=1h
4.1.2 资源管理黄金法则
| 原则 |
具体做法 |
常见错误 |
| Request基于实际用量 |
取p50-p75的实际使用量 |
拍脑袋设值,要么太大浪费要么太小被驱逐 |
| Memory Limit留余量 |
Request的1.5-2倍 |
Limit = Request,一个内存尖峰就OOMKill |
| CPU Limit慎重设置 |
不设或设很大的值 |
设太小导致CFS throttle,延迟飙升 |
| 定期right-sizing |
每月根据监控数据调整 |
设完就忘,半年后实际用量和配置差3倍 |
| 使用VPA辅助决策 |
部署VPA recommender模式 |
直接开VPA auto模式,频繁重启Pod |
# 使用 kubectl top 快速查看资源使用情况
kubectl top pods -n production --sort-by=memory
kubectl top nodes
# 使用 Prometheus 查询历史资源使用(更准确)
# container_memory_working_set_bytes 的 p95
# container_cpu_usage_seconds_total 的平均值
4.1.3 安全加固清单
# 1. 检查集群中的高危配置
# 查找使用 hostNetwork 的 Pod
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.hostNetwork==true) | "\(.metadata.namespace)/\(.metadata.name)"'
# 查找使用 privileged 的容器
kubectl get pods -A -o json | \
jq -r '.items[] | select(.spec.containers[].securityContext.privileged==true) | "\(.metadata.namespace)/\(.metadata.name)"'
# 查找没有设置 securityContext 的 Deployment
kubectl get deployments -A -o json | \
jq -r '.items[] | select(.spec.template.spec.securityContext == null) | "\(.metadata.namespace)/\(.metadata.name)"'
# 2. 强制 Pod Security Standards(K8s 1.25+ GA)
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
4.2 注意事项
4.2.1 版本升级注意事项
K8s 1.32相比之前版本有几个重要的行为变化:
| 变更项 |
影响 |
应对措施 |
| cgroup v2成为唯一支持 |
cgroup v1的节点无法加入集群 |
升级前确认所有节点的cgroup版本 |
| Flowcontrol API GA |
APF(API Priority and Fairness)替代max-inflight |
检查自定义的FlowSchema配置 |
| kubectl事件格式变化 |
kubectl get events 输出格式调整 |
更新依赖事件输出的脚本 |
| CSI Migration完成 |
in-tree存储插件全部迁移到CSI |
确认CSI驱动版本兼容性 |
4.2.2 常见配置错误速查
| 错误现象 |
根因 |
快速修复 |
| Pod反复CrashLoopBackOff |
OOMKill或应用启动失败 |
查 kubectl describe pod 的Last State |
| Service无法访问 |
selector和Pod label不匹配 |
kubectl get endpoints 确认是否为空 |
| Ingress返回404 |
path或host配置错误 |
检查Ingress Controller日志 |
| PVC一直Pending |
StorageClass不存在或配额不足 |
kubectl describe pvc 看Events |
| 节点SchedulingDisabled |
被cordon了没uncordon |
kubectl uncordon <node> |
| HPA不生效 |
metrics-server没部署或Resource没设Request |
检查 kubectl get hpa 的TARGETS列 |
五、故障排查和监控
5.1 故障排查
5.1.1 通用排查流程
不管遇到什么问题,排查思路都是从上到下、从外到内:
用户请求 -> Ingress/LB -> Service -> Pod -> Container -> Application
|
Node (kubelet/containerd/OS)
|
Cluster (etcd/apiserver/controller)
# 第一步:确认问题范围
kubectl get nodes # 节点是否健康
kubectl get pods -n <ns> -o wide # Pod 状态和分布
kubectl get events -n <ns> --sort-by='.lastTimestamp' | tail -30 # 最近事件
# 第二步:定位问题 Pod
kubectl describe pod <pod> -n <ns> # 详细状态和事件
kubectl logs <pod> -n <ns> --tail=100 # 应用日志
kubectl logs <pod> -n <ns> -p # 上一次崩溃的日志(关键)
# 第三步:检查网络连通性
kubectl exec <pod> -n <ns> -- wget -qO- --timeout=3 http://<service>:<port>/healthz
kubectl exec <pod> -n <ns> -- nslookup <service>
# 第四步:检查资源状态
kubectl top pod <pod> -n <ns> # 实时资源使用
kubectl get events -n <ns> --field-selector involvedObject.name=<pod>
5.1.2 高频问题快速定位表
| 症状 |
第一步排查命令 |
常见原因 |
| Pod Pending |
kubectl describe pod 看Events |
资源不足 / nodeSelector不匹配 / PVC未绑定 |
| Pod CrashLoopBackOff |
kubectl logs <pod> -p |
应用启动失败 / OOMKill / 配置错误 |
| Pod ContainerCreating |
kubectl describe pod 看Events |
镜像拉取失败 / 存储卷挂载失败 / Secret不存在 |
| Service不通 |
kubectl get endpoints |
selector不匹配 / Pod未Ready / NetworkPolicy拦截 |
| Node NotReady |
SSH到节点查 journalctl -u kubelet |
kubelet挂了 / 磁盘满 / containerd故障 |
| API Server慢 |
etcdctl endpoint status |
etcd磁盘慢 / API Server过载 / 大量LIST请求 |
5.1.3 调试工具箱
# 临时调试 Pod(当目标 Pod 没有 shell 时)
# K8s 1.25+ ephemeral containers GA
kubectl debug <pod> -n <ns> -it --image=nicolaka/netshoot --target=<container>
# 在指定节点上启动调试 Pod
kubectl debug node/<node-name> -it --image=nicolaka/netshoot
# 抓包分析(在调试容器中)
tcpdump -i eth0 -nn -c 100 port 80
# 查看 iptables/nftables 规则(Service 网络排查)
# 如果使用 kube-proxy iptables 模式
iptables-save | grep <service-cluster-ip>
# 如果使用 Cilium
cilium service list
cilium endpoint list
5.2 性能监控
5.2.1 必须监控的核心指标
| 层级 |
指标 |
告警阈值 |
数据源 |
| etcd |
etcd_disk_wal_fsync_duration_seconds p99 |
> 10ms warning, > 50ms critical |
etcd metrics |
| etcd |
etcd_server_proposals_failed_total |
> 0 持续5分钟 |
etcd metrics |
| API Server |
apiserver_request_duration_seconds p99 |
> 1s |
apiserver metrics |
| API Server |
apiserver_request_total 5xx比例 |
> 1% |
apiserver metrics |
| kubelet |
kubelet_running_pods |
接近 --max-pods 限制 |
kubelet metrics |
| Node |
磁盘使用率 |
> 80% warning, > 90% critical |
node-exporter |
| Node |
内存使用率 |
> 85% warning, > 95% critical |
node-exporter |
| Pod |
kube_pod_container_status_restarts_total |
5分钟内 > 3次 |
kube-state-metrics |
| Certificate |
certmanager_certificate_expiration_timestamp_seconds |
< 7天 |
cert-manager |
| CoreDNS |
coredns_dns_request_duration_seconds p99 |
> 100ms |
CoreDNS metrics |
5.2.2 Prometheus告警规则示例
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: k8s-core-alerts
namespace: monitoring
spec:
groups:
- name: kubernetes-core
rules:
# Pod 频繁重启
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total[1h])>5
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} restarted {{ $value }} times in 1h"
# 节点磁盘即将满
- alert: NodeDiskPressure
expr: (node_filesystem_avail_bytes{mountpoint="/"}/node_filesystem_size_bytes{mountpoint="/"})<0.15
for: 5m
labels:
severity: warning
annotations:
summary: "Node {{ $labels.instance }} root disk usage > 85%"
# etcd leader 频繁切换
- alert: EtcdLeaderChanges
expr: increase(etcd_server_leader_changes_seen_total[1h])>3
for: 5m
labels:
severity: critical
annotations:
summary: "etcd leader changed {{ $value }} times in 1h"
# API Server 错误率升高
- alert: ApiServerErrorRate
expr: sum(rate(apiserver_request_total{code=~"5.."}[5m]))/sum(rate(apiserver_request_total[5m]))>0.01
for: 10m
labels:
severity: critical
annotations:
summary: "API Server 5xx error rate > 1%"
5.3 备份与恢复
5.3.1 etcd备份(集群的生命线)
#!/bin/bash
# etcd 定时备份脚本
# 建议通过 crontab 每小时执行一次
BACKUP_DIR="/backup/etcd"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
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"
# 创建备份
ETCDCTL_API=3 etcdctl snapshot save "${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db" \
--endpoints=${ENDPOINTS} \
--cacert=${CACERT} \
--cert=${CERT} \
--key=${KEY}
# 验证备份完整性
ETCDCTL_API=3 etcdctl snapshot status "${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db" \
--write-out=table
# 清理 7 天前的备份
find ${BACKUP_DIR} -name "etcd-snapshot-*.db" -mtime +7 -delete
echo "[$(date)] etcd backup completed: etcd-snapshot-${TIMESTAMP}.db"
5.3.2 集群资源备份
# 使用 Velero 进行集群资源和 PV 数据备份
# 安装 Velero
velero install \
--provider aws \
--bucket k8s-backup \
--secret-file ./credentials-velero \
--backup-location-config region=us-east-1 \
--snapshot-location-config region=us-east-1
# 创建定时备份计划
velero schedule create daily-backup \
--schedule="0 2 * * *" \
--include-namespaces production,staging \
--ttl 168h
# 恢复操作
velero restore create --from-backup daily-backup-20260206020000
六、总结
6.1 技术要点回顾
这10个故障场景覆盖了K8s生产环境中最常见的故障场景,核心要点:
- etcd是集群的命脉:给它独立的NVMe SSD,监控WAL fsync延迟,定期压缩和碎片整理
- 资源管理是基本功:Request基于实际用量,Memory Limit留余量,CPU Limit慎重设置,LimitRange和ResourceQuota是安全网
- 零停机更新三件套:
maxUnavailable=0 + readinessProbe + preStop hook
- PDB不是可选项:多副本服务必须配PDB,配合Pod反亲和性分散副本
- RBAC最小权限:从零开始按需授权,禁止cluster-admin滥用,关闭不必要的Token挂载
- ConfigMap更新有坑:subPath不更新、环境变量不更新,用Reloader或hash注解方案
- 节点NotReady排查:kubelet -> containerd -> 磁盘/内存/网络 -> 证书,逐层排查
- DNS ndots必须调优:外部服务为主设
ndots:2,部署NodeLocal DNSCache
- 证书必须自动化管理:cert-manager + 过期告警,手动管理必翻车
- 存储卷问题看四层:PVC状态 -> VolumeAttachment -> CSI驱动 -> 节点挂载
6.2 防御性运维体系
把事后救火变成事前防御,需要建立三道防线:
第一道防线:准入控制
- LimitRange / ResourceQuota防止资源滥用
- Pod Security Standards防止特权容器
- OPA/Kyverno策略引擎强制执行组织规范
第二道防线:监控告警
- 核心组件指标监控(etcd、apiserver、kubelet)
- 业务指标监控(Pod重启次数、错误率、延迟)
- 证书过期、磁盘空间、节点状态告警
第三道防线:备份恢复
- etcd定时快照备份
- Velero集群资源和PV数据备份
- 定期演练恢复流程,确保备份可用
附录
A. 命令速查表
# 集群状态快速检查
kubectl get nodes -o wide # 节点状态
kubectl get pods -A | grep -v Running # 非 Running 的 Pod
kubectl top nodes # 节点资源使用
kubectl get events -A --sort-by='.lastTimestamp' | tail -20 # 最近事件
# etcd 健康检查
etcdctl endpoint health --write-out=table
etcdctl endpoint status --write-out=table
etcdctl alarm list
# 证书检查
kubeadm certs check-expiration # 控制面证书
kubectl get certificates -A # cert-manager 管理的证书
# 存储排查
kubectl get pvc -A | grep -v Bound # 未绑定的 PVC
kubectl get volumeattachment # 卷挂载状态
# 网络排查
kubectl get endpoints -A | awk '$2==""' # 空 Endpoints 的 Service
kubectl get networkpolicy -A # 网络策略
B. 关键配置参数速查
| 参数 |
默认值 |
推荐值 |
说明 |
ndots |
5 |
2(外部服务为主) |
DNS search域触发阈值 |
terminationGracePeriodSeconds |
30 |
60 |
Pod优雅关闭等待时间 |
maxUnavailable |
25% |
0 |
滚动更新最大不可用数 |
maxSurge |
25% |
1 |
滚动更新最大超出数 |
node-monitor-grace-period |
40s |
40s |
节点心跳超时判定 |
eviction-hard memory.available |
100Mi |
500Mi |
内存驱逐阈值 |
eviction-hard nodefs.available |
10% |
15% |
磁盘驱逐阈值 |
etcd auto-compaction-retention |
0(关闭) |
1h |
etcd自动压缩周期 |
如果你在实践过程中遇到了文中未覆盖的其他棘手问题,或者有不同的见解和优化方案,欢迎在云栈社区的技术论坛进行交流讨论,共同探索更稳定、更高效的云原生运维之道。