refresh 是计算,sync 是应用
一句话总结
refresh 只会重新渲染并计算 diff,不会应用任何内容。只有 sync 会修改集群。 如果忽略这句话,就无法理解 Argo CD 的一半机制。
为什么需要它
在 UI 中点击 Refresh 后,应用仍保持 OutOfSync,大多数人会怀疑“是不是坏了?”其实这是正常行为。refresh 只做以下三件事。
- 请求 repo-server 重新生成最新 manifest(根据是否使 cache invalidation,hard refresh 可能从 clone 开始)。
- 从目标集群重新读取 live state。
- 比较二者,更新 Sync Status(Synced / OutOfSync)与 Health Status。
到此为止。apply 是 sync 的工作,而 sync 只会在人手动触发或启用 syncPolicy.automated 时发生。正因为分离了这两项操作,系统才能在不修改任何内容的情况下,持续观察“当前 Git 与集群相差多少”。许多组织关闭自动同步运行,理由正在于此:始终监控,由人决定 apply。
工作原理
3-way diff——为什么需要三种状态
Argo CD diff 查看的是三种状态,而不是两种。
| 状态 | 来源 | 回答的问题 |
|---|---|---|
| Desired | Git 渲染结果 | 我们想要什么 |
| Live | 集群当前 object | 现在实际是什么 |
| Last-applied | object 的 last-applied-configuration annotation |
我们之前声明要管理什么 |
只比较两种状态会产生致命误判。例如 HPA 已把 replicas 提高到 5,而 Git 中完全没有 replicas 字段。如果只比较 Desired 与 Live,会得出“这是只存在于 Live 的字段,应删除”。加入第三种状态后,答案不同:last-applied 中也没有 replicas,说明我们从未管理过该字段,它属于其他 controller,不应触碰。
Normalization 也发生在这里。metadata.resourceVersion、uid、generation、creationTimestamp、managedFields 以及大部分 status 都会从 diff 中移除。Kubernetes 自动填充的默认值——Service 的 clusterIP,以及 image tag 为 latest 时的 imagePullPolicy: Always——同样会被忽略。没有 normalization,所有应用都会永远显示 OutOfSync。
selfHeal 的真实含义
syncPolicy.automated.selfHeal: true 表示“自动恢复 drift”。换个角度,它意味着:
故障处理中手动修改的内容会被恢复。
凌晨 Pod 故障后,若紧急通过 kubectl scale 增加 replicas,下一个 reconciliation loop 会把它恢复为 Git 中的值。这不是 bug,而是设计。启用 selfHeal 的组织,也接受了“紧急修复同样通过 commit 完成”的纪律。如果需要紧急出口,标准做法是暂时关闭自动同步,或将相应字段加入 ignoreDifferences,排除在管理范围之外。
Wave 与 hook——创建顺序的两种机制
声明式无法直接表达顺序,因此叠加了两种机制。
Sync wave 通过 argocd.argoproj.io/sync-wave annotation 的数字对 resource 分组,并从较小数字开始 apply。关键在于:只有当前 wave 中所有 resource 都变为 Healthy,才会进入下一 wave。如果错误划分 wave,deployment 就会停在原地。例如,把永远无法 Healthy 的 resource(没有入站流量就无法 ready 的 Job 等)放进前一个 wave,后续内容将永远不会执行。同一 wave 内使用 resource kind 的默认顺序(Namespace → NetworkPolicy → ResourceQuota → LimitRange → ServiceAccount → Secret/ConfigMap → RBAC → CRD → PV/PVC → Service → workload → Ingress)。
Hook 则直接拆分 phase:PreSync → Sync → PostSync,失败时运行 SyncFail。hook 通常是 Job,通过 annotation argocd.argoproj.io/hook: PreSync 指定。删除策略 argocd.argoproj.io/hook-delete-policy 有 HookSucceeded、HookFailed、BeforeHookCreation 三个值,默认值是 BeforeHookCreation。也就是说,即使 hook resource 成功,它仍会保留,直到下一次 sync 创建新对象前才删除。正因这个默认值,失败后仍能查看 migration Job 日志。
Retry 与 backoff
sync 失败后,会通过 exponential backoff 重试。如果 duration: 5s、factor: 2、maxDuration: 3m,等待时间会按 5s → 10s → 20s → 40s → 80s 增加,并在 3 分钟达到上限。触发 retry 的场景包括 resource apply 失败、Health Check timeout、hook Job 失败和临时网络错误。
prune 的危险
prune: true 会把 Git 中已消失的 resource 也从集群删除。判断标准不是“集群中存在而 Git 中不存在”的全部对象,而是Argo CD 已标记为自己所有、但 Git 中不存在的对象。标记方式称为 resource tracking,默认使用 annotation。
argocd.argoproj.io/tracking-id: APP_NAME:GROUP/KIND:NAMESPACE/NAME
예) checkout-prod:apps/Deployment:cgoa-prod/prod-checkout
Legacy 方式使用 label app.kubernetes.io/instance。Helm 等其他工具也使用该 label,可能造成 ownership 判定冲突,因此推荐 annotation 方式。
prune 可怕的原因是:一个修改错误路径的 commit 就可能造成批量删除。如果把 source.path 错写为 empty directory,渲染结果会变成 0 个 resource,该应用管理的全部 resource 都会成为 prune candidate。防护措施包括 allowEmpty: false(拒绝空渲染结果)、PruneLast=true(其他 resource 同步结束后最后 prune),以及单个 resource 的 argocd.argoproj.io/sync-options: Prune=false。
现场会遇到的情况
作者家庭实验室扩展 GPU node 时,就需要这种判断能力。GPU Feature Discovery 会自动为新节点添加 label——RTX 3090 24GB、5090 32GB,以及两台 4070 Laptop 8GB。但如果 Pod 只请求 nvidia.com/gpu: 1,需要 32GB 的 training task 仍可能被调度到 8GB laptop GPU,因为 Kubernetes 只知道二者都是“一个 GPU”。
因此,团队手动添加了 semantic label(如 gpu.homelab/tier=xlarge、vram=32g)。从 GitOps 角度,由此得到一项教训:controller 添加的 label 与人工声明的 label 会共存于同一个 object。如果通过 GitOps 管理该 Node object,就必须用 ignoreDifferences 排除 GFD 添加的 label,否则 reconciliation loop 与 controller 会彼此删除对方字段。这个场景完整体现了为什么需要 3-way diff 与 field ownership。
下一项实验要做什么
先把 manifest 部署到真实集群,再手动修改 replicas 制造 drift,使用 kubectl diff 将差异保存到文件,然后恢复(人工 self-heal)。接着编写三个带 sync wave annotation 的 manifest 与一个 PreSync hook Job;最后遍历集群,亲自判断哪些 object 是 prune candidate。