路线图

面试 · 场景排查型

星辉 2026-07-02 阅读 25 min 5,141 字 路线图
面试 · 场景排查型 封面

实战排障思路。每题给排查决策树(不是列一堆命令),从故障现象逐层下钻到根因。包含真实案例改编的故障场景。


子类 1:CI/CD 流水线排障#

题目1:流水线从 5 分钟变 30 分钟,怎么查?#

故障现象: 某服务的 CI 流水线原本 5 分钟完成,最近一周逐渐变慢到 30 分钟。构建步骤、测试步骤、部署步骤都变慢了,但没有代码量暴增。

排查路径:

text
流水线变慢
  ├── 是哪一步变慢?
  │     ├── 分 stage 计时(云效/GitHub Actions 都有 per-step timing)
  │     ├── 如果是 build 慢 → 跳到 ①
  │     ├── 如果是 test 慢 → 跳到 ②
  │     └── 如果是 deploy 慢 → 跳到 ③
  │
  ① build 慢
  ├── 构建机 CPU/内存/磁盘 IO 水位?
  │     ├── kubectl top node / docker stats 看构建机资源
  │     └── 高 → 是不是其他 job 占用?构建机池太小?
  ├── Docker layer cache 失效?
  │     ├── 看 build 日志 "CACHED" 字样数量
  │     └── 失效 → Dockerfile 顺序改了?base image 更新了?
  ├── 依赖下载慢?
  │     ├── pip/npm/go mod 下载时间
  │     └── 慢 → 镜像源切换?构建机访问境外慢?
  └── 构建产物体积变大?
        └── 是不是把不必要的文件打进镜像?
  
  ② test 慢
  ├── 测试用例数暴增?
  ├── 测试并发数下降?
  │     └── CI 配置改了?runner 资源限制?
  └── 测试等待外部依赖(DB/Redis)慢?
        └── 测试环境资源不足?
  
  ③ deploy 慢
  ├── ArgoCD sync 慢?
  │     ├── argocd app sync --timeout 看 sync 耗时
  │     ├── 大量 OutOfSync 资源堆积?
  │     └── application-controller CPU 高?(kubectl top -n argocd)
  ├── Pod 启动慢?
  │     ├── 镜像拉取慢 → registry 跨境?镜像体积大?
  │     ├── ReadinessProbe 太严格?
  │     └── 节点资源不足导致调度慢?
  └── wait 逻辑死等?
        └── 等 App health=Healthy 但 baseline OutOfSync 导致 Degraded
            → 改成等 Pod Ready 而非 App health

关键命令:

bash
# 1. 看 ArgoCD App sync 耗时
argocd app history <app-name>
# 2. 看 application-controller 资源
kubectl top pod -n argocd -l app.kubernetes.io/name=argocd-application-controller
# 3. 看镜像拉取耗时
kubectl describe pod <pod> | grep -A5 Events
# 4. 看构建机资源
docker stats  # 或 kubectl top node

根因分析: 逐渐变慢(不是突变)通常是资源累积导致——① registry 镜像增多导致拉取变慢;② 构建机磁盘满导致 cache 失效;③ ArgoCD Application 数量增长导致 controller 压力大;④ 数据库表数据量增长导致测试慢。

修复方案: ① 构建机定期清理(docker system prune + 磁盘扩容);② ArgoCD application-controller 分片(--shard);③ registry lifecycle 清 untagged 镜像;④ 流水线分 stage 计时持续监控,发现变慢趋势主动治理。

题目2:镜像测试正常生产报错——环境差异排查#

故障现象: 某服务镜像在 QA/PRE 测试通过,部署到 PROD 后启动报错 connection refused,业务无法启动。

排查路径:

text
测试通过生产报错
  ├── 对照组:QA/PRE 和 PROD 的差异是什么?
  │     ├── 配置差异(Nacos namespace 不同)
  │     ├── 环境变量差异(PROD 有额外的 env)
  │     ├── 资源限制差异(PROD resources 更大?)
  │     ├── 网络策略差异(PROD 有 NetworkPolicy?)
  │     └── 镜像差异(是不是真的同一镜像 tag?)
  │
  ├── 镜像一致性验证
  │     ├── 部署的 image tag 是否与 CI 构建的一致?
  │     │     kubectl get deploy <name> -o jsonpath='{.spec.template.spec.containers[0].image}'
  │     ├── image digest 是否一致(不是 tag)?
  │     │     docker pull <image> && docker inspect --format='{{index .RepoDigests 0}}' <image>
  │     └── 不一致 → deploy.py 改 tag 没成功?GitOps overlay 没更新?
  │
  ├── 启动日志分析
  │     ├── kubectl logs <pod> --previous  # 看崩溃前日志
  │     ├── 报错是连什么服务?
  │     └── 连接目标在 PROD 是否可达?
  │           ├── kubectl exec <pod> -- nc -zv <target> <port>
  │           └── 不通 → NetworkPolicy?SG?DNS?
  │
  └── 环境变量/配置对比
        ├── kubectl exec <pod> -- env  # PROD 实际环境变量
        ├── 对比 QA/PRE 的 env
        └── 差异 → Nacos 配置?K8s Secret?ConfigMap?

关键命令:

bash
# 1. 验证镜像 digest 一致
docker inspect --format='{{index .RepoDigests 0}}' <qa-image>
docker inspect --format='{{index .RepoDigests 0}}' <prod-image>
# 2. 看 Pod 启动事件
kubectl describe pod <pod> | grep -A20 Events
# 3. 测试网络连通性
kubectl exec <pod> -- nc -zv <target-svc> <port>
# 4. 对比环境变量
kubectl exec <prod-pod> -- env | sort > /tmp/prod-env.txt
kubectl exec <qa-pod> -- env | sort > /tmp/qa-env.txt
diff /tmp/qa-env.txt /tmp/prod-env.txt

根因分析: 常见原因——① Nacos 配置 namespace 不同,PROD 配置缺少目标服务地址;② NetworkPolicy 阻止了 PROD Pod 访问某些服务;③ K8s Secret 在 PROD namespace 不存在或值不同;④ ESO 同步失败导致 Secret 为空(检查 ExternalSecret status);⑤ ServiceAccount 权限差异(IRSA/Pod Identity 绑定的 role 不同)。

修复方案: ① 配置管理统一化——所有环境用同一套配置模板,差异只在 namespace/region;② GitOps overlay 显式声明所有环境差异;③ 部署前 dry-run——kustomize build overlay/prod | kubectl diff --dry-run=server -f -;④ 启动健康检查——ReadinessProbe 验证关键依赖可达性,失败时不接流量。

题目3:ArgoCD Sync 失败"selector immutable"#

故障现象: PR 隔离 overlay 模板升级后,存量开发者下次推送代码时所有人的 PR 环境全部 SyncFailed,报错 spec.selector is immutable。

排查路径:

text
所有 PR 环境 SyncFailed
  ├── 错误信息:spec.selector is immutable
  │     └── K8s 不允许修改已存在 Deployment 的 selector
  │
  ├── 为什么突然所有 PR 都报错?
  │     ├── 模板升级只对新生成的 overlay 生效
  │     ├── 存量 overlay yaml 仍是旧模型 patch
  │     └── 开发者推送触发 ArgoCD 用新 yaml 尝试 patch 已有 Deployment
  │
  ├── 对照组:新建的 PR 环境正常
  │     └── 确认是"存量 overlay 用旧模板 + 新代码尝试 patch"导致
  │
  └── 根因:deploy.py 模板升级时没重渲染存量 overlay

关键命令:

