K8s生产环境那些文档不会告诉你的坑
写在前面用 K8s 好几年了从最开始的”照着文档搭集群”到现在管理几十个节点的生产集群踩过的坑已经够写一本书了。官方文档当然很重要但文档告诉你的是”怎么用”不会告诉你 用了之后会出什么问题。很多坑只有你真正在生产上跑过、出过事之后才知道。这篇文章整理了我在生产环境遇到过的 7 个坑每一个都是真金白银的教训。有些坑让我半夜爬起来处理有些坑让我在复盘会上被问得哑口无言。希望你看完之后能少走一些我走过的弯路。坑一etcd 磁盘满了整个集群变成只读发生了什么某天上午 10 点开发同学跑来说”K8s 集群不能创建新 Pod 了。”我上去一看$ kubectl run test --imagenginx Error from server (Forbidden): error when creating test: pods test is forbidden: etcdserver: request timed out不只是创建 Podkubectl get nodes 都开始超时了。赶紧 ssh 到 master 节点$ journalctl -u etcd --no-pager -n 50 etcdserver: mvcc: database space exceededetcd 磁盘空间超限了。etcd 有一个默认的存储配额2GB当数据量超过配额时etcd 会变成只读模式拒绝所有写操作。而 K8s 的几乎所有操作创建 Pod、更新 Deployment、甚至心跳上报都依赖 etcd 写入所以整个集群就”瘫痪”了。为什么会满查了一下罪魁祸首是事件Event记录。K8s 会为每个 Pod 的每次事件调度、拉取镜像、启动、重启、OOM 等创建一个 Event 对象存在 etcd 里。我们有个服务因为配置问题一直在 CrashLoopBackOff每次重启都会产生一条 Event。跑了三天产生了 100 多万条 Event把 etcd 撑爆了。# 查看事件数量 $ kubectl get events --all-namespaces | wc -l 1234567怎么解决紧急止血# 临时扩大 etcd 配额从 2GB 扩到 8GB $ etcdctl --endpointshttps://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 \ put quota/bytes -- 8589934592扩容后 etcd 恢复写入集群恢复正常。清理事件# 删除所有 namespace 下的旧事件保留最近 1 小时的 $ kubectl get events --all-namespaces --field-selector eventTimestamp2026-04-27T09:00:00Z \ | awk {print $1} | sort -u | xargs -I {} kubectl delete events --field-selector eventTimestamp2026-04-27T09:00:00Z -n {}永久预防部署 eventrouter 或使用 kube-controller-manager 自带的事件清理功能# 在 kube-controller-manager 配置中设置事件 TTL $ vi /etc/kubernetes/manifests/kube-controller-manager.yaml spec: containers: - command: - kube-controller-manager - --terminated-pod-gc-threshold1000 - --event-ttl1h # 事件保留 1 小时后自动清理教训etcd 磁盘空间是 K8s 集群的”生命线”一旦满了整个集群就废了。一定要设置事件 TTL并且监控 etcd 的存储使用量。建议在监控里加一条规则etcd 存储使用率超过 70% 就告警。坑二CoreDNS 的 ndots:5 让 DNS 解析慢了 5 倍发生了什么上线了一个新服务调用方反馈”第一次请求特别慢要 2-3 秒之后就正常了”。第一反应是”连接池冷启动”但查了应用日志连接池是预热过的。用 dig 在 Pod 里测试 DNS 解析$ kubectl exec -it dev-app-pod -- dig dev-service.default.svc.cluster.local ;; Query time: 12 msec12ms看起来正常。但测试一个不存在的域名$ kubectl exec -it dev-app-pod -- dig dev-service.default.svc.cluster.local.xxx ;; Query time: 3125 msec3 秒 这就是第一次请求慢的原因。为什么会这样K8s Pod 里的 DNS 配置默认是这样的$ kubectl exec -it dev-app-pod -- cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5关键在 ndots:5。DNS 解析的规则是如果域名中”.“的数量小于 ndots这里是 5会先按照 search 列表中的后缀依次拼接尝试解析全部失败后才用原始域名解析。比如解析 dev-service1 个点小于 5DNS 会按这个顺序尝试1. dev-service.default.svc.cluster.local → NXDOMAIN 2. dev-service.svc.cluster.local → NXDOMAIN 3. dev-service.cluster.local → NXDOMAIN 4. dev-service原始域名 → 成功每次 NXDOMAIN 都要等超时默认 5 秒所以最坏情况下要等 15 秒以上。我们的应用在连接数据库时用的是短域名 mysql每次解析都要走完整个 search 列表所以第一次连接特别慢。怎么解决方案一用完整域名最简单代码里把 mysql 改成 mysql.default.svc.cluster.local域名中的点数超过 ndots直接解析不走 search。方案二降低 ndots推荐在 CoreDNS 的 ConfigMap 中把 ndots 改成 2$ kubectl edit configmap coredns -n kube-system apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } prometheus :9153 forward . /etc/resolv.conf { max_concurrent 1000 } cache 30 loop reload loadbalance }然后在 Pod 的 DNS 配置中覆盖 ndotsspec: dnsConfig: options: - name: ndots value: 2或者通过 Deployment 的 dnsPolicy 设置spec: template: spec: dnsConfig: options: - name: ndots value: 2方案三给内部服务加 headless Service 的 A 记录如果服务名是 mysql可以创建一个同名的 headless Service让 DNS 直接解析到对应的 IP不需要走 search 列表。教训如果你的应用里用了短域名连接其他服务一定要关注 DNS 解析耗时。建议把 ndots 从 5 降到 2对绝大多数场景都够用而且能显著减少 DNS 查询次数。坑三Secret 更新了Pod 里还是旧的发生了什么我们有个服务连接第三方 API用的密钥存在 K8s Secret 里。密钥到期了运维同学更新了 Secret$ kubectl create secret generic api-secret \ --from-literalapi-keynew-key-12345 \ --dry-runclient -o yaml | kubectl apply -f -更新完之后通知开发同学”密钥已更新”。开发同学说”好的”然后继续用。过了半小时第三方 API 那边反馈我们的请求还是用的旧密钥。开发同学查了应用日志确认应用读到的确实是旧密钥。“Secret 不是已经更新了吗”为什么会这样K8s Secret 有两种挂载方式方式一环境变量env: - name: API_KEY valueFrom: secretKeyRef: name: api-secret key: api-key方式二Volume 挂载volumes: - name: secret-volume secret: secretName: api-secret volumeMounts: - name: secret-volume mountPath: /etc/secrets readOnly: true两种方式的更新行为完全不同挂载方式Secret 更新后需要重启 Pod 吗环境变量不会更新Pod 里还是旧值必须重启Volume 挂载会更新K8s 会定期同步大约 1 分钟不需要重启我们的服务用的是环境变量方式挂载 Secret所以更新 Secret 后Pod 里读到的还是旧值。必须重启 Pod 才能生效。但运维同学不知道这个区别以为更新了 Secret 就完事了。怎么解决短期重启 Pod$ kubectl rollout restart deployment app-service长期如果需要 Secret 热更新改用 Volume 挂载方式。应用层做文件监听检测到文件变化后重新加载密钥。spec: containers: - name: dev-app volumeMounts: - name: secret-volume mountPath: /etc/secrets readOnly: true env: - name: SECRET_PATH value: /etc/secrets/api-key volumes: - name: secret-volume secret: secretName: api-secret应用代码里监听文件变化// Go 示例监听 Secret 文件变化 watcher, _ : fsnotify.NewWatcher(/etc/secrets/api-key) go func() { for { select { case event : -watcher.Events: if event.Opfsnotify.Write fsnotify.Write { newKey, _ : os.ReadFile(/etc/secrets/api-key) reloadAPIKey(string(newKey)) } } } }()教训K8s Secret 的更新机制是面试高频题但在生产上踩坑的人真的不少。如果你不确定团队是否清楚这个区别建议在运维文档里明确写上更新 Secret 后必须确认 Pod 的挂载方式如果是环境变量则需要重启 Pod。坑四HPA 扩容了但新 Pod 一直 Pending发生了什么晚上 8 点流量高峰来了。HPA 正常触发了扩容$ kubectl get hpa NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS dev-service Deployment/dev-service/Scale 85%/80% 3 20 8副本数从 3 扩到了 8。但业务同学说”还是扛不住响应时间还是很长”。我上去一看8 个 Pod 里只有 3 个是 Running剩下 5 个全是 Pending$ kubectl get pods | grep dev-service dev-service-xxx-abc12 1/1 Running 0 15m dev-service-xxx-def34 1/1 Running 0 15m dev-service-xxx-ghi56 1/1 Running 0 15m dev-service-xxx-jkl78 0/1 Pending 0 5m dev-service-xxx-mno90 0/1 Pending 0 5m dev-service-xxx-pqr12 0/1 Pending 0 5m dev-service-xxx-stu34 0/1 Pending 0 5m dev-service-xxx-vwx56 0/1 Pending 0 5mHPA 扩了但新 Pod 调度不上去。等于 HPA 做了无用功。为什么会这样$ kubectl describe pod dev-service-xxx-jkl78 | tail -10 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 5m default-scheduler 0/5 nodes are available: 3 Insufficient cpu, 2 Insufficient memory.节点资源不够了。问题出在我们的 Pod 的 resources.requests 设得太低了resources: requests: cpu: 100m memory: 128Mi limits: cpu: 2 memory: 4Girequest 设了 100m CPU但实际运行时每个 Pod 要用 800m-1500m CPU。调度器按 request 算觉得每个节点还能塞很多 Pod就都调度上去了。结果 Pod 跑起来之后疯狂抢占 CPU节点负载飙升真正需要扩容时反而没资源了。这就是经典的request 和 limit 差距过大的问题。怎么解决紧急手动清理一些低优先级的 Pod腾出资源# 查看哪些 Pod 占用资源最多 $ kubectl top pods --all-namespaces | sort -k3 -rn | head -20 # 临时缩容非核心服务 $ kubectl scale deployment non-critical-service --replicas0长期合理设置 request 值request 应该反映 Pod 的实际资源使用量而不是”最小可能值”。建议用以下方法确定# 查看 Pod 历史资源使用量需要 metrics-server $ kubectl top pod dev-service-xxx-abc12 --no-headers # 输出类似523m 384Mi # 建议取 P50 或 P70 的值作为 request # 如果 P50 CPU 500mP70 CPU 800m # 那 request 设为 600m-800m 比较合理 resources: requests: cpu: 800m # 接近实际使用量 memory: 512Mi limits: cpu: 2000m memory: 2Gi教训request 设太低不是”省资源”而是”骗调度器”。调度器按 request 算你告诉它每个 Pod 只要 100m它就会往节点上塞很多 Pod结果实际运行时资源不够真正需要扩容时反而没地方放。request 应该设成实际使用量的 70%-80%给一些余量但不要差太多。坑五kubectl drain 把集群搞崩了发生了什么有次要升级一批节点的内核版本需要先把节点上的 Pod 驱逐走。我用了 kubectl drain$ kubectl drain worker-03 --ignore-daemonsets --delete-emptydir-data node/worker-03 cordoned evicting pod default/my-service-xxx-abc12 evicting pod kube-system/calico-node-xxx ...看起来正常。然后我对 worker-04 执行了同样的操作。然后对 worker-05然后监控告警炸了多个服务不可用。为什么会这样kubectl drain 会驱逐节点上的所有 Pod除了 DaemonSet。但驱逐是”优雅的”——它会先发 SIGTERM等 Pod 完成清理工作后再删除。问题出在我们的 Pod 没有配置优雅终止。# 很多 Pod 的 terminationGracePeriodSeconds 用的是默认值 30 秒 # 但应用没有监听 SIGTERM 信号不会主动退出Pod 收到 SIGTERM 后应用不处理30 秒后 kubelet 发 SIGKILL 强杀。但在这 30 秒内Pod 还是 Running 状态Service 的 endpoint 里还有它。当我同时 drain 3 个节点时3 个节点上的 Pod 都在”等死”状态。新 Pod 调度到其他节点需要时间而旧 Pod 还没完全退出。Service 的 endpoint 里混着”正在退出的旧 Pod”和”刚启动的新 Pod”流量分发混乱部分请求打到了正在退出的 Pod 上导致错误。更糟糕的是有个服务的 Pod 里有本地缓存Redis 连接池优雅终止时需要 30 秒来关闭连接。但 terminationGracePeriodSeconds 只有 30 秒还没来得及关完就被杀了导致 Redis 连接泄漏。怎么解决1. 配置优雅终止spec: terminationGracePeriodSeconds: 60 # 给足够的时间 containers: - name: dev-app lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10] # 预停止钩子先从 Service 摘除 # 应用本身需要监听 SIGTERM 信号2. 配置 PodDisruptionBudgetapiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: dev-service-pdb spec: minAvailable: 50% selector: matchLabels: app: dev-service有了 PDBkubectl drain 在驱逐 Pod 之前会检查驱逐后是否还有足够的可用 Pod。如果不够drain 会拒绝操作避免服务不可用。$ kubectl drain worker-03 --ignore-daemonsets error when evicting pods/default/my-service-xxx: Cannot evict pod as it would violate the pods disruption budget.3. 逐个节点操作不要同时 drain 多个节点# 正确做法一个一个来 $ kubectl drain worker-03 --ignore-daemonsets --delete-emptydir-data # 等所有 Pod 都迁移完成 $ kubectl get nodes worker-03 # 确认 Ready 后再操作下一个 $ kubectl drain worker-04 --ignore-daemonsets --delete-emptydir-data教训kubectl drain 看起来是个简单的命令但它会触发一系列连锁反应。在执行之前确保① 配置了 PDB② Pod 有优雅终止逻辑③ 逐个节点操作不要贪快。坑六日志采集把节点打挂了发生了什么某天凌晨 3 点值班同事打电话给我”好几台机器 CPU 100%服务全挂了。”我迷迷糊糊爬起来打开电脑一看确实是好几台 worker 节点 CPU 飙满。但不是我们的业务应用而是$ top -bn1 | head -20 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 5678 root 20 0 512.4m 128.6m 12.2m S 95.3 1.6 2345:12.78 fluentd 5679 root 20 0 498.2m 115.3m 10.8m S 92.1 1.4 2198:34.56 fluentdFluentd日志采集组件占了 90% 的 CPU。为什么会这样查了一下 Fluentd 的日志发现它在疯狂处理日志。原因是有个服务的日志级别被开发同学调试时临时改成了 DEBUG每秒产生上百 MB 的日志。Fluentd 拼命采集、解析、转发CPU 直接被打满了。更惨的是Fluentd 是以 DaemonSet 方式部署的每个节点上都跑着一个。所以日志暴增的服务的 Pod 在哪个节点上那个节点的 Fluentd 就被打满进而影响节点上所有其他 Pod。怎么解决紧急先给 Fluentd 加资源限制防止它把节点打挂apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd namespace: kube-system spec: template: spec: containers: - name: fluentd resources: requests: cpu: 100m memory: 200Mi limits: cpu: 1000m # 限制最大 CPU memory: 1Gi然后找到产生大量日志的 Pod把日志级别改回正常# 找到日志量最大的 Pod $ kubectl top pods --all-namespaces | sort -k3 -rn | head -10 # 修改日志级别 $ kubectl set env deployment dev-service LOG_LEVELINFO长期1、给所有日志采集组件设置资源限制很多团队用 DaemonSet 部署 Fluentd/Filebeat 时不设 limits这是定时炸弹2、配置日志采集的速率限制# Fluentd 配置 source type tail path /var/log/containers/*.log pos_file /var/log/fluentd-containers.log.pos tag kubernetes.* read_from_head true parse type json time_key time time_format %Y-%m-%dT%H:%M:%S.%NZ /parse /source # 限制采集速率 filter kubernetes.** type throttle group_key log group_bucket_limit 100 # 每秒最多采集 100 条 group_interval 10s group_reset_rate 10/m /filter3、在监控里加上日志采集组件的资源使用告警教训DaemonSet 部署的组件日志采集、监控 Agent一定要设资源限制。它们跑在每个节点上一旦出问题影响面是全集群级别的。不要相信”日志采集组件很轻量”这种说法——它轻量是正常的但不正常的时候能把你整个集群搞崩。坑七滚动更新导致的连接中断发生了什么某次发布新版本用的是正常的滚动更新$ kubectl set image deployment/dev-service dev-appevd-app:v2.0.0 deployment.apps/dev-service image updated发布过程中监控显示有大约 1-2% 的请求返回了 502。业务同学问”不是滚动更新吗为什么还有请求失败”为什么会这样滚动更新的流程是这样的创建新 Pod等待新 Pod Ready新 Pod Ready 后删除旧 Pod重复直到所有旧 Pod 被替换问题出在第 1 步和第 2 步之间。新 Pod 变成 Ready意味着 readinessProbe 通过了。但 readinessProbe 通过 ≠ 应用真正能处理请求。我们的应用启动流程是这样的JVM 启动 → Spring 初始化 → 连接数据库 → 加载缓存 → 开始监听端口readinessProbe 配置的是 TCP 端口检查readinessProbe: tcpSocket: port: 8080 initialDelaySeconds: 10 periodSeconds: 5端口打开了readinessProbe 就通过了。但这时候 Spring 可能还在初始化数据库连接池、加载缓存。如果这时候有请求打过来应用能收到但处理不了就会返回 502。而旧 Pod 被删除后它的连接是直接断掉的不会有优雅关闭的过程。怎么解决1. 改用 HTTP 探针检查应用真正就绪readinessProbe: httpGet: path: /health/ready # 应用提供一个真正的就绪检查接口 port: 8080 initialDelaySeconds: 15 periodSeconds: 5 failureThreshold: 3应用的 /health/ready 接口应该检查GetMapping(/health/ready) public ResponseEntityString ready() { // 检查数据库连接池是否就绪 if (!dataSource.isRunning()) { return ResponseEntity.status(503).body(database not ready); } // 检查缓存是否加载完成 if (!cacheManager.isReady()) { return ResponseEntity.status(503).body(cache not ready); } return ResponseEntity.ok(ok); }2. 配置 preStop 钩子让旧 Pod 优雅退出spec: terminationGracePeriodSeconds: 60 containers: - name: my-app lifecycle: preStop: exec: command: [/bin/sh, -c, sleep 10]preStop 的 sleep 10 看起来很蠢但它有重要作用让 kubelet 在发 SIGTERM 之前先等 10 秒。在这 10 秒内endpoint controller 会把旧 Pod 从 Service 的 endpoint 列表中移除。这样就不会有新请求打到正在退出的旧 Pod 上了。时序是这样的t0s preStop sleep 10 开始执行 t0s endpoint controller 从 endpoint 列表中移除旧 Pod t10s preStop 执行完毕kubelet 发送 SIGTERM t10s 应用收到 SIGTERM开始优雅关闭 t40s 应用完成清理主动退出 t60s terminationGracePeriodSeconds 到期强制 SIGKILL3. 配置 maxSurgespec: strategy: rollingUpdate: maxSurge: 1 # 滚动更新时最多多创建 1 个 Pod maxUnavailable: 0 # 滚动更新时不允许有 Pod 不可用maxUnavailable: 0 确保更新过程中可用 Pod 数量不会减少。maxSurge: 1允许临时多一个 Pod 来承接流量。教训滚动更新”零中断”不是 K8s 默认就能做到的需要三个条件配合① HTTP 就绪探针不是 TCP② preStop 钩子 合理的 terminationGracePeriodSeconds③ 合理的 maxSurge 和 maxUnavailable。缺一个都可能出问题。最后整理一份清单把上面 7 个坑的预防措施整理成一份上线前检查清单#检查项不做的后果操作1etcd 存储监控 事件 TTLetcd 满了集群瘫痪设置 --event-ttl1h监控 etcd 存储使用率2DNS ndots 配置DNS 解析慢首次请求延迟高ndots 改为 2内部服务用完整域名3Secret 挂载方式更新 Secret 后应用不生效需要热更新用 Volume 挂载否则更新后要重启 Pod4resources.request 合理设置HPA 扩容时调度不上request 设为实际使用量的 70%-80%5PodDisruptionBudget 优雅终止drain 节点时服务中断配置 PDB preStop terminationGracePeriodSeconds6DaemonSet 资源限制日志采集/监控组件打满节点 CPU所有 DaemonSet 必须设 requests limits7滚动更新零中断配置发布时有少量请求失败HTTP 探针 preStop maxSurge/maxUnavailable如果你也在用 K8s 跑生产环境并且踩过其他文档里没写的欢迎在评论区分享。踩过的坑多了路就平了。