状态:草稿(2026-10-17),部分为实测数据。 基准版本为 argoproj/argo-cd v3.5.1(提交 109ca7ca7113)、argoproj/argo-workflows v4.1.5(提交 6c80c7e56ae6)。由于小型集群运行着相同的版本,我们在 2026-10-17 通过 仅读取 进行了确认(Pod、Application 状态、配置键、CronJob 状态字段)。小型集群中没有 Argo Workflows,因此仅阅读了代码(未实测)。编写规则:以实测数据为基础,未能测量的数据标记为“未实测”,未能确认的标记为“未确认”。
目录
- 1. 一句话总结:“对比循环”与“用 Pod 工作的 Worker”
- 2. 架构图 ①:Argo CD 的对比循环
- 3. 代码地图
- 4. 沿着核心路径前行 — Argo CD
- 5. 沿着核心路径前行 — Argo Workflows
- 6. 作者的贡献与后续延伸
- 7. 实测:小型集群中的 Argo CD
- 8. 从哪里开始阅读
- 9. 适合首次贡献的地方
- 10. 局限性
- 参考资料
1. 一句话总结:“对比循环”与“用 Pod 工作的 Worker”
虽然名字里都有 Argo,但这两个项目的性质有所不同。
- Argo CD 是一个 恒温器。它持续对比 git 中记录的目标温度(期望状态)和房间里的温度(集群的实际状态),如果不一致就会发出提醒(OutOfSync)或者进行调整(sync)。这个无限循环的对比过程就是代码的核心。
- Argo Workflows 则是 工厂作业指导书。它接收将各个步骤按顺序和依赖关系组合起来的 Workflow,为每个步骤启动一个 Pod,而步骤之间传递的文件(制品)则通过 S3 等存储进行交接。
| 组件 | 职责 | 运行位置 |
|---|---|---|
| argocd-repo-server | git·helm 저장소를 받아 helm/kustomize/plain 으로 매니페스트 렌더 | Deployment |
| argocd-application-controller | 每个应用的对比、状态判定、sync。持有集群缓存 | StatefulSet(支持分片) |
| argocd-server | API、UI、CLI 的入口,认证 | Deployment |
| redis | 渲染结果、应用资源树缓存 | Deployment |
| applicationset-controller | 通过生成器批量创建 Application | Deployment |
| workflow-controller | Workflow 的节点状态机,创建 Pod | Deployment |
| argoexec | 步骤 Pod 内的 init/wait(或 supervisor)容器,包裹 main 的 emissary | 每个步骤 Pod 专属 |
2. 架构图 ①:Argo CD 的对比循环
图解说明:
- 从上到下是一次完整的“刷新”。控制器将 repo-server 渲染的 期望状态与集群缓存持有的 实际状态进行对比(②),针对每个资源判定状态(③)并写入 Application。
- 同步是单独运行的第二个队列(④·⑤)。将对比和应用拆分到不同的队列中,是为了防止耗时较长的 sync 阻塞大量应用的状态更新。
- 集群缓存通过 watch 将所有资源保留在内存中,而不需要每次都查询 API 服务器。虽然对比的代价变高了,但控制器的内存占用也变大了。
- 最下方的方框是本文 4.3 节的主题,即选择状态判定脚本的顺序。
3. 代码地图
在 v3.5 的 argo-cd 仓库中,过去独立的 gitops-engine gitops-engine/ 已经合并到了 文件夹中(位于go.mod 的 replace … => ./gitops-engine中)。对比、sync 和集群缓存的核心都在这里。
| 路径 | 内容 | 初次阅读的文件 |
|---|---|---|
argo-cd controller/ |
应用控制器:队列、对比、状态、sync | appcontroller.go, state.go, health.go, sync.go |
argo-cd controller/cache/ |
从应用视角封装集群缓存的层 | cache.go |
argo-cd gitops-engine/pkg/ |
cache(watch 缓存)、 diff, sync(灰度发布与钩子)、 health(Go 内置检查) |
sync/sync_context.go, health/health.go |
argo-cd reposerver/repository/ |
清单生成、渲染缓存 | repository.go |
argo-cd resource_customizations/ |
各资源类型的 Lua(状态、动作)、测试数据 | batch/CronJob/health.lua |
argo-cd util/lua/ |
Lua 执行器、脚本选择器 | lua.go |
argo-workflows workflow/controller/ |
Workflow 控制器、operator(状态机)、创建 Pod | controller.go, operator.go, workflowpod.go |
argo-workflows workflow/executor/ |
在 Pod 内运行的执行器(存储与加载制品) | executor.go |
argo-workflows workflow/artifacts/ |
存储驱动(s3、gcs、azure、oss、hdfs、http、git 等) | artifacts.go |
argo-workflows cmd/argoexec/commands/ |
argoexec init·wait·emissary 命令 |
emissary.go |
4. 路径追踪 — Argo CD
4.1 刷新:什么时候再次查看
控制器中有两个队列(位于controller/appcontroller.go:118 appRefreshQueue,第 121 行 appOperationQueue). appcontroller.go:955 Run 为这两个队列分别附加了 Worker Goroutine)。
- 刷新 Worker 是
appcontroller.go:1775processAppRefreshQueueItem。appcontroller.go:2105needRefreshAppStatus首先通过argocd_application_controller.go:45询问“现在真的是重新对比的时候吗”。如果用户点击了刷新、管理的资源发生了变化,或者设定的周期(默认 120 秒,带有最大 60 秒的抖动,位于 - )已过,就会进行对比。
controller/state.go:633CompareAppState对比是由state.go:206GetRepoObjs执行的。该函数通过 gRPC 从 repo-server 获取期望状态(位于controller/cache/cache.go:729GetManagedLiveObjs),从缓存中取出实际状态(位于pkg/diff/diff.go:76Diff),然后通过 gitops-engine 的 - 对每个资源进行对比。
state.go:1051在同一个函数的末尾会进行状态判定(在setApplicationHealth中调用 - )。我们将在 4.3 节中详细探讨这一点。如果开启了自动 sync,
appcontroller.go:2322autoSync会将 operation 写入 Application,然后第二个队列就会处理该应用。
4.2 渲染与应用:repo-server 与 sync
- repo-server 的入口是
reposerver/repository/repository.go:631GenerateManifest。它会先检查缓存(位于repository.go:1016getManifestCacheEntry),如果没有找到,则将仓库切换到对应提交,然后由repository.go:1714GenerateManifests使用repository.go:1921GetAppSourceType在 helm、kustomize、plain 和插件中选择一个进行渲染。 将渲染分离为独立服务的理由出于安全和资源的考虑。由于 helm、kustomize 和插件需要执行他人仓库的内容,因此需要将其与拥有集群凭证的控制器隔离;同时,繁重的渲染工作只需通过水平扩展 repo-server 来应对。 - 应用是由
controller/sync.go:102SyncAppState调用 gitops-engine 的pkg/sync/sync_context.go:478Sync来完成的。在此过程中,sync_context.go:954getSyncTasks将资源按阶段(PreSync、Sync、PostSync 钩子)和灰度序号进行排序,当前一个灰度健康之后才会进入下一个灰度。判断“是否健康”的过程再次回到了 4.3 节的状态判定。 - 实际状态缓存是由
gitops-engine/pkg/cache/cluster.go:1094sync在最初获取所有 API 资源的列表后,通过cluster.go:792watchEvents接收后续变更的 watch 来维护的。
4.3 状态判定:选择哪个脚本
单个资源的状态由 gitops-engine 的 pkg/health/health.go:70 GetResourceHealth 决定。如果正在被删除,则为 Progressing;否则, 会首先询问是否有覆盖(override) 。只有在没有覆盖的情况下,才会使用 Go 编写的内置检查(位于health.go:104 GetHealthCheckFunc,包括 Deployment、Pod、Job 等)。
Argo CD 侧的“覆盖”即为 Lua。如果简化 util/lua/lua.go:169 GetHealthScript 的顺序,结果如下:
key := GetConfigMapKey(obj.GroupVersionKind())
if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" {
return script.HealthLua, script.UseOpenLibs, nil // argocd-cm 에 사용자가 적은 것
}
// 와일드카드 항목 → 내장 Lua 파일 → 내장 와일드카드 순으로 찾는다
builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile)
来源:argoproj/argo-cd v3.5.1 util/lua/lua.go(GetHealthScript 摘要),Apache-2.0。
内置 Lua 是 resource_customizations/<그룹>/<종류>/health.lua 文件,通过 resource_customizations/embed.go 的
go:embed 整体打包进二进制文件中。同一文件夹中的 health_test.yaml 和 testdata/ 是该脚本的测试。 为新 CRD 的状态判定做出贡献的最简单途径就是在该文件夹中添加一个 Lua 文件和测试数据。无需懂 Go 代码,测试由 util/lua/health_test.go 查找并运行该文件夹下的 health_test.yaml 来完成。
这样判定出的资源状态由 controller/health.go:20 setApplicationHealth 汇总为整个应用的状态。最差的状态即为应用的状态,如果在资源上存在 argocd.argoproj.io/ignore-healthcheck: "true" 注解,则会从汇总中排除(第 41 行)。在 v3 中,资源级别的状态不再保存在 Application 对象中,而是保存在 redis 的资源树中 。这是因为 --persist-resource-health 的默认值是 false (位于argocd_application_controller.go:293)。这样选择是为了避免随着 Application 变大而导致 etcd 写入变重。
4.4 CronJob 案例:文档与 Lua 不一致的地方
CronJob 的内置 Lua 是位于 resource_customizations/batch/CronJob/health.lua 的一段 56 行的代码。决策分为五个分支:
| 条件(自上而下首个满足的条件) | 判定 | 行 |
|---|---|---|
spec.suspend == true |
Healthy(“CronJob is Suspended”)— 在 Issue #24428 解决前的临时方案 | 3 |
从未成功过(没有lastSuccessfulTime ) |
Healthy(“never completed successfully”) | 15 |
当前有正在运行的 Job(有status.active) |
)Healthy(“job is running”)— Issue #24429 | 27 |
| 最后成功时间 < 最后调度时间 | Degraded | 35 |
| 其他(包括从未调度过) | Healthy | 41·50 |
v3.5.1 版文档(docs/operator-manual/health.md)在 CronJob 章节中写道:“如果最后一个 Job 失败则为 Degraded”、“如果最后一个 Job 正在运行则为 Progressing”,并将
Suspended 状态的例子举为“停止的 CronJob”。与代码对比后发现这三处都不对:失败后如果下一个 Job 正在运行会恢复为 Healthy,运行期间不是 Progressing 而是 Healthy,停止的 CronJob 不是 Suspended 而是 Healthy。
5. 核心路径追踪 — Argo Workflows
workflow/controller/controller.go:337Run启动了 informer 和 Worker,而controller.go:922processNextItem取出一个 Workflow 并创建 operator。workflow/controller/operator.go:212operate是单次 reconcile。首先通过operator.go:1216podReconciliation将已启动 Pod 的状态反映到节点状态中,然后通过operator.go:2235executeTemplate从入口模板开始向下遍历,决定下一步要做什么。根据模板类型的不同,会分叉到steps.go:42executeSteps,dag.go:278executeDAG,operator.go:3435executeContainer。由于每次都从头开始遍历但会跳过已完成的节点,因此即使控制器重启,只要有 Workflow 的status.nodes就能继续执行。- 容器步骤由
workflowpod.go:341createWorkflowPod创建为 Pod。在用户容器main旁边附加了init(workflowpod.go:1092newInitContainers)和wait,并将main的命令用argoexec emissary包裹起来(位于cmd/argoexec/commands/emissary.go:46)。v4 中引入了无需 init 容器、supervisor单个容器同时处理 init 和 wait 工作的模式 (workflowpod.go:1127newSupervisorContainer,initlessPod配置)。 - 在 Pod 内部,
wait会等待 main 结束,然后将输出文件上传至存储库(通过workflow/executor/executor.go:1355Wait),executor.go:449SaveArtifacts)。 - 存储驱动由
workflow/artifacts/artifacts.go:29NewDriver根据制品中指定的类型进行选择。 S3 排在最前面(第 38 行),访问密钥即时从 Secret 中读取并填充。MinIO 作为 S3 驱动接入。 - 如果在制品中未指定存储库,
workflow/artifactrepositories/artifactrepositories.go:38Resolve会依次查找 工作流命名空间的默认 ConfigMap,然后是控制器配置中的默认存储库。
为什么要这样拆分? 集群中只有一个控制器,但阶段(Step)可能有数千个。如果繁重的文件传输不通过控制器处理,而是交由阶段 Pod 内的 argoexec 完成,
则即使传输量增加,也只需扩展阶段 Pod。不过代价是每个 Pod 都会附带一个辅助容器,v4 的 supervisor 模式被认为是一种旨在降低该成本的尝试(设计意图未通过文档确认,未核实)。
6. 作者的贡献与后续动向
- argoproj/argo-cd Issue #30209 → #30243。 最初的 Issue (#30209) 指的是 4.4 节中的“正在运行则为 Progressing”这一句,经由其他贡献者的 #30212 修正该行后已关闭。
其余三个问题(失败后下一个 Job 运行则为 Healthy、从未成功过则为 Healthy、已停止的 CronJob 不是 Suspended)已作为新的 Issue #30243 提出。
其依据全部来自 4.4 节的表格、
health.lua的第 3、15、27、35 行。 - argoproj/argo-workflows#17246(审查中)。这是一个 PR,旨在将目前使用无法下载的镜像及过大默认值的制品存储库 MinIO 安装章节,
替换为使用存储库中已有的 quick-start 的 MinIO 配置(
manifests/quick-start/base/minio)。第 5 节的第 5 点(通过 S3 驱动接入 MinIO 的方式)即为本文档说明的对象。 曾出现过因文档检查工具(cspell)不识别镜像名称而需要在词典中手动添加词汇的情况。
7. 实测:小型集群中的 Argo CD
2026-10-17,仅使用了读取命令。不记录应用名称和存储库地址。
| 项目 | 读取值 |
|---|---|
| 组件 | application-controller(StatefulSet 1)、repo-server、server、applicationset-controller、notifications-controller、redis、dex — 全部为 v3.5.1(dex 和 redis 除外) |
| repo-server 重启 | 4 次(其余为 0) |
| Application | 5 个,全部属于 Synced·Healthy |
status.resourceHealthSource |
全部 5 个 appTree — 延续 4.3 节的“资源状态保存在 redis 的资源树中” |
argocd-cm 的 resource.customizations.* 键 |
ignoreResourceUpdates.* 只有 8 个,没有 health.* → CronJob 由内置 Lua 判定 |
Application 的 status.resources 中有 44 个 CronJob,其 health 栏全部为空(因为上述的 appTree 原因)。因此,读取了 44 个 CronJob 的实际状态字段
(spec.suspend, status.lastScheduleTime, lastSuccessfulTime, active),并 用 Python 实现了 4.4 节表格中的规则 进行分类。输入为实际测量值,判定则是基于 Lua 转换后的计算结果。
| 所处分支 | 数量 | 判定 |
|---|---|---|
已停止(suspend: true) |
27 | Healthy |
| 当前 Job 正在运行 | 3 | Healthy |
| 最后一次执行成功 | 14 | Healthy |
| 最后一次执行失败(无正在运行的 Job) | 0 | Degraded |
在 44 个 CronJob 中,有 27 个是已停止的 CronJob。这意味着按文档所述本应在 UI 中显示为 Suspended 的内容全部混入了 Healthy 当中, 通过“筛选 Suspended”一个也找不到。这种分类方式直观地展现了 Issue #30243 中第 3 点的实际数字。
8. 从哪里开始阅读
resource_customizations/batch/CronJob/— 56 行 Lua 与测试数据。这是最轻量的入口。util/lua/lua.go:169GetHealthScript→gitops-engine/pkg/health/health.go:70.controller/appcontroller.go:1775processAppRefreshQueueItem— 完整的一次刷新过程。controller/state.go:633CompareAppState— 虽然很长,但汇集了比较所需的所有素材。- 对于 Workflows,掌握了
workflow/controller/operator.go:212operate和workflowpod.go:341createWorkflowPod这两个地方就能建立整体概念。 - 最后是
workflow/artifacts/artifacts.go:29— 选择一个驱动(s3)并追踪到底。
9. 适合首次贡献的地方
resource_customizations添加 Lua 与测试。 许多 CRD 尚无状态判定。health.lua+health_test.yaml+testdata/*.yaml只需满足这三点, 且无需运行全部测试,只需通过go test ./util/lua/即可验证(4.3)。- 整理 CronJob 文档(#30243)。 使文档与 4.4 节的表格保持一致,同时介绍覆盖方法(
resource.customizations.health.batch_CronJob)以及ignore-healthcheck注释。 如果维护者接受该 Issue,便可直接推进 PR。 - Lua 的陈旧语法。 8 处内置 Lua 使用了
table.getn(包含 CronJob 的第 27 行)。由于执行器支持,目前尚无问题,但 将其整理为#长度运算符的范围很小。 不过,必须同时提交行为一致的测试才能被采纳。 - Workflows 文档。 4.1.5 中无 init 的 supervisor 模式在代码中有很好的注释,但在用户文档中很难找到此配置(本次未下载文档目录,未核实)。
- 规则:Argo 项目有生成式 AI 政策(argoproj/argoproj
community/genai.md),Argo Workflows 的 PR 模板中包含声明 AI 使用情况的### AI章节。两个存储库都要求 DCO 签名(Signed-off-by), 并且 Argo CD 带有 PR 标题格式检查(.github/pr-title-checker-config.json)以及“没有测试的 PR 很难被合并”的检查清单项。
10. 局限性
- 由于小型集群中没有部署 Argo Workflows,因此没有实测数据。supervisor 模式、制品插件和归档(persist/)仅通过浏览代码了解。
- 未涉及 Argo CD 的分片(Sharding)、ApplicationSet 生成器、通知、服务端 diff 以及 hydrator(commitserver)。
- CronJob 分类是基于 Lua 转换的计算结果。并未通过 Argo CD API 读取资源树的实际判定(因需要登录而未执行,未实测)。
蒂蒂有话说。 如果只在正在休息的选手身上写着“我很健康!”,那么当教练问“谁在休息?”时,一个人也点不出来呀!明明有 27 个人在休息呢!
参考
所有访问日期均为 2026-10-17。
- argoproj/argo-cd v3.5.1:
controller/,gitops-engine/pkg/,reposerver/repository/repository.go,util/lua/lua.go,resource_customizations/batch/CronJob/health.lua,docs/operator-manual/health.md - argoproj/argo-workflows v4.1.5:
workflow/controller/,workflow/executor/executor.go,workflow/artifacts/artifacts.go,workflow/artifactrepositories/artifactrepositories.go - 公开 Issue/PR:argoproj/argo-cd#30209·#30212·#30243·#24428·#24429,argoproj/argo-workflows#17246
- 引用的代码片段均来自上述存储库,且全部遵循 Apache License 2.0 协议。在文章中经过了简写处理。