TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

Argo CD·Argo Workflows 读后感:对比循环、Pod 中的 Worker,以及单个 CronJob 的状态判定

状态:草稿(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”

虽然名字里都有 Argo,但这两个项目的性质有所不同。

组件 职责 运行位置
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 的对比循环

Argo CD: git 의 원하는 상태와 클러스터를 계속 비교한다 Git 저장소 원하는 상태 repo-server helm·kustomize·plain 렌더 → 매니페스트 결과는 redis 에 캐시 application-controller 큐 두 개: 새로 고침 · 작업 ① 새로 고침 큐 다시 볼 때인가 ② 비교 CompareAppState ③ 상태 판정 자원별 Lua·Go Synced/OutOfSync · Healthy/Degraded 를 Application 에 기록 ④ operation 큐 자동·수동 sync 요청 ⑤ SyncAppState 웨이브·훅 순서로 적용 비교·적용의 핵심은 저장소 안 gitops-engine 이 맡는다 클러스터 캐시 모든 자원을 watch 로 유지 쿠버네티스 API 대상 클러스터(여럿 가능) ③ 의 순서: 먼저 찾은 것 하나만 쓴다 1 argocd-cm 의 resource.customizations.health.{그룹_종류} 2 argocd-cm 의 와일드카드 항목 3 내장 Lua: resource_customizations/{그룹}/{종류}/health.lua 4 내장 Lua 와일드카드 → 5 Go 로 짠 내장 검사(Deployment 등)
图:Argo CD 的组件及对比、状态判定、sync 循环,以及选择状态判定脚本的顺序

图解说明:

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)。

  1. 刷新 Worker 是 appcontroller.go:1775 processAppRefreshQueueItem 。 appcontroller.go:2105 needRefreshAppStatus 首先通过 argocd_application_controller.go:45询问“现在真的是重新对比的时候吗”。如果用户点击了刷新、管理的资源发生了变化,或者设定的周期(默认 120 秒,带有最大 60 秒的抖动,位于
  2. )已过,就会进行对比。 controller/state.go:633 CompareAppState 对比是由 state.go:206 GetRepoObjs 执行的。该函数通过 gRPC 从 repo-server 获取期望状态(位于 controller/cache/cache.go:729 GetManagedLiveObjs ),从缓存中取出实际状态(位于 pkg/diff/diff.go:76 Diff ),然后通过 gitops-engine 的
  3. 对每个资源进行对比。state.go:1051 在同一个函数的末尾会进行状态判定(在 setApplicationHealth 中调用
  4. )。我们将在 4.3 节中详细探讨这一点。如果开启了自动 sync, appcontroller.go:2322 autoSync 会将 operation 写入 Application,然后第二个队列就会处理该应用。

4.2 渲染与应用:repo-server 与 sync

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

Argo Workflows: 컨트롤러는 파드를 만들고, 파드가 일한다 Workflow CR(템플릿 묶음) workflow-controller · operate() 노드 상태 머신: steps·DAG·컨테이너 createWorkflowPod 단계 하나 = 파드 하나 init argoexec 입력 아티팩트 불러오기 main 사용자 이미지 emissary 가 감싸 실행 wait argoexec 출력 아티팩트 저장·결과 보고 아티팩트 저장소 — NewDriver 가 종류로 고른다 S3(MinIO 호환) · GCS · Azure · OSS · HDFS HTTP · Artifactory · Git · Raw · Plugin 어느 저장소를 쓰나(artifactRepositoryRef 해석) 지정함: 워크플로 네임스페이스의 ConfigMap → 컨트롤러 쪽 지정 안 함: 네임스페이스 기본 → 컨트롤러 설정의 기본
图:Argo Workflows 的控制器、步骤 Pod、制品存储
  1. workflow/controller/controller.go:337 Run 启动了 informer 和 Worker,而 controller.go:922 processNextItem 取出一个 Workflow 并创建 operator。
  2. workflow/controller/operator.go:212 operate 是单次 reconcile。首先通过 operator.go:1216 podReconciliation 将已启动 Pod 的状态反映到节点状态中,然后通过 operator.go:2235 executeTemplate 从入口模板开始向下遍历,决定下一步要做什么。根据模板类型的不同,会分叉到 steps.go:42 executeSteps, dag.go:278 executeDAG, operator.go:3435 executeContainer 。由于每次都从头开始遍历但会跳过已完成的节点,因此即使控制器重启,只要有 Workflow 的 status.nodes 就能继续执行。
  3. 容器步骤由 workflowpod.go:341 createWorkflowPod 创建为 Pod。在用户容器 main 旁边附加了 init(workflowpod.go:1092 newInitContainers)和 wait ,并将 main 的命令用 argoexec emissary 包裹起来(位于cmd/argoexec/commands/emissary.go:46)。v4 中引入了无需 init 容器、 supervisor 单个容器同时处理 init 和 wait 工作的模式 (workflowpod.go:1127 newSupervisorContainer, initlessPod 配置)。
  4. 在 Pod 内部, wait 会等待 main 结束,然后将输出文件上传至存储库(通过workflow/executor/executor.go:1355 Wait), executor.go:449 SaveArtifacts )。
  5. 存储驱动由 workflow/artifacts/artifacts.go:29 NewDriver 根据制品中指定的类型进行选择。 S3 排在最前面(第 38 行),访问密钥即时从 Secret 中读取并填充。MinIO 作为 S3 驱动接入。
  6. 如果在制品中未指定存储库, workflow/artifactrepositories/artifactrepositories.go:38 Resolve 会依次查找 工作流命名空间的默认 ConfigMap,然后是控制器配置中的默认存储库。

为什么要这样拆分? 集群中只有一个控制器,但阶段(Step)可能有数千个。如果繁重的文件传输不通过控制器处理,而是交由阶段 Pod 内的 argoexec 完成, 则即使传输量增加,也只需扩展阶段 Pod。不过代价是每个 Pod 都会附带一个辅助容器,v4 的 supervisor 模式被认为是一种旨在降低该成本的尝试(设计意图未通过文档确认,未核实)。

6. 作者的贡献与后续动向

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. 从哪里开始阅读

  1. resource_customizations/batch/CronJob/ — 56 行 Lua 与测试数据。这是最轻量的入口。
  2. util/lua/lua.go:169 GetHealthScript → gitops-engine/pkg/health/health.go:70.
  3. controller/appcontroller.go:1775 processAppRefreshQueueItem — 完整的一次刷新过程。
  4. controller/state.go:633 CompareAppState — 虽然很长,但汇集了比较所需的所有素材。
  5. 对于 Workflows,掌握了 workflow/controller/operator.go:212 operate 和 workflowpod.go:341 createWorkflowPod 这两个地方就能建立整体概念。
  6. 最后是 workflow/artifacts/artifacts.go:29 — 选择一个驱动(s3)并追踪到底。

9. 适合首次贡献的地方

10. 局限性

蒂蒂有话说。 如果只在正在休息的选手身上写着“我很健康!”,那么当教练问“谁在休息?”时,一个人也点不出来呀!明明有 27 个人在休息呢!

参考

所有访问日期均为 2026-10-17。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论