bash
# 1. 看哪个 PR overlay 失败
argocd app list -l pr-env=true | grep -i degraded
# 2. 看 sync 错误详情
argocd app get <app-name> | grep -A5 "Condition"
# 3. 看 overlay yaml 是新还是旧
cat gitops/clusters/us-qa/applications/*-pr/*/kustomization.yaml | grep "matchLabels"

根因分析: deploy.py 模板升级(如把 op: replace /spec/selector/matchLabels/app 改为新逻辑)只对新生成的 overlay 生效。存量 overlay yaml 仍是旧模型的 patch。开发者推送触发 ArgoCD 用新 yaml 尝试 patch 已有 Deployment,撞上 K8s 的 selector immutable 约束——Deployment 的 selector 一旦创建就不能改,要改只能 delete 重建。

修复方案: 任何会改 PR overlay yaml 结构的变更(selector/labels/annotations/Service patch)上线前必须:① 扫所有存量 overlay——find gitops/clusters/*-pr/ -name kustomization.yaml;② 用新模板重渲染并 commit;③ 批量 kubectl delete deployment 让 ArgoCD 用新 yaml 重建(selector 变化只能重建不能 patch);④ 提前钉钉群通知可能影响到的开发者。

题目4:GitOps 漂移检测——手动改了集群状态没生效#

故障现象: 运维为了应急手动 kubectl scale deploy backend --replicas=5,10 分钟后发现副本数又变回 2。

排查路径:

text
手动改集群状态被回滚
  ├── 是不是 ArgoCD selfHeal 开了?
  │     ├── kubectl get application <app> -o jsonpath='{.spec.syncPolicy.automated.selfHeal}'
  │     └── true → ArgoCD 检测到 live != desired,自动 apply Git 声明拉回
  │
  ├── 是不是 Flux reconcile 触发了?
  │     ├── kubectl get kustomization <name> -o jsonpath='{.spec.interval}'
  │     └── interval 到点全量 apply,手动改的被覆盖
  │
  └── 正确的应急改法
        ├── 临时改:先关 selfHeal(ArgoCD)或 suspend(Flux)
        ├── 永久改:改 GitOps 仓库的 overlay(副本数声明)
        └── 应急后恢复:改完 Git 重新开启 selfHeal

关键命令:

bash
# ArgoCD 查看 selfHeal 状态
kubectl get application <app> -o jsonpath='{.spec.syncPolicy.automated}' | jq
# 临时关闭 selfHeal
kubectl patch application <app> --type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
# Flux suspend
flux suspend kustomization <name>

根因分析: GitOps 的核心就是"Git 是唯一事实源"——selfHeal/reconcile 会持续把集群状态拉回 Git 声明。手动 kubectl 改的状态被视为"漂移"被自动纠正。这是 GitOps 的设计而非 bug。

修复方案: ① 应急场景——先关 selfHeal/suspend,改完集群状态,事后补 Git PR 同步,再恢复 selfHeal;② 永久变更——走 GitOps 流程改 overlay,不要手动 kubectl;③ HPA replicas / Istio sidecar 注入等被 mutate 的字段要配 ignoreDifferences,否则 selfHeal 反复拉回、永久 OutOfSync(哪些字段该忽略、ArgoCD/Flux 怎么配见 1-概念·子类2·题目6)。

题目5:流水线缓存失效导致构建变慢#

故障现象: CI 流水线 Docker build 步骤原本 2 分钟(大部分 layer CACHED),最近变成 8 分钟(所有 layer 重新构建)。

排查路径:

text
Docker build 缓存失效
  ├── 是不是 Dockerfile 改了顺序?
  │     ├── 对比最近 Dockerfile 改动
  │     └── COPY 顺序变了?提前 COPY 了易变文件?
  ├── 是不是 base image 更新了?
  │     ├── FROM xxx:1.2.3 → digest 变了?
  │     └── 用 digest 而非 tag 锁定 base image
  ├── 是不是构建机换了?
  │     ├── runner ID 是否变化?
  │     └── 新构建机没有 cache → 预热或用共享 cache
  ├── 是不是 BuildKit cache mount 失效?
  │     ├── --mount=type=cache,target=... 配置对吗?
  │     └── CN 构建机访问不了 Docker Hub 导致 BuildKit 失败?
  └── 是不是 .dockerignore 改了导致 COPY 上下文变?

关键命令:

bash
# 1. 看 build 日志 CACHED 数量
docker build . 2>&1 | grep -c CACHED
# 2. 对比 Dockerfile 历史
git log --oneline -10 dockerfiles/<service>.Dockerfile
# 3. 看 base image digest
docker inspect <base-image>:<tag> --format='{{index .RepoDigests 0}}'

根因分析: 常见原因——① Dockerfile 里 COPY . . 放在 RUN go build 前面,任何代码改动都导致后续 layer 失效。正确做法是先 COPY go.mod go.sum + RUN go mod download(很少变),再 COPY . . + RUN go build;② base image 用了 :latest tag,每次 pull 可能是不同 digest;③ 构建机池扩展后新机器没 cache;④ CN 构建机禁用 # syntax=docker/dockerfile:1 和 --mount=type=cache(访问不了 Docker Hub)。

修复方案: ① Dockerfile 优化——把不常变的层放前面(依赖安装先于代码拷贝);② base image 用 digest 锁定(FROM ubuntu@sha256:abc123...);③ 构建机用共享 cache(BuildKit cache mount 到 PVC);④ CN 构建机禁用 BuildKit 特有语法,用手动 docker build + --cache-from。


子类 2:可观测性排障#

题目1:Prometheus 抓不到某个 Pod 的指标#

故障现象: 某服务上线后,Prometheus 上看不到它的指标,但服务本身正常提供 HTTP 接口。

排查路径:

text
Prometheus 抓不到指标
  ├── target 是否在 Prometheus 列表里?
  │     ├── curl http://prometheus:9090/api/v1/targets
  │     ├── 不在 → ServiceMonitor 没被选上(label 不匹配)
  │     └── 在但 down → 跳到 ②
  │
  ① ServiceMonitor 不被选
  ├── Prometheus.spec.serviceMonitorSelector 是什么?
  │     └── kubectl get prometheus -o jsonpath='{.items[0].spec.serviceMonitorSelector}'
  ├── ServiceMonitor.metadata.labels 是否匹配 selector?
  │     └── kubectl get servicemonitor <name> -o jsonpath='{.metadata.labels}'
  └── 不匹配 → 改 ServiceMonitor labels 匹配 selector
  │     注意:ruleSelector 也一样,label 不匹配规则不加载(静默失效)
  │
  ② target 在但 down
  ├── target 的 __address__ 是什么?
  │     ├── 端口对吗?Pod 暴露的 /metrics 端口
  │     └── Service selector 匹配 Pod 吗?
  ├── Pod 内 /metrics 可访问吗?
  │     ├── kubectl exec <pod> -- curl localhost:<port>/metrics
  │     └── 不通 → 应用没暴露 /metrics 路径?
  ├── NetworkPolicy 阻止 Prometheus 抓取?
  │     └── 检查 Pod 所在 namespace 的 NetworkPolicy
  └── Pod 重启频繁导致 target 短暂可达?
        └── kubectl get pods -w 看重启次数

关键命令:

bash
# 1. 看 target 状态
curl -s http://prometheus:9090/api/v1/targets | jq '.data.activeTargets[] | select(.labels.job=="<job-name>") | {health, lastError, scrapeUrl}'
# 2. 看 ServiceMonitor 是否被加载
kubectl get servicemonitor <name> -o yaml | grep -A5 labels
# 3. 看 Prometheus ruleSelector
kubectl get prometheus -o jsonpath='{.items[0].spec.ruleSelector}'
# 4. 验证规则真的被加载(不能只看 kubectl get prometheusrule)
curl -s http://prometheus:9090/api/v1/rules | jq '.data.groups[].rules[].name' | grep <rule-name>

根因分析: 最常见的坑是"ServiceMonitor/PrometheusRule 的 metadata.labels 不匹配 Prometheus.spec.selector"——规则在 kubectl get 里能看到,但 Prometheus 根本没加载。某团队 12 条规则因 label 不匹配 75 天没生效,是季度复盘时随手抽查才发现。必须从 /api/v1/rules 接口验证规则真的被加载,不能只看 kubectl get。

修复方案: ① ServiceMonitor/PrometheusRule 的 labels 必须匹配 selector(如 release: monitoring);② 上线告警必须主动制造一次 firing 验证全链路真能推到群;③ 每周巡检脚本对比 git yaml 和 Prometheus API 加载的规则数。

题目2:Alertmanager 告警不触发——规则明明满足条件#

故障现象: Grafana 上看到某指标明显超过阈值,但钉钉群没收到告警。

排查路径:

text
告警不触发
  ├── 规则是否被 Prometheus 加载?
  │     ├── curl http://prometheus:9090/api/v1/rules | jq '.data.groups[].rules[] | select(.name=="<alert-name>")'
  │     ├── 不在 → PrometheusRule label 不匹配 ruleSelector(静默失效)
  │     └── 在 → 继续
  │
  ├── 规则评估状态?
  │     ├── curl 'http://prometheus:9090/api/v1/alerts' | jq '.data.alerts[] | select(.labels.alertname=="<name>")'
  │     ├── state=inactive → 条件没满足(expr 写错?for 还没到?)
  │     ├── state=pending → for 时间还没到
  │     └── state=firing → 已触发,问题在 Alertmanager
  │
  ├── Alertmanager 收到了吗?
  │     ├── curl 'http://alertmanager:9093/api/v2/alerts' | jq '.[] | select(.labels.alertname=="<name>")'
  │     ├── 没收到 → Prometheus → Alertmanager 网络不通?
  │     └── 收到但没发 → 被 silence 了?被 inhibit 了?
  │
  ├── 被 silence 了?
  │     ├── amtool silence query --alertmanager.url=$AM_URL
  │     └── 命中 → 删除 silence 或等过期
  │
  ├── 被 inhibit 了?
  │     ├── 检查 Alertmanager config 的 inhibit_rules
  │     └── 有更高 severity 的告警在 firing 抑制了这条
  │
  └── 发了但钉钉没收到?
        ├── webhook_configs URL 对吗?
        ├── 钉钉关键词白名单拦截?(消息体必须含"告警/运维/恢复")
        ├── resolved 通知被拒?(模板只含"恢复"不含"告警")
        └── webhook-dingtalk 服务挂了?

关键命令:

bash
# 1. 验证规则被加载(不能只看 kubectl get prometheusrule)
curl -s http://prometheus:9090/api/v1/rules | jq '.data.groups[].rules[] | select(.name=="<alert>")'
# 2. 看告警状态
curl -s 'http://prometheus:9090/api/v1/alerts' | jq '.data.alerts[] | select(.labels.alertname=="<alert>")'
# 3. 看 Alertmanager 收到的告警
curl -s 'http://alertmanager:9093/api/v2/alerts?filter=alertname=<alert>' | jq '.[] | {state: .status.state, silencedBy: .status.silencedBy}'
# 4. 查 silence
amtool silence query --alertmanager.url=$AM_URL

根因分析: 常见原因——① PrometheusRule label 不匹配 ruleSelector(75 天没生效案例);② 被 silence 误伤(.+ 全匹配把 Watchdog 也 silence);③ 钉钉关键词白名单拦截(模板渲染的消息体不含关键词);④ webhook-dingtalk 服务挂了(Pod CrashLoop)。最隐蔽的是"resolved 通知被拒"——firing 能发但 resolved 被钉钉关键词拦截(原理详见 3-原理·Alertmanager 路由题)。

修复方案: ① 上线告警主动制造 firing 验证全链路;② 每周巡检 silence/规则加载状态;③ 钉钉关键词同时覆盖 firing + resolved("告警/运维/恢复");④ Watchdog 心跳验证告警链路活着。

题目3:Grafana 看板无数据#

故障现象: 打开 Grafana dashboard,所有 panel 都显示"No data",但 Prometheus 上能查到指标。

排查路径:

text
Grafana 无数据
  ├── 数据源配置对吗?
  │     ├── datasource URL 可达?
  │     ├── datasource 是正确的 Prometheus 实例?
  │     └── 测试连接 "Save & Test"
  │
  ├── 时间范围对吗?
  │     ├── 时间范围太短(如 last 5 min 但指标 1h 才更新)
  │     ├── 时间范围太长(查 30 天但 Prometheus 只存 15 天)
  │     └── 时区问题(UTC vs Local)
  │
  ├── PromQL 写对了吗?
  │     ├── 在 Prometheus UI 直接跑 PromQL 验证
  │     ├── label 匹配吗?(service="backend" vs app="backend")
  │     └── 函数用对吗?(rate vs increase vs sum)
  │
  └── 指标存在吗?
        ├── curl http://prometheus:9090/api/v1/label/__name__/values | grep <metric>
        └── 不存在 → 应用没暴露这个指标?exporter 没装?

关键命令:

bash
# 1. 测试数据源连接
curl -s http://grafana:3000/api/datasources/<id>/resources/api/v1/query --data-urlencode 'query=up' | jq
# 2. 在 Prometheus 验证 PromQL
curl -sG http://prometheus:9090/api/v1/query --data-urlencode 'query=<your-promql>' | jq
# 3. 看指标是否存在
curl -s http://prometheus:9090/api/v1/label/__name__/values | jq '.data[]' | grep -i <keyword>

根因分析: 常见原因——① 数据源选错实例(有多个 Prometheus);② 时间范围超过 retention(Prometheus 只存 15 天,查 30 天无数据);③ label 名称不匹配(dashboard 用 app="backend",实际指标是 service="backend");④ 指标重命名了但 dashboard 没更新;⑤ Prometheus 没抓到这个指标(ServiceMonitor label 不匹配)。

题目4:Loki 查询超慢#

故障现象: Loki 查询某服务最近 1 小时的日志,等了 30 秒还没返回。

排查路径:

text
Loki 查询慢
  ├── 查询范围太大?
  │     ├── 时间范围 1h 还是 24h?
  │     ├── label 匹配吗?({app="backend"} vs {service="backend"})
  │     └── 没有 label 过滤导致全表扫描?
  │
  ├── stream 数太多?
  │     ├── label 基数高?(user_id 当 label 导致 stream 爆炸)
  │     └── 高基数 → 重新设计 label
  │
  ├── Loki 资源不足?
  │     ├── kubectl top pod -n monitoring | grep loki
  │     ├── ingester 内存满?
  │     └── querier CPU 高?
  │
  ├── 存储后端慢?
  │     ├── S3/OSS 延迟高?
  │     ├── filesystem 后端 PVC 满?
  │     └── 换对象存储(S3/OSS)避免 PVC 满
  │
  └── 查询语句优化
        ├── 先按 label + 时间范围缩小,再用 |= 过滤内容
        ├── 避免 {namespace=~".+"} 这种宽匹配
        └── 用 count_over_time 看量级再决定是否全量查

关键命令:

bash
# 1. 看 Loki 资源
kubectl top pod -n monitoring -l app.kubernetes.io/name=loki
# 2. 看 PVC 使用(filesystem 后端)
kubectl get pvc -n monitoring
# 3. 看 stream 数(label 基数)
curl -sG http://loki:3100/loki/api/v1/label/__name__/values | jq '.data | length'
# 4. 优化查询(先看量级)
logcli query --limit=100 --since=1h 'count_over_time({app="backend"}[1m])'

根因分析: 常见原因——① label 基数高导致 stream 爆炸(把 user_id/request_id 当 label);② 时间范围太大(查 24h 但没有 label 过滤);③ filesystem 后端 PVC 满(filesystem PVC 会满是高频事故,详见 3-原理·Loki 写入路径题);④ ingester 资源不足。

修复方案: ① 严格控制 label 基数(只用 app/namespace/env 等低基数 label);② 生产用对象存储(S3/OSS)而非 filesystem;③ PVC 配 KubePersistentVolumeFillingUp 告警;④ retention 配置自动清理(默认永久保留会撑爆)。

题目5:Trace 断裂——TraceID 不连续跨服务#

故障现象: 在 Tempo/Jaeger 查某次请求的 Trace,发现只看到入口服务的 span,下游服务的 span 没有出现。

排查路径:

text
Trace 断裂
  ├── TraceID 是否传递?
  │     ├── 入口服务收到请求时生成 TraceID
  │     ├── 调下游服务时 TraceID 是否在 header 里?
  │     │     ├── W3C traceparent header
  │     │     ├── B3 header(Zipkin 兼容)
  │     │     └── OTel propagator 配置对吗?
  │     └── 下游服务是否解析了 TraceID?
  │
  ├── OTel SDK 配置对吗?
  │     ├── 自动注入(HTTP/gRPC middleware)配对了吗?
  │     ├── otelhttp.NewTransport 包装 http.Client 了吗?
  │     └── 漏 wrap → baggage/trace 不会自动注入 header
  │
  ├── 跨协议传递?
  │     ├── HTTP → HTTP:OTel propagator 自动
  │     ├── HTTP → Kafka:要显式 inject/extract
  │     ├── HTTP → Redis:用 key 前缀
  │     └── 跨进程边界要显式处理
  │
  └── 采样导致丢失?
        ├── 入口采样了但下游没采样?
        ├── tail_based_sampling 配置一致吗?
        └── 用概率采样要所有服务一致

关键命令:

bash
# 1. 看请求 header 是否有 traceparent
kubectl exec <pod> -- curl -sv http://downstream-svc/api 2>&1 | grep -i traceparent
# 2. 看 OTel collector 是否收到所有 span
curl -s http://otel-collector:8888/metrics | grep otelcol_receiver_accepted_spans
# 3. 看应用日志的 trace_id 字段
kubectl logs <pod> | jq 'select(.trace_id != null) | .trace_id' | sort -u

根因分析: 常见原因——① HTTP client 没 wrap otelhttp(某团队 PR 隔离 v2 通宵部署发现 backend 出站 HTTP client 全是裸的,baggage 不会自动注入);② 跨 Kafka 没显式 inject/extract;③ 跨 Redis 没用 key 前缀传递;④ 采样配置不一致(入口采样但下游不采样)。baggage 跨 hop 透传必须应用层注入——sidecar 不自动透传,app 调下游要 OTel propagator 注入。

修复方案: ① 所有 HTTP client 用 otelhttp.NewTransport(http.DefaultTransport) 包装;② 跨 Kafka 用 OTel inject/extract API;③ 跨 Redis 用 key 前缀(pr:{env_id}:cache:xxx);④ 采样策略所有服务一致(或用 tail_based_sampling 在 collector 层统一)。


子类 3:IaC 排障#

题目1:Terraform State 冲突——多人同时 apply#

故障现象: 两个工程师同时 terraform apply,A 先完成,B 报错 state is locked。

排查路径:

text
State 冲突
  ├── 是不是 DynamoDB lock 没配?
  │     ├── backend "s3" 配置里有 dynamodb_table 吗?
  │     └── 没配 → 加上:dynamodb_table = "tf-locks"
  │
  ├── lock 被 A 持有但 A 已离开?
  │     ├── aws dynamodb get-item --table-name tf-locks --key '{"LockID":{"S":"<lock-id>"}}'
  │     └── A 的 apply 已完成但 lock 没清 → 强制解锁
  │
  ├── 强制解锁
  │     ├── terraform force-unlock <lock-id>
  │     └── 注意:确认 A 真的没在 apply,否则 state 损坏
  │
  └── 根因:state lock 机制没正确配置或被异常中断

关键命令:

bash
# 1. 看 DynamoDB lock
aws dynamodb get-item --table-name tf-locks --key '{"LockID":{"S":"<bucket>/<key>.tfstate"}}'
# 2. 强制解锁
terraform force-unlock <lock-id>
# 3. 验证 state 完整性
terraform plan  # 不应该有意外 diff

根因分析: ① DynamoDB lock 没配——backend "s3" 缺 dynamodb_table 参数;② apply 中途被 Ctrl+C 中断,lock 残留;③ lock 表被误删。

修复方案: ① 必须配 DynamoDB lock(或用 HCP Terraform 托管);② apply 中断后用 terraform force-unlock 清理;③ 团队协作约定——谁 apply 谁 负责到底,不要同时 apply;④ State 远端存储 + 加锁 + 加密 + 备份(S3 versioning)。

题目2:Ansible playbook 不幂等——重复跑报错#

故障现象: Ansible playbook 第一次跑成功,第二次跑报错 "file already exists" 或 "user already exists"。

排查路径:

text
不幂等
  ├── 哪个 task 报错?
  │     ├── ansible-playbook 输出会标明 task name
  │
  ├── file task 用 state=touch?
  │     ├── state=touch 每次创建新文件 → 改 state=file + 用 template
  │
  ├── user task 没用 state 参数?
  │     ├── user: name=alice state=present → 幂等
  │     └── 不加 state 默认 present,但加 create_home=yes 可能不幂等
  │
  ├── shell task 用了非幂等命令?
  │     ├── shell: mkdir /tmp/foo → 改为 file: path=/tmp/foo state=directory
  │     ├── shell: echo "..." >> /etc/hosts → 用 lineinfile 或 blockinfile
  │     └── shell: curl ... | bash → 加 creates= 参数或 changed_when
  │
  └── command task 本身不幂等?
        ├── command: apt install nginx → 改用 apt module(幂等)
        └── command: systemctl start nginx → 改用 service module(幂等)

关键命令:

bash
# 1. 看 playbook 哪个 task 不幂等
ansible-playbook -i inventory playbook.yml --check  # dry run
# 2. 看 task 输出
ansible-playbook -i inventory playbook.yml -v

根因分析: Ansible 的大部分 module 内置幂等(apt/service/file/user),但 shell/command module 不幂等——除非显式加 creates=/changed_when=。常见坑:① 用 shell 跑 mkdir 而不是用 file module;② 用 shell 跑 echo >> 追加文件而不是用 lineinfile;③ 用 command 跑 apt install 而不是用 apt module。

修复方案: ① 优先用 Ansible module(file/user/apt/service)而非 shell/command;② 必须用 shell 时加 creates=(文件存在则跳过)或 changed_when: false(标记不变更);③ 跑 --check dry run 验证幂等性;④ CI 里重复跑 playbook 验证幂等。

题目3:Terraform plan 报权限错#

故障现象: terraform plan 报 AccessDenied 或 Unauthorized 错误。

排查路径:

text
权限错
  ├── 是 AWS 凭据问题还是资源策略问题?
  │     ├── aws sts get-caller-identity → 当前身份
  │     ├── 凭据是否过期?aws configure list
  │     └── MFA 要求?AWS SSO 登录?
  │
  ├── 是哪个资源的权限不够?
  │     ├── plan 输出会标明 resource 类型
  │     ├── 如 aws_vpc → ec2:CreateVpc 权限
  │     └── 如 aws_s3_bucket → s3:CreateBucket 权限
  │
  ├── IRSA/Pod Identity 配置对吗?
  │     ├── ServiceAccount 注解 eks.amazonaws.com/role-arn
  │     ├── trust policy 绑定 ServiceAccount 名吗?
  │     └── Pod 内 aws sts get-caller-identity 验证
  │
  └── 跨账号访问?
        ├── 跨账号 role assume 配置对吗?
        └── target 账号的 trust policy 包含 source 账号?

关键命令:

bash
# 1. 看当前身份
aws sts get-caller-identity
# 2. 看 IRSA 注解
kubectl get sa <sa-name> -o jsonpath='{.metadata.annotations.eks\.amazonaws\.com/role-arn}'
# 3. 在 Pod 内验证
kubectl exec <pod> -- aws sts get-caller-identity
# 4. 用 IAM Policy Simulator 验证权限
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names ec2:CreateVpc

根因分析: ① AWS 凭据过期或 MFA 没输入;② IAM Role 缺少具体资源权限(如 ec2:CreateVpc);③ IRSA trust policy 没绑定 ServiceAccount 名;④ 跨账号 role assume 的 trust policy 配置错。

修复方案: ① 用 IAM Policy Simulator 验证具体权限;② IRSA trust policy 必须包含 oidc.eks.<region>.amazonaws.com/id/<cluster-id>:aud: sts.amazonaws.com 和 oidc.eks...:sub: system:serviceaccount:<namespace>:<sa-name>;③ 跨账号访问配 AssumeRole;④ 最小权限——给 Role attach 具体 policy(如 AmazonEC2FullAccess),不要 AdministratorAccess。


子类 4:安全排障#

题目1:Vault token 过期——应用连不上数据库#

故障现象: 应用报错 permission denied: token expired,无法连接数据库。

排查路径:

text
Vault token 过期
  ├── 是 ESO 的 token 还是应用直接用的 token?
  │     ├── ESO 用 Kubernetes Auth 换 token(不应该过期)
  │     └── 应用直接用静态 token → 改用 ESO 动态获取
  │
  ├── ESO 同步失败?
  │     ├── kubectl describe externalsecret <name>
  │     ├── 看事件 "reason=SecretSyncedError"
  │     └── 常见原因:Vault 不可达/K8s Auth 配置错/Policy 没权限
  │
  ├── Vault Pod 是否健康?
  │     ├── kubectl get pod -n vault
  │     ├── vault status → 是否 sealed?
  │     └── sealed → Auto-unseal 没配?手动 unseal
  │
  ├── K8s Auth 配置对吗?
  │     ├── Vault 的 auth/kubernetes/config 配置
  │     ├── bound_service_account_names 匹配吗?
  │     └── token_reviewer_jwt 有效吗?
  │
  └── Policy 有 read 权限吗?
        ├── vault policy read <policy-name>
        ├── path "secret/data/myapp/prod/*" { capabilities = ["read"] }
        └── KV v2 路径要用 /data/ 前缀

关键命令:

bash
# 1. 看 ExternalSecret 状态
kubectl describe externalsecret <name> -n <namespace>
# 2. 看 Vault 状态
kubectl exec vault-0 -n vault -- vault status
# 3. 测试 K8s Auth
kubectl exec vault-0 -n vault -- vault write auth/kubernetes/login role=<role> jwt=$(kubectl get secret <sa-token> -o jsonpath='{.data.token}' | base64 -d)
# 4. 看 ESO 日志
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets --tail=50

根因分析: ① Vault Pod 重启后进入 sealed 状态(没配 Auto-unseal);② K8s Auth 的 token_reviewer_jwt 过期(K8s 1.24+ ServiceAccount Token 不再自动创建 Secret);③ Policy 路径写错(KV v2 CLI/API 路径差异见 3-原理·子类4·题目2);④ ESO 的 SecretStore 配置的 serviceAccountRef 指向错误的 SA。

修复方案: ① Vault 生产必须配 Auto-unseal(AWS KMS/阿里云 KMS),否则 Pod 重启 sealed 要手动 unseal(HA 部署与 unseal 配置详见 5-方案·子类3·题目2);② K8s 1.24+ 要手动创建 ServiceAccount Token Secret 或用 TokenRequest API;③ Policy 路径严格按 KV v2 规范(secret/data/myapp/prod/*);④ ESO 配置 serviceAccountRef 指向正确的 SA。

题目2:Pod 被 SecurityContext 阻止启动#

故障现象: Pod 创建失败,事件报 PodSecurityViolation 或 container has runAsNonRoot but image will run as root。

排查路径:

text
Pod 被阻止
  ├── 是 PSA 还是 Kyverno/OPA 拦截?
  │     ├── kubectl describe pod <pod> | grep -A10 Events
  │     ├── PSA 拦截 → "violates PodSecurity"
  │     └── Kyverno 拦截 → "admission webhook denied"
  │
  ├── PSA restricted 要求什么?
  │     ├── runAsNonRoot: true
  │     ├── runAsUser: 非 0
  │     ├── allowPrivilegeEscalation: false
  │     ├── readOnlyRootFilesystem: true
  │     ├── capabilities.drop: [ALL]
  │     └── seccompProfile: RuntimeDefault
  │
  ├── 镜像默认用什么用户?
  │     ├── docker inspect <image> --format '{{.Config.User}}'
  │     ├── 空 → 默认 root → 与 runAsNonRoot 冲突
  │     └── 改 Dockerfile 加 USER 10001:10001
  │
  └── 应用需要写文件吗?
        ├── readOnlyRootFilesystem: true 但应用要写 /tmp
        → 挂 emptyDir volume 到 /tmp

关键命令:

bash
# 1. 看 Pod 事件
kubectl describe pod <pod> | grep -A20 Events
# 2. 看镜像默认用户
docker inspect <image> --format '{{.Config.User}}'
# 3. 看 namespace PSA 配置
kubectl get ns <namespace> -o jsonpath='{.metadata.labels}' | jq
# 4. 看 Kyverno policy
kubectl get clusterpolicy -o yaml | grep -A10 "verify-images\|pod-security"

根因分析: ① 镜像没指定 USER,默认 root,与 PSA restricted 的 runAsNonRoot: true 冲突;② 应用要写 /tmp 但 readOnlyRootFilesystem: true;③ 基础镜像(如 nginx)需要 privileged 才能绑定 80 端口。

修复方案: ① Dockerfile 加 USER 10001:10001;② 挂 emptyDir 到可写目录(/tmp、/var/log);③ 需要特权的能力用 capabilities.add(如 NET_BIND_SERVICE 绑 80 端口);④ 不要用 privileged: true,用具体 capabilities;⑤ 基础镜像用 distroless(默认非 root)。

题目3:NetworkPolicy 误拦截业务流量#

故障现象: 部署 NetworkPolicy 后,某服务 A 调服务 B 超时。

排查路径:

text
NetworkPolicy 误拦截
  ├── 是哪个方向被拦?
  │     ├── A → B 不通:B 的 ingress policy 没放行 A
  │     ├── A 出站不通:A 的 egress policy 没放行 B
  │     └── DNS 不通:egress 没放行 kube-dns
  │
  ├── 默认 deny-all 配了吗?
  │     ├── kubectl get networkpolicy -n <namespace>
  │     └── default-deny-all 会拦所有流量,需要显式放行
  │
  ├── podSelector 匹配吗?
  │     ├── NetworkPolicy 的 spec.podSelector 选中的 Pod
  │     └── ingress.from.podSelector 匹配源 Pod 吗?
  │
  ├── namespaceSelector 匹配吗?
  │     ├── 跨 namespace 流量要配 namespaceSelector
  │     └── namespace label 对吗?(kubernetes.io/metadata.name)
  │
  └── 端口对吗?
        ├── ingress.ports.port = 8080(不是 80)
        └── protocol = TCP(不是 UDP)

关键命令:

bash
# 1. 看 NetworkPolicy
kubectl get networkpolicy -n <namespace> -o yaml
# 2. 测试连通性
kubectl exec <a-pod> -- nc -zv <b-svc> <port>
# 3. 看 Pod label
kubectl get pods -n <namespace> --show-labels
# 4. 看 namespace label
kubectl get ns --show-labels

根因分析: ① default-deny-all 部署后没配放行规则;② namespaceSelector 用错 label(应该是 kubernetes.io/metadata.name 不是自定义 label);③ 跨 namespace 流量既要 podSelector 又要 namespaceSelector(AND 关系);④ DNS(kube-dns)没放行导致解析失败。

修复方案: ① 分阶段部署——先 audit 模式(记录不拦截)观察一段时间再切 enforce;② 每个 namespace 配 allow-internal-and-egress policy 放行必要流量;③ DNS 必须放行(egress 到 kube-dns 的 53 端口);④ 用 Hubble/Cilium 查网络流量可视化排查。

题目4:Cosign 验证失败——镜像拉不下来#

故障现象: Pod 创建失败,报错 image policy violation: missing signature 或 signature verification failed。

排查路径:

text
Cosign 验证失败
  ├── 镜像有签名吗?
  │     ├── cosign verify --key <pub> <image>
  │     ├── 没签名 → CI 没跑签名步骤
  │     └── 有签名但验证失败 → 跳到 ②
  │
  ├── 公钥对吗?
  │     ├── Kyverno policy 里的 publicKeys 是正确的公钥?
  │     └── 密钥轮换了但 policy 没更新?
  │
  ├── Keyless 签名的 identity 对吗?
  │     ├── certificate-identity-regexp 匹配签名的 identity?
  │     ├── certificate-oidc-issuer 是正确的 issuer?
  │     └── 如 GitHub Actions: issuer=https://token.actions.githubusercontent.com
  │
  ├── Rekor 可达吗?
  │     ├── Keyless 验证要查 Rekor 透明日志
  │     ├── 网络不通 → 自部署 Rekor 或用传统密钥签名
  │     └── 公共 Rekor 服务可能被墙
  │
  └── Kyverno policy 配置对吗?
        ├── verifyImages.imageReferences 匹配镜像名?
        ├── attestors.count 配置对吗?
        └── validationFailureAction: Enforce 还是 Audit?

关键命令:

bash
# 1. 验证镜像签名
cosign verify --key cosign.pub <image>
# 2. Keyless 验证
cosign verify --certificate-identity-regexp "example-org/myapp" --certificate-oidc-issuer "https://token.actions.githubusercontent.com" <image>
# 3. 看 Kyverno policy
kubectl get clusterpolicy verify-image-signature -o yaml
# 4. 看 Pod 事件
kubectl describe pod <pod> | grep -A10 Events

根因分析: ① CI 没跑签名步骤(cosign sign 命令缺失或失败);② Kyverno policy 的公钥/identity 配置错;③ Keyless 签名的 Rekor 服务不可达;④ 镜像 tag 被覆盖(同一 tag 不同 digest,签名验证的是旧 digest)。

修复方案: ① CI 流水线必须串 cosign sign 步骤;② Kyverno policy 先用 Audit 模式观察一段时间再切 Enforce;③ Keyless 验证依赖 Rekor,网络不通时用传统密钥签名;④ 镜像 tag 用 {commit}-{timestamp} 不可变,避免 tag 覆盖导致签名验证错。


子类 5:综合排障(真实案例改编)#

题目1:CNI 扩不动——新节点全 NotReady(自锁死循环)#

故障现象: EKS 集群扩容,新节点起来全是 NotReady,aws-node DaemonSet 报 MissingIAMPermissions: ec2:DescribeNetworkInterfaces。

排查路径:

text
新节点 NotReady
  ├── CNI Pod 状态?
  │     ├── kubectl -n kube-system get pod -l k8s-app=aws-node
  │     ├── CrashLoopBackOff → 看 logs
  │     └── 日志报 MissingIAMPermissions
  │
  ├── 第一直觉:子网 IP 耗尽?
  │     ├── aws ec2 describe-subnets --query 'Subnets[0].AvailableIpAddressCount'
  │     └── 3914 个 IP → 证伪,不是 IP 问题
  │
  ├── 第二直觉:有人删了 CNI policy?
  │     ├── aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DetachRolePolicy
  │     └── 0 条 → 证伪,没人删过
  │
  ├── 顺藤摸瓜:老节点凭什么正常?
  │     ├── 新节点 CNI 走什么权限路径?
  │     ├── kubectl -n kube-system get pod aws-node-xxx -o jsonpath='{.spec.containers[*].env}' | grep AWS_ROLE_ARN
  │     └── 发现 IRSA role(addon-vpc-cni-Role1)
  │
  ├── 真相:IRSA 失效 fallback 到 node role
  │     ├── node role 没有 CNI policy(建 nodegroup 时漏配)
  │     ├── 老节点走 IRSA 正常
  │     ├── 新节点在 addon UPDATING 中间态 IRSA 短暂失效
  │     └── fallback 到没权限的 node role → CNI 失败 → 节点 NotReady
  │
  └── 自锁死循环
        ├── addon UPDATING → 新节点 fallback 失败
        ├── CNI 不健康 → addon rollout 完不成
        └── 永远停在 UPDATING

关键命令:

bash
# 1. 看 CNI Pod 日志
kubectl -n kube-system logs aws-node-xxxx
# 2. 看 IRSA 配置
kubectl -n kube-system get pod aws-node-xxxx -o jsonpath='{.spec.containers[*].env}' | grep AWS_ROLE_ARN
# 3. CloudTrail 溯源
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateAddon
# 4. 看 node role policies
aws iam list-attached-role-policies --role-name <node-role-name>

根因分析: eksctl 纯 IRSA 模式建的 node role 不带 CNI policy 兜底——平时 IRSA 正常时毫无影响,addon 更新或 IRSA 边缘失效时新节点全挂。AWS 官方推荐 belt-and-suspenders:IRSA 是主,node role 的 CNI policy 是底,两个都配。

修复方案: ① 止血——给 node role attach AmazonEKS_CNI_Policy + 删 CrashLoop pod 重建;② 根治——所有 EKS node role 普配 CNI policy 兜底,拆掉同类定时炸弹;③ 双层归因——触发因素是 addon 更新中间态(偶发),结构性缺陷是没有兜底(该根治)。

可迁移决策树骨架: 组件依赖云 IAM/权限时,永远追问「主授权路径失效会 fallback 到什么、fallback 路径有没有兜底权限」——IRSA/Pod Identity 短暂失效 fallback 到 node role 是通用陷阱,换任何云的托管插件(CNI/CSI/LB controller)都适用;且遵循「发现一个缺陷立即当一类,排查所有同款 role」。

题目2:file 发版超时——内存型 HPA 把副本虚扩到 13#

故障现象: cn-prod 的 file 服务每次发版流水线必报错 部署超时 (900s),但 us-prod 同一镜像发版完全正常。

排查路径:

text
发版超时
  ├── 对照组:us-prod 正常 vs cn-prod 超时
  │     ├── 同镜像同脚本 → 问题在 cn 集群部署环境
  │     └── 不是镜像/脚本问题
  │
  ├── wait 超时的本质
  │     ├── gitops 改 tag ✅
  │     ├── ArgoCD Synced ✅
  │     ├── 但 Health Degraded(等待新 RS)
  │     └── 新 ReplicaSet 的 pod 起不来
  │
  ├── Pod 为什么起不来?
  │     ├── kubectl describe pod <new-pod> | grep -A3 Events
  │     ├── FailedScheduling: didn't trigger scale-up due to missing matching nodepool
  │     └── 专用节点池满了
  │
  ├── 为什么节点池满?
  │     ├── kubectl top pod → file 每 pod CPU ~6-7m(≈0%),但常驻内存 1.4-2.5Gi
  │     ├── hpa.yaml 同时挂 cpu(70%)+memory(80%)
  │     ├── 文件代理常驻内存天然卡 84-92% > 80% 阈值
  │     ├── HPA 误把"内存高"读成"要扩容" → 副本 min4 一路顶到 13
  │     └── 13 个 pod × ~2Gi ≈ 24Gi 内存,伺候 0.09 核的活——越扩越多
  │
  └── 双根因叠加
        ├── 根因①:内存型 HPA 把副本虚扩到 13
        ├── 根因②:maxSurge 撞满专用节点池(13 副本 + surge 第 14 个 Pending)
        └── 单修一个不彻底,必须两个都修

关键命令:

bash
# 1. 看 Pod 调度失败
kubectl --context cn-prod -n goalfymax describe pod <new-file-pod> | grep -A5 Events
# 2. 看资源使用
kubectl top pod -n goalfymax -l app=file
# 3. 看 HPA 状态
kubectl get hpa -n goalfymax file
# 4. 看节点池容量
kubectl get nodes -l node-type=file -o wide

根因分析: 常驻内存型服务(Node 堆、文件代理)禁用 memory-Utilization HPA——加副本不降单 pod 内存,只会越扩越多、与流量脱钩。maxSurge=1/maxUnavailable=0(先建后删)在专用节点池满时 surge 的 pod 没空位 → Pending → wait 超时。

修复方案: ① hpa.yaml 删掉 memory 指标,只留 CPU → 副本自动 13→4;② deployment-patch.yaml 加 maxSurge=0/maxUnavailable=1(先删后建,现有容量原地滚);③ 排查所有服务的 HPA 有没有同款误配——这是一类问题不是单点。

可迁移决策树骨架: "越扩越多/缩不动"类问题先验证「扩容指标与真实瓶颈是否同向」——常驻内存型服务的内存、长连接型的连接数、队列积压型的 CPU,都可能是"加副本不缓解反而恶化"的伪指标。扩容前先问一句:多一个副本能不能降低这个指标?不能就说明 HPA 指标选错了。

题目3:passport 全挂——system-pool 节点轮换引爆潜伏炸弹#

故障现象: passport.goalfyai.com(SSO 登录)突然全挂,TLS ClientHello 发出去后收不到 ServerHello,12 秒超时。但 goalfymax.goalfyai.com 完全正常。

排查路径:

text
passport 全挂
  ├── 对照组:passport 挂 vs goalfymax 正常
  │     ├── passport 直连 NLB
  │     ├── goalfymax 走 CloudFront
  │     └── 问题在 passport → NLB 链路
  │
  ├── NLB target 健康?
  │     ├── aws elbv2 describe-target-health --target-group-arn <tg>
  │     ├── target 为空 / 全 unhealthy
  │     └── NLB 没有健康后端
  │
  ├── ingress-gateway Pod 状态?
  │     ├── kubectl -n istio-system get pod -l istio=ingressgateway
  │     ├── ingress-gateway-xxx 0/1 Pending
  │     └── Pod 起不来
  │
  ├── Pod 为什么 Pending?
  │     ├── kubectl describe pod <ingress-gateway> | grep -A5 Events
  │     ├── FailedScheduling: node(s) had untolerated taint {system-pool=true: NoSchedule}
  │     ├── NotTriggerScaleUp(autoscaler 救不了)
  │     └── 绑 nodeSelector=system-pool 但没配 toleration
  │
  ├── 真相:节点轮换引爆潜伏炸弹
  │     ├── system-pool nodegroup 节点带 taint: system-pool=true:NoSchedule
  │     ├── ingress-gateway 绑 nodeSelector=system-pool 但没配 toleration
  │     ├── NoSchedule 只拦"新调度"不驱逐"已运行" → 平时好好的(炸弹被掩盖)
  │     ├── 04:25 system-pool 节点轮换(新节点 ip-10-3-32-204)
  │     ├── gateway 单副本 pod 重建 → 只准上 system-pool 又不容忍它的 taint
  │     └── 永久 Pending → service endpoint 空 → NLB 无健康 target → passport 全挂
  │
  └── 元教训:发现一个缺陷要排查所有同款
        ├── 之前修 cert-manager 时已知道 system-pool 缺 toleration
        ├── 但只修了 cert-manager,没预警同款的 istio gateway
        ├── 同一个缺陷、同一个集群、同一类组件,修了一个漏了另一个 → P0
        └── "一个缺陷往往是一类"——必须当集群级缺陷排查所有同 nodeSelector 的组件

关键命令:

bash
# 1. 看 NLB target 健康
aws elbv2 describe-target-health --target-group-arn <tg>
# 2. 看 ingress-gateway Pod
kubectl -n istio-system get pod -l istio=ingressgateway -o wide
kubectl -n istio-system describe pod <ingress-gateway> | grep -A10 Events
# 3. 看节点 taint
kubectl get nodes -l node-role=system-pool -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

根因分析: NoSchedule 只拦"新调度"、不驱逐"已运行",所以缺 toleration 这个缺陷在节点不动时完全无感,能潜伏几个月——直到某次节点轮换/扩容/pod 重建,pod 要重新调度,炸弹瞬间引爆。"平时好好的"恰恰是这类炸弹最危险的地方。

修复方案: ① 止血——手动摘掉节点 taint,gateway pod 立刻能调度恢复;② 治本——在 istiod chart 的 gateway 注入模板里加 system-pool toleration;③ 排查所有同款——所有绑 nodeSelector=system-pool 的组件都要检查是否有对应 toleration(cert-manager 已修,gateway 漏修是教训);④ 监控——给关键 ingress-gateway pod 配 Pending 告警。

可迁移决策树骨架: "平时好好的突然全挂"优先查「最近有没有节点轮换/重建/扩容/pod 重调度」——凡是"只拦新调度、不驱逐已运行"的约束(taint/nodeSelector/亲和性/资源配额),都会潜伏到下次重调度才引爆,单副本关键组件(网关/证书签发)尤其危险。发现一个缺陷立即当集群级缺陷排查所有同 nodeSelector 组件。

题目4:子环境 ID 撞车污染 10 万条数据#

故障现象: 新建的 env-AI 环境上线 17 天后,env-QA 的一个用户反馈"我在自己 9 月创建的老项目里看到了不认识的对话流"。

排查路径:

text
跨环境数据污染
  ├── 污染规模量化
  │     ├── 扫 realtime_messages 表,找 created_at > 'env-AI 上线日' 的脏数据
  │     ├── 用 CTE 找跨库 id 撞车:project_id BETWEEN 1 AND 274 + created_at >= 上线日 + 老项目
  │     └── 共 95,430 行脏数据(4 张表)
  │
  ├── 4 件套根因同时成立
  │     ├── ① 跨环境业务表 id 区间重叠
  │     │     ├── env-AI project.id 从 1 自增,最大才 274
  │     │     └── env-QA 老项目大量分布在 1-274 区间
  │     ├── ② 广播总线(RabbitMQ)共用
  │     │     ├── 同 broker 同 vhost 同 fanout exchange
  │     │     └── env-QA dispatcher 也订阅了这个 exchange
  │     ├── ③ 消费者侧没有按环境标签强过滤
  │     │     ├── dispatcher 按 project_id 数字直接写本地库
  │     │     └── 未校验 dispatch_env
  │     └── ④ 配置中心复制粘贴漏改
  │           ├── Nacos 配置复制后只改 env 标签
  │           └── host/vhost/consumer_group 全保留旧值
  │
  └── 4 件套缺一不会爆雷
        ├── 独立 id 区间 → 即使消息串了也写不到老项目
        ├── 独立 broker → 消息根本不会跨环境
        ├── 严格 env 过滤 → 串了的消息被丢弃
        └── 配置正确 → 不会共用 broker

根因分析: 新子环境上线时"复用现成环境的中间件"是最快的姿势,但 Aurora/RabbitMQ/Kafka/Valkey 五件套任一共用 + id 区间重叠 + 过滤缺失 + 配置漏改 = 跨环境数据污染。每个独立缺陷都不致命,叠加起来就是事故。

修复方案: ① 止血——起独立 Valkey + 独立 RabbitMQ broker + 改 Nacos 6 个服务配置 + Kafka consumer group 切换;② 数据清洗——CTAS 备份 + 临时表中转 + 分批 DELETE 1000 行/批(避免锁表 + binlog 巨大事务);③ 根治——业务表 AUTO_INCREMENT 起点设到 1000 万(留充足缓冲带防未来撞车)+ dispatch_env 字段强制 + 应用层 middleware 注入;④ 防回退——IaC pre-commit 检查 broker/cluster 不共用 + 每日自动巡检脚本扫跨环境 id 区间重叠。

题目5:PR 隔离 v2 通宵部署——6 个 hotfix 的踩坑大全#

故障现象: PR 隔离从 v1(X-env + 克隆派)升级到 v2(baggage + 路由派),通宵 8.5 小时部署,24 MR 合 main + 6 hotfix。

排查路径(6 个典型 hotfix):

text
v2 通宵部署踩坑
  ├── ① commit msg vs diff 漂移
  │     ├── commit msg 写"删了 router.go 4 行"
  │     ├── git diff-tree -r <sha> 只看到删 debug_baggage.go
  │     ├── 合 main 后流水线编译 fail undefined: handler.EchoBaggage
  │     └── 教训:删除/重构 commit 写 msg 前先 git diff --cached
  │
  ├── ② OTel 改造遗漏 HTTP client wrap
  │     ├── hub-backend W1 改 22 文件 29 处 http.Client → otelhttp wrap
  │     ├── backend W1 只改 1 文件 18 行
  │     ├── backend 出站 http.Client 全是裸的 → baggage 不会自动注入
  │     └── v2 路由派对 backend 这一环完全断
  │
  ├── ③ baseline Service selector "version=baseline" vs DR subset 矛盾
  │     ├── v1 时代踩过坑:base svc selector 反向选中 PR Pod
  │     ├── v2 路由派要求 PR Pod 保留 app=base 才能被 baseline DR/VS subset 识别
  │     ├── 凌晨先加 selector: version=baseline 防反选 → 破 DR subset 路由派
  │     └── 后撤回:mesh 启用后流量经 sidecar VS 不走 kube-proxy,反选不发生
  │
  ├── ④ pre-mesh excludeOutboundPorts annotation 跟 sidecar iptables setup 竞争
  │     ├── backend base deployment 含 excludeOutboundPorts: "3306,6379,6380"
  │     ├── pre-mesh 时设的让 sidecar 不捕获该端口
  │     ├── mesh 启用后 PR Pod 启动时 sidecar iptables setup 跟业务 connect MySQL 竞争
  │     └── DB init 卡死 → CrashLoopBackOff
  │
  ├── ⑤ HTTPRoute X-env match vs Istio VS baggage match 双轨
  │     ├── ingress-gateway 用 K8s Gateway API HTTPRoute(X-env header match)
  │     ├── Istio VS 只控制东西流量(pod-to-pod 经 sidecar,baggage match)
  │     ├── 浏览器 X-env → HTTPRoute 直接到 K8s Service(v1 兼容旁路)
  │     ├── 浏览器 baggage → HTTPRoute 不识别 → 走 catch-all baseline
  │     └── 解决:EnvoyFilter Lua 在 ingress-gateway 入口把 X-env 转 baggage
  │
  └── ⑥ deploy.py wait App Health 死等
        ├── 旧 success 条件 health == 'Healthy' 要求 ArgoCD App 整体 health
        ├── M-13 自动 commit 写 baseline overlay → baseline app OutOfSync
        ├── PR app 聚合 health 标 Degraded → 死等 900s timeout
        └── 修:M-18 加 pod_healthy_count,Pod Healthy 30s 就 success

根因分析: 每个 hotfix 都是"v1 → v2 升级时的边界情况"——① commit msg 不实证导致漏改;② OTel 改造范围评估不准(应该对照 hub-backend 的改动量警觉);③ v1 时代的"防反选"在 v2 mesh 启用后反而破坏路由派;④ pre-mesh annotation 在 mesh 启用后变成竞争条件;⑤ 南北流量(HTTPRoute)和东西流量(Istio VS)是两套独立路由不互通;⑥ wait 逻辑依赖 App 整体 health 但 baseline OutOfSync 会污染聚合状态。

修复方案: ① commit msg 必须 git diff --cached 实证;② OTel 改造要对照参考实现(hub-backend)评估改动量;③ mesh 启用后基础 Service selector 只保留 app=<svc>,PR Pod 和 baseline Pod 都进 endpoints,由 Istio DR subset 通过 Pod label version=xxx 在 endpoint 级筛选;④ mesh 启用前清查所有 excludeOutboundPorts annotation,对应端口添加 ServiceEntry;⑤ EnvoyFilter 在 ingress-gateway 入口做 X-env → baggage 转换让双轨都工作;⑥ wait 逻辑以 Pod 状态为真相来源,不依赖 App 整体 health。

题目6:Sandbox 锁超时——子环境 ID 撞车的延伸事故#

故障现象: 某子环境新建后,用户创建 sandbox 一直 pending,报错 lock timeout。

排查路径:

text
Sandbox 创建 pending
  ├── Sandbox 不是 K8s Pod
  │     ├── kubectl get pods | grep sandbox-id → 查不到
  │     ├── gVisor 沙箱是 containerd + runsc 在裸 EC2 节点运行
  │     └── 判断存活要调控制面 API /portal DB/节点 state JSON
  │
  ├── 控制面 API 报什么错?
  │     ├── GET /sbv1/sandboxes/<id> → status=creating
  │     ├── 日志报 lock timeout
  │     └── 某个资源被锁住无法获取
  │
  ├── 是不是集群合并/迁移时辅助组件没迁全?
  │     ├── 业务 workload(agent/portal/meter/ui)都 Running
  │     ├── 但 placeholder Pod 没迁 → Karpenter 不触发节点创建
  │     ├── 配置中心 nodepool_name 字段还指向旧环境
  │     └── 沙箱被调度到错误的 NodePool
  │
  └── 根因:迁移时只搬业务 workload 漏了辅助组件
        ├── placeholder Pod(触发 Karpenter NodePool)
        ├── 配置中心的 nodepool_name 字段
        ├── node-protector / custom scaler
        └── RBAC / 证书 / init job

根因分析: 集群合并/迁移时只看业务 workload Running 不够,必须逐项检查辅助组件。DaemonSet 不触发 Karpenter 创建节点,必须有 placeholder 这种 pending pod 才会拉起新 node。

修复方案: ① 集群合并 checklist——业务 workload + 辅助组件(placeholder/protector/scaler/RBAC/证书)+ 配置中心字段 + 端到端验证(创建→使用→销毁完整链路);② 沙箱存活判断按优先级——控制面 API > portal DB > 节点 state JSON,不要用 kubectl get pods;③ 锁超时排查——看锁的持有者(哪个进程/ Pod 持有)+ 锁的 TTL(是否过期未释放)+ 锁的实现(DB 行锁/Redis 分布式锁/etcd lease)。