找回密码
立即注册
搜索
发回帖 发新帖

4885

积分

0

好友

621

主题
发表于 2026-2-12 15:04:27 | 查看: 443| 回复: 0

一、概述

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

常见的错误配置模式:

  1. Limit设得太低:Java应用的JVM堆 + 非堆 + 线程栈 + NIO DirectBuffer加起来超过Limit
  2. Request远小于Limit(过度超卖):节点实际内存不够,触发节点级OOM
  3. 没设Limit:BestEffort QoS,节点内存紧张时第一个被干掉
  4. 忽略了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 一起工作。任何一个配错都可能导致服务中断:

    1. maxUnavailable设太大:比如replicas=3且maxUnavailable=2,更新时只剩1个Pod扛流量
    2. readinessProbe缺失或配置不当:新Pod还没准备好就被加入Service Endpoints
    3. terminationGracePeriodSeconds太短:旧Pod还在处理请求就被强制Kill
    4. preStop hook缺失:Pod收到SIGTERM后立即停止接收请求,但Endpoints还没更新,流量还在往这个Pod发
    5. 启动时间过长:应用启动需要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。权限过大的常见原因:

    1. 图省事绑cluster-admin:开发初期为了方便,给所有ServiceAccount都绑了cluster-admin
    2. ClusterRoleBinding滥用:本来只需要某个命名空间的权限,却用了ClusterRoleBinding授予全集群权限
    3. 通配符权限:resources: ["*"] 和 verbs: ["*"] 一把梭
    4. ServiceAccount Token泄露:默认挂载的Token被应用日志打印或被攻击者获取
    5. 没有定期审计:权限只增不减,离职员工的权限没有回收

    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的更新机制有几个关键细节:

    1. Volume挂载方式:以Volume形式挂载的ConfigMap,kubelet会定期同步更新(默认同步周期60秒 + 一定的缓存TTL),但不是实时的
    2. subPath挂载不更新:使用 subPath 挂载的文件 永远不会自动更新,这是K8s的设计行为,不是Bug
    3. 环境变量方式不更新:通过 envFrom 或 env.valueFrom 引用的ConfigMap/Secret,Pod运行期间不会更新,必须重启Pod
    4. 应用不感知文件变化:即使文件更新了,应用如果没有watch文件变化的逻辑,也不会重新加载配置
    5. 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。

    常见原因按频率排序:

    1. kubelet进程挂了:OOM、证书过期、配置错误
    2. containerd/CRI运行时故障:containerd进程卡死、shim进程泄漏
    3. 系统资源耗尽:磁盘满(特别是/var/lib/kubelet和/var/lib/containerd)、inode耗尽、内存不足
    4. 内核问题:内核Bug导致的soft lockup、cgroup泄漏
    5. 网络问题:节点到API Server的网络中断
    6. 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解析器会依次尝试:

    1. api.example.com.default.svc.cluster.local -- 失败
    2. api.example.com.svc.cluster.local -- 失败
    3. api.example.com.cluster.local -- 失败
    4. 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年)。证书过期的常见原因:

    1. 没有自动续期机制:手动申请的证书,到期了没人记得续
    2. cert-manager续期失败:ACME challenge失败(DNS记录没配对、HTTP-01验证路径被WAF拦截)但没有告警
    3. 证书更新了但Ingress Controller没重载:Nginx Ingress Controller的SSL证书热更新有时会失败
    4. 通配符证书覆盖不全:*.example.com 不覆盖 example.com(裸域名)和 *.sub.example.com(多级子域名)
    5. 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)驱动负责实现这三个阶段的具体操作。

    常见的存储卷挂载失败原因:

    1. Multi-Attach冲突:RWO(ReadWriteOnce)类型的PV只能挂载到一个节点。Pod被调度到新节点时,旧节点上的VolumeAttachment还没释放,新节点挂载失败
    2. CSI驱动Pod异常:CSI Node Plugin(DaemonSet)或CSI Controller(Deployment)挂了
    3. 云厂商API限流:大量Pod同时创建时,云盘API被限流导致Attach超时
    4. 存储后端故障:NFS服务器不可达、Ceph集群降级、云盘服务异常
    5. PV/PVC状态不匹配:PVC处于Pending状态(StorageClass不存在或配额不足)
    6. 文件系统损坏:非正常卸载导致文件系统需要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自动压缩周期

    如果你在实践过程中遇到了文中未覆盖的其他棘手问题,或者有不同的见解和优化方案,欢迎在云栈社区的技术论坛进行交流讨论,共同探索更稳定、更高效的云原生运维之道。




    上一篇:Mock 与 Spy 的核心哲学:Jest/Sinon 测试中的依赖隔离与有效性风险
    下一篇:Qwen-2.0-Image技术解析:从2K原生画质到专业级排版,AI绘图进入设计时代
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-10-15 09:41 , Processed in 0.137395 second(s), 41 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表