GitOps 可观测与分层排障:从 Source 到 Runtime 找到首个失败点
发布窗口里,Git 提交已经合并,控制器页面显示 Synced,Deployment 也显示 Available,用户请求却持续 503。值班人员先重启 GitOps controller,再清 Redis 缓存,最后强制同步,二十分钟后才发现 Service selector 与 Pod label 不匹配。控制面、Kubernetes 资源健康和真实请求属于不同证据层;把绿色状态当成业务成功,会让排障从最远的组件开始试错。
另一次事故里,数百个应用同时停在旧 revision。仓库可访问、Pod 也 Running,日志却被相同的 retry 信息淹没。真正的首个异常是目标 API Server 持续返回 429,workqueue age 不断增长,控制器经过退避后还在处理旧 generation。只查单个 Application 看不出全局积压,只查 CPU 又看不出限流;GitOps 可观测的核心,是把每次变更从 source 到 runtime 的状态和容量证据关联起来。
用六层证据替代一张绿色状态图
第一段是 source:控制器是否取得批准的 commit、tag、Chart 或 OCI digest,认证、签名、TLS、缓存和 artifact 是否正确。第二段是 render:实际使用哪个 Helm、Kustomize 或 plugin 版本,输入参数、远程依赖、耗时、对象数、输出字节和 hash 是什么。第三段是 diff:候选对象是否经过目标 API 的 schema、默认化、准入与权限判断,desired/live 的差异是否可解释。
第四层是 apply:调谐器是否接收新一代声明、进入队列、执行 dry-run/apply/delete,API 是否返回冲突、429、5xx 或 webhook timeout,inventory 是否更新。第五层是 health:Deployment、StatefulSet、Job、Rollout 或自定义资源是否达到产品定义的 Healthy/Ready。第六层才是 runtime:真实请求是否到达新版本,关键事务、数据不变量、延迟、错误率和业务 SLO 是否达标。Health 回答“资源控制器如何评价对象”,Runtime 回答“用户是否得到正确结果”,两层必须独立留证。
SOURCE
commit / digest / signature / artifact
|
v
RENDER
renderer version / input / object set / hash
|
v
DIFF
desired vs live / server validation / admission
|
v
APPLY
generation / queue / API request / inventory
|
v
HEALTH
conditions / rollout / job terminal state
|
v
RUNTIME
request / dependency / data invariant / SLO每次变更生成一个关联键:source revision -> rendered hash -> cluster -> reconciler namespace/name/generation -> live UID/resourceVersion -> runtime evidence。日志与指标至少携带其中稳定且低基数的部分;完整对象关系放入事件记录或追踪存储,不把 commit、对象名、客户名全部塞进 Prometheus label。没有关联键,“最近一次同步成功”可能只是旧 generation 的状态。
安装观测入口前先确认组件拓扑与身份
观测不是先装一个 Dashboard。先画出 Git/OCI、repo 或 source controller、渲染进程、application/kustomize/helm controller、管理集群 API、目标集群 API、通知与业务探针的调用方向。Argo CD 的 repo-server 负责取源和渲染,application-controller 维护 cluster cache 并调谐;Flux 把 source、kustomize、helm 等职责拆成不同 controller;Fleet 还要经过 management cluster 的 GitRepo/Bundle、BundleDeployment 和下游 agent。拓扑不同,第一跳日志和故障域也不同。
最小启用顺序是:先保留 CR status 与 Kubernetes Event,再采组件结构化日志和 /metrics,最后接入集中日志、Prometheus、告警与业务探针。指标抓取身份只需要访问 metrics endpoint;排障身份只读 GitOps CR、目标对象、Event 和非敏感日志,不应默认读取 Secret、执行 Pod 或修改 Application。HA 多副本要逐 Pod 或 Endpoint 抓取,不能只抓 ClusterIP 的一个后端;Argo CD 的组件指标与抓取注意点见 Metrics。
不要尝试用一个选择器同时匹配三套产品。先从目标组件反查 namespace、Pod label 和指标端口,再为每个组件建立独立抓取对象;这样某个产品升级改了 label 或端口时,门禁能明确指出失效组件。下面以 Flux 的 kustomize-controller 为例,先确认 app=kustomize-controller 与 http-prom 确实存在,再应用 PodMonitor。sampleLimit 和 relabel 是容量保护,不应在不知道现有序列规模时直接放大。若集群没有 Prometheus Operator,可用等价 scrape 配置,但仍要保持 TLS、NetworkPolicy 与最小 RBAC。
kubectl get pod -n flux-system -l app=kustomize-controller --show-labels
kubectl get pod -n flux-system -l app=kustomize-controller \
-o jsonpath='{range .items[*].spec.containers[*].ports[*]}{.name}{"\\n"}{end}'apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: flux-kustomize-controller
namespace: observability
spec:
namespaceSelector:
matchNames: [flux-system]
selector:
matchLabels:
app: kustomize-controller
podMetricsEndpoints:
- port: http-prom
interval: 30s
scrapeTimeout: 10s
honorLabels: false
sampleLimit: 20000
relabelings:
- action: labeldrop
regex: "pod-template-hash|controller-revision-hash"接入后先验证抓取目标数量与实际副本一致,再让一个低风险对象调谐并观察状态、Event、日志和 reconcile 指标是否出现同一时间窗口。metrics endpoint 可访问只证明采集链通,不证明各产品所需指标名在当前版本存在;升级前要对仪表盘和告警执行指标清单差异。
Controller 状态先判断它看到了哪一代
排障第一步不是读日志,而是确认产品实际暴露了哪一种“已观察状态”。Flux 的 Kustomization 等对象通常可比较 metadata.generation 与 status.observedGeneration:前者更大表示控制器尚未处理最新 spec;两者相等但 Ready=False 表示它已处理并给出失败原因。Argo CD Application 不应套用这条判断,它主要通过 reconciledAt、sync revision、operation state 和资源状态表达结果;时间戳只能证明刷新时间,不能证明某一 generation 已成功应用。Fleet 还要把管理侧 generation、BundleDeployment 的 syncGeneration 与下游 agent/Helm 状态串起来。
Argo CD 关注 Application 的 desired source、sync revision、sync status、health、operation phase/message 和资源列表;Flux 关注 Source artifact revision、Kustomization/HelmRelease 的 lastAttemptedRevision、lastAppliedRevision、inventory 与 condition;Fleet 关注 GitRepo commit、预期 Bundle 数、BundleDeployment syncGeneration、agent 状态和下游 Helm release。它们不是同名字段的不同拼法,不能用一个 JSONPath 模板强行统一。Fleet 的逐层入口和 fleet monitor 检查项可对照 Troubleshooting 与 fleet monitor。
# Argo CD:只读状态,不输出 repository 或 cluster Secret。
kubectl get application demo-api -n argocd -o json \
| jq '{generation:.metadata.generation,reconciledAt:.status.reconciledAt,desired:(.spec.source.targetRevision // [.spec.sources[]?.targetRevision]),syncedRevision:(.status.sync.revision // .status.sync.revisions),operationRevision:(.status.operationState.syncResult.revision // .status.operationState.syncResult.revisions),sync:.status.sync.status,health:.status.health.status,operation:.status.operationState.phase,conditions:.status.conditions}'
# Flux:将 source 与 applier 分开看。
flux get sources all -A
flux get kustomizations -A
kubectl get kustomization demo-api -n flux-system -o json \
| jq '{generation:.metadata.generation,observed:.status.observedGeneration,attempted:.status.lastAttemptedRevision,applied:.status.lastAppliedRevision,conditions:.status.conditions,inventory:.status.inventory}'
# Fleet:从管理对象走到下游部署对象。
kubectl get gitrepo,bundle,bundledeployment -A
kubectl get gitrepo demo-api -n fleet-default -o json \
| jq '{generation:.metadata.generation,observedGeneration:.status.observedGeneration,commit:.status.commit,gitJob:.status.gitJobStatus,desiredReady:.status.desiredReadyClusters,ready:.status.readyClusters,summary:.status.summary,conditions:.status.conditions}'状态 message 适合定位入口,不是长期审计。控制器可能在下一轮覆盖旧 message,Event 也有保留期限。发现异常后立即保存对象 identity、generation、condition reason、revision 与时间,不导出整个 Secret 或带明文的渲染结果。
三套产品的状态分流也应对应各自拓扑。Argo CD 先看 Application,再按失败层转到 repo-server、application-controller 或目标集群资源;Synced 只说明期望对象与 live 状态达到其同步判定,Healthy 只说明 health assessment 通过。Flux 先看 Source 是否产生目标 artifact,再看 Kustomization/HelmRelease 是否消费同一 revision;某个 controller Ready 不能替代另一段 controller 的状态。Fleet 则从 GitRepo 走到 Bundle、BundleDeployment、下游 agent 和 Helm release,管理侧 Ready 不能证明每个下游集群已经完成。任何聚合页面都必须允许下钻到原始 CR、Event 和目标对象,不能只保留被压平的红绿灯。
Event、日志和指标各自回答不同问题
Event 是对象附近的离散事实,适合回答哪个 controller 因何拒绝了哪个 generation,例如 ReconciliationFailed、DependencyNotReady、OwnerRefInvalidNamespace 或 admission timeout。日志补充调用栈、重试上下文与组件内部阶段;指标用于判断频率、持续时间、总体规模和是否跨对象扩散。三者必须互证:单条 ERROR 可能已恢复,错误率下降也可能只是队列停止消费。Flux 的 Event 关联方式和 Prometheus 暴露方式分别见 Events 与 Metrics。
优先按对象过滤 Event,再按 controller、namespace、时间窗和 correlation 信息查日志,最后用指标判断局部还是系统性。不要先 kubectl logs -f 盯屏;日志量大时最早异常会被 retry 覆盖。结构化日志应包含 component、reconciler kind、namespace/name、generation、revision 摘要、result、reason、duration 与 request UID,禁止输出 authorization header、private key、kubeconfig、Secret data 或解密 manifest。
kubectl events -n flux-system --for kustomization/demo-api
kubectl get events -A --sort-by=.lastTimestamp \
| grep -E 'Failed|BackOff|Conflict|Forbidden|Unhealthy|OwnerRef'
kubectl logs -n flux-system deploy/kustomize-controller \
--since=20m --prefix \
| grep -E 'demo-api|reconcile|error|rate|conflict'
kubectl logs -n argocd deploy/argocd-repo-server \
--since=20m --prefix \
| grep -E 'manifest|timeout|repository|error'
kubectl logs -n cattle-fleet-system deploy/fleet-controller \
--since=20m --prefix \
| grep -E 'gitrepo|bundle|cluster|reconcile|error'支持包进入工单前删除真实 repo URL、集群地址、客户 namespace、用户名和对象中的业务值。日志保留期按故障发现窗口与审计要求设置;无限保留既增加成本,也扩大敏感信息暴露面。Kubernetes audit 对 Secret 不应记录 RequestResponse,否则审计后端会成为新的明文存储。
沿六层链路找到第一个异常而不是最后一个报错
source 失败时先确认 resolved revision 是否存在、artifact 是否产生、认证与 CA/known_hosts 是否有效、签名或来源验证是否通过、缓存是否过旧。Git 200 响应不代表拿到了批准 commit,source Ready 也不代表下游已经消费该 artifact。仓库凭证错误时不得为了恢复自动降级为跳过 TLS 或允许任意 host key。
render 失败关注 renderer/plugin 版本、依赖锁定、超时、OOM、临时目录和输出规模。一个非确定 plugin 可能每次生成不同 annotation 或顺序,使控制器持续 OutOfSync。保存同 revision 的两次规范化 hash;若不同,先隔离渲染器,不能通过提高 sync 频率掩盖。Argo CD repo-server 是常见的内存和并发热点,Flux 则要定位到 source、kustomize 或 helm controller。
diff 失败要区分工具退出码、有业务差异和执行错误。目标 API 的 CRD 缺失、conversion webhook 不可达、validation 拒绝、mutation、默认值和不可变字段都可能只在 server-side dry-run 出现。新建对象与现存对象的 diff 能力也可能不同。不要把“本地 template 成功”升级为“目标集群可应用”。
apply 失败关注 observed generation、workqueue、retry/backoff、RBAC forbidden、SSA conflict、API 429/5xx、watch relist 与 webhook latency。对 429 增加 worker 往往更糟:先降低并发、错开触发、确认客户端限速,再评估 API Server 与 admission 容量。对 conflict 则找 managedFields writer,不自动 force。
health 失败从 Deployment/StatefulSet/Job/Rollout condition 与 Event 走到镜像拉取、调度、探针、网络和配置。资源 Healthy 以后进入独立的 runtime 验证:从与真实用户相同的入口发送请求,核对流量是否到达目标版本、依赖是否可用、关键数据不变量是否保持、SLO 是否恢复。Service selector 错误时 Deployment 可以健康;数据库迁移副作用失败时 Pod 也可能 Ready。排障结束的判据是最新 revision、live 对象与运行结果关联,而不是日志停止报错。
队列、退避与 API 限流决定恢复速度
调谐器通常使用 workqueue,把对象事件或周期扫描转成 reconcile。要同时看 queue depth、oldest age、add/rate、reconcile duration、result、retry 和 backoff。depth 上升但 age 稳定,可能只是短时突发;age 持续上升且 completion rate 低于 arrival rate,才是积压。只看平均时延会掩盖长尾对象,至少观察 p95/p99 和最老项。
积压的根因可能在 source、render CPU/内存、目标 API、webhook、网络或单个毒性对象。按 reason 和阶段拆分失败,避免所有 retry 共用一个告警。控制器恢复或目标集群重连后,数千对象会形成 reconcile storm;若同时启用 self-heal、prune 和 Job hook,写放大与副作用风险会一起上升。
容量调节按证据进行:render 饱和时增加隔离 worker、缓存与资源,前提是远程依赖已固定;API 429 时降低 apply 并发并分批恢复;单集群延迟时隔离 shard 或 cohort;大对象导致 OOM 时限制 manifest 数量和输出字节。Argo CD 文档中的 processor 示例只是起点,不是容量承诺;Flux/Fleet 也需要结合实际 CR、对象与集群扇出压测。
指标入口同样不能跨产品硬凑。Argo CD 应从官方 Metrics 中选择与 Application、reconcile、repo 请求和 cluster cache 对应的指标,并保留 application-controller 与 repo-server 的组件维度。Flux 除 controller-runtime 队列与调谐指标外,还公开 GitOps Toolkit 对象的 condition、suspend 与资源信息,具体抓取和 recording rule 以当前版本的 Metrics 为准。Fleet 排障先用 GitRepo、Bundle、BundleDeployment 状态和 fleet monitor 判断哪一层停滞,再按 Observability 分别采集 fleet-controller 与 gitjob 服务的指标;controller 负责调谐和下游状态,gitjob 负责取源,二者不能合成一个无组件维度的错误率。对任何产品,发布仪表盘前都应抓取一份实际指标清单,并用非空查询断言关键面板确实有序列。
# 示例表达式需替换为当前产品和版本实际暴露的 metric。
sum by (controller) (rate(controller_runtime_reconcile_total{result="error"}[5m]))
histogram_quantile(
0.99,
sum by (le, controller) (rate(controller_runtime_reconcile_time_seconds_bucket[5m]))
)
max by (controller) (workqueue_depth)
max by (controller) (workqueue_longest_running_processor_seconds)
sum by (code, host) (rate(rest_client_requests_total{code=~"429|5.."}[5m]))指标名和 label 由具体 controller-runtime、产品版本与构建选项决定。部署告警前先查询 /metrics 和 release notes;不存在的指标应让门禁失败,不能让 Dashboard 静默显示空图。
失败分流要按恢复动作而不是组件名称
第一类是永久配置错误:无效 revision、schema、RBAC、签名、不可变字段或 ownership conflict。它们不会靠重试自愈,应暂停自动重试、回退 source 或修复声明。第二类是暂时依赖故障:Git、Registry、目标 API、DNS、webhook 或网络短时不可用。允许有上限的退避,并验证恢复后控制器消费最新 revision,而不是依次重放每个过期 revision。
第三类是容量过载:OOM、render timeout、queue age、API 429、watch relist 和日志阻塞。恢复动作是限流、分片、扩容或减少工作集,不是重启所有组件。第四类是运行态失败:资源已应用但探针、请求、数据或 SLO 不达标,需要暂停晋级、执行业务回滚或补偿。第五类是副作用不确定:Job、数据库迁移、prune、finalizer 或外部 API 已部分执行,这类场景禁止机械重试,先确定幂等键和实际完成状态。
告警消息直接给出层级、对象、最新 revision、observed generation、第一条异常证据、当前自动动作和停止按钮。仅发送“controller pod down”不足以指导响应;Pod 重启后若 queue 继续增长,事故仍在。反过来,一个副本短暂重启但队列、调谐成功率和 SLO 正常,可以按低优先级处理。
用正反实验验证证据链和恢复路径
实验只能在隔离集群或明确划定的 canary namespace 进行,并预先准备暂停调谐、回退 commit、恢复凭证和撤销限流的操作。任一实验出现非目标 namespace 变化、prune 候选扩大、API 429 超出预算、队列最老项持续恶化、真实业务 SLO 下降或副作用状态不明,立即停止注入、暂停自动动作并恢复安全 revision。停止条件不是实验失败,而是防止观测训练变成生产事故的控制器。
正向实验提交一个只修改 ConfigMap 标记的 commit,记录 source 解析 revision、artifact digest 或摘要、render hash、diff、controller generation、live resourceVersion 和一个真实 HTTP 响应。预期每一层都指向同一 revision,队列短暂上升后回落,没有重复 apply,业务探针返回新标记。若关联链中任一层仍指向旧 revision,停止继续注入并先修复证据关联。删除实验对象前保存这条完整链,作为后续故障注入基线。
第一组反例让 source 指向不存在的 commit 或使用专用、可立即撤销的失效只读凭证。预期 source condition 首先失败,不产生新 artifact,下游仍停留在旧安全 revision,业务不应被空渲染覆盖。若出现空对象集、prune 候选或共享仓库访问异常,立即暂停调谐并回退声明。恢复新凭证后,控制器应直接消费最新批准 revision;旧凭证必须在服务端继续被拒绝。
第二组反例让渲染仍成功,但提交目标集群缺失的测试 CRD 或会被 canary admission 策略拒绝的对象。预期 render 证据存在,server validation/diff 或 apply 层失败,live 对象不出现。若拒绝范围扩散到 canary 之外、webhook 延迟影响正常请求或出现同名生产对象,立即撤销策略并停止实验。该实验可证明本地 template 与目标 API 是两道门;恢复方式是回退 commit 或先完成 CRD/admission 依赖,不是跳过 dry-run。
第三组反例对无持久数据的测试对象制造 SSA conflict,并在隔离 API 或网络代理上对测试主体施加限流。预期 condition/Event 明确区分 conflict 与 429,前者进入 ownership 处置,后者进入退避与容量处置。queue age 超过预设预算、429 出现在非测试主体或 managedFields 的合法 owner 被覆盖时立即撤销限流和冲突写入。若两类故障都只显示 generic sync failed,说明观测维度不足,需要补充 reason、REST client 指标和 managedFields 读取路径后再试。
第四组反例在没有真实流量的 canary Service 上保持 Deployment Healthy,但让 Service selector 不匹配。预期 GitOps sync 与 Deployment health 可为绿色,canary 真实请求失败并触发 runtime SLO 告警。若 selector 变化命中共享 Service、Endpoints 影响非测试 Pod 或真实用户错误率变化,立即回退。恢复安全 revision 后,既要确认 selector、EndpointSlice 和目标 Pod,也要从用户入口确认新请求成功。这个实验是防止团队把控制器状态当作发布结果的最后一道训练。
告警与 SLO 要覆盖新鲜度、正确性和运行结果
控制面 SLI 至少有 source fetch 成功率与新鲜度、render 成功率与 p99 时延、reconcile 成功率与 queue age、API 错误率、observed generation lag、inventory/drift/prune 异常数。业务 SLI 仍由真实请求、延迟、错误率、数据和关键事务定义。两套 SLI 关联但不合并:GitOps 绿色而业务红色,或 GitOps 暂时红色而旧版本业务正常,响应策略完全不同。
告警按影响而不是每次 retry 触发。单对象永久失败、同租户批量失败、单集群 API 异常、全局 source 故障和业务 SLO 破坏应有不同优先级。使用时间窗口抑制瞬时 reconcile,保留“最新批准 revision 多久未被消费”的 freshness 告警;这比单纯统计 error log 更接近交付风险。
容量 SLO 还应定义恢复能力,例如目标 API 中断一个演练窗口后,控制器必须在团队约定的恢复时限内处理积压,同时不突破 API 429、Job 重跑、prune 和业务错误预算。阈值不能从示例照抄:先测量稳定期 arrival rate、completion rate、queue age 和目标 API 余量,再由平台团队与业务 owner 共同签署。压测变量包含应用数、每应用对象数、manifest 字节、渲染耗时、集群数、drift 比例和同时提交数。成本评估同时计算控制器 CPU/RSS、repo 缓存、Prometheus 序列、日志吞吐、长期审计与跨区流量。
指标标签禁止放完整 repo URL、commit message、Secret 名中的客户信息、用户邮箱或未经归一化的错误文本。对象级状态可通过 kube-state-metrics 或事件存储按需查询,不要为每个 GVK/name 创建永久高基数时序。
权限、凭证与敏感数据贯穿整个排障过程
观测账号采用只读、分层授权:平台值班可读 controller namespace 中的 CR、Event 和日志;应用值班只读自己 namespace 的对象;安全人员按审批读取审计。pods/log、pods/exec、Secret 读取和 GitOps API override 是不同权限,不能打包成一个“排障角色”。能够修改 Pod 的主体可能间接挂载 Secret,因此还要配合 admission 和 ServiceAccount 限制。
调试时关闭 shell set -x,不要在命令行传 token,不输出完整 kubeconfig、authorization header 或 Secret YAML。repository credential、cluster credential、KMS key ID、私有 hostname、namespace 和渲染值都可能进入日志、diff、Event 与截图。日志脱敏规则要有反向测试:故意注入测试 token,确认采集、转发、索引、告警和支持包均不出现原值。
临时提升日志级别会显著增加敏感风险和成本。记录组件、级别、开始时间、负责人和自动回落时间;故障结束后恢复级别,清理临时文件和本地端口转发,撤销短期 RoleBinding。凭证轮换的完成信号包括新身份成功、旧身份服务端拒绝、控制器重新取得 source/cluster、最新 revision 被应用以及业务继续正常。
回滚、清理与长期运行形成闭环
回滚先确认失败位于哪一层。source 或 render 错误通常回退 Git revision;apply 造成不可变对象替换时,还要验证 live UID、存储和流量;业务 SLO 失败可能需要回退镜像、配置或执行向前补偿。Job、数据库变更、prune 和外部 API 具有副作用,不能假设 Git revert 会撤销已经发生的动作。
故障恢复后保留精简证据:首个异常层、source revision、render hash、generation、API request UID、Event reason、关键指标时间窗、runtime 结果和恢复提交。删除带明文或客户标识的临时日志,按保留策略归档脱敏记录。实验清理则先 suspend 自动调谐,确认没有进行中的 prune/hook,再删除测试 Application/Kustomization、低风险对象、监控规则和临时权限;最后检查 orphan namespace、PVC、LoadBalancer 与历史日志费用。
长期值守把 Dashboard 按证据层组织,而不是按 Pod 列表组织。平台团队维护采集、容量、队列和 API 预算,应用团队维护 runtime 探针、数据不变量与回滚判据,安全团队维护日志脱敏、访问审计和凭证轮换;任何无人负责的面板最终都会变成静默空图。每次版本升级比较 metrics、condition reason、日志字段和告警查询;按风险周期注入 source 不可达、render timeout、admission 拒绝、SSA conflict、API 429 和 runtime 失败,确认告警能指向首个异常层、值班能停止自动动作、恢复后能证明最新 revision 与真实业务结果一致。可观测不是收集更多数据,而是让团队在最短路径上作出正确且可回滚的判断。
