Git 子模块在项目里的作用,相当于在代码仓库里“挂”了一个指向另一个仓库的链接。好处是能复用公共代码,但当你把这种仓库交给 ArgoCD 去同步的时候,经常会遇到一种让人抓狂的现象:明明只是改了一行配置,整个同步过程却慢得像蜗牛,甚至偶尔还失败。你会怀疑是不是 ArgoCD 出问题了,其实根源很可能就是那个不起眼的子模块引用。

一、问题是怎么一步一步发生的

1.1 子模块的“隐藏成本”

很多人最初用子模块,是想让多个服务共享同一份公共库。比如我有个项目,主仓库叫 app-config,里面需要引用一个叫 common-lib 的公共代码仓库。做法通常是:


# 在 app-config 仓库里添加子模块
git submodule add https://github.com/example/common-lib.git common-lib

# 提交 .gitmodules 文件
git add .gitmodules common-lib
git commit -m "添加 common-lib 子模块"

这样 app-config 仓库里就多了一个“特殊目录”。它并不真正保存 common-lib 的所有文件,只保存一个提交号(commit hash)。当别人克隆 app-config 时,需要执行:


# 克隆主仓库
git clone https://github.com/example/app-config.git

# 初始化并拉取子模块
git submodule init
git submodule update

看着没什么,但对 ArgoCD 来说,问题就来了。ArgoCD 把 Git 仓库当作“事实来源”,它要读取仓库里的文件来生成 Kubernetes 资源。如果仓库里有子模块,ArgoCD 也必须先把子模块内容拉取下来。每次同步,它都会尝试去检查子模块引用的提交号是否发生变化,这会导致额外的网络请求和 Git 操作。

1.2 单仓库多路径是什么

ArgoCD 支持一个仓库里配置多个“应用”,每个应用对应仓库里不同的目录路径。比如我的仓库结构是:


app-config/
├── apps/
│   ├── frontend/
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   └── backend/
│       ├── deployment.yaml
│       └── configmap.yaml
└── common-lib/   # 这是子模块

我可以在 ArgoCD 里创建两个 Application,一个指向 apps/frontend,另一个指向 apps/backend。这样改前端配置只同步前端,看起来效率很高。但如果 common-lib 是子模块,ArgoCD 在扫描整个仓库时,还是要先处理子模块。更麻烦的是,如果子模块更新了,ArgoCD 需要在主仓库的锁文件里看到新的提交号,否则它永远用的旧版本。

二、为什么子模块会让同步效率变差

2.1 频繁的“重新拉取”

ArgoCD 默认会定期刷新 Git 仓库状态,比如每 3 分钟。如果仓库包含子模块,每次刷新 ArgoCD 都要做两件事:

  • 检测主仓库最新的 commit,这很快。
  • 检查子模块引用的 commit 是否有变化,这需要访问子模块的远程服务器。

哪怕子模块没变,这个检查也有成本。如果子模块托管在一个很慢的服务器上,或者网络不稳定,同步就会一直卡着。我自己就遇到过,一个子模块放在 GitHub,而 ArgoCD 跑在内网的私有集群里,每次拉子模块都要走一次外网代理,慢到怀疑人生。

2.2 锁竞争与“卡死”现象

Git 子模块在更新时,会在 .git/modules 目录下创建锁文件。如果 ArgoCD 同时有多个 Application 操作同一个仓库,比如前端和后端分别触发同步,它们可能会同时去更新同一个子模块,然后一个拿到锁,另一个就只能等待。严重的时候直接报错,提示“unable to lock file 'xxx/.git/index.lock'”。这种问题在多路径模式下尤其明显,因为你本来是想“分而治之”,结果变成“互相打架”。

2.3 子模块的“脏状态”问题

有时你会手动进到子模块目录里切换分支或修改文件,导致子模块处于“脏状态”。ArgoCD 检测到主仓库的 commit 没变,但子模块的引用变了,它会尝试强制把子模块重置到正确的 commit。如果失败,同步直接失败。你去看日志,里面全是子模块相关的错误。

三、ArgoCD 单仓库多路径的优势

在说解决方案之前,得先搞清楚为什么我们要用单仓库多路径。它在很多场景下其实非常香:

  • 配置集中管理,一个仓库搞定所有环境的所有应用。
  • 代码评审容易,改动都在一起。
  • 权限控制简单,给仓库配权限就行,不用每个应用单独配。

多路径模式可以这样配置:在 argocd-cm ConfigMap 里不需要额外配置,只要在 Application 的 spec.source.path 里写不同的目录即可。


# 示例:backend 应用的 Application 配置
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/app-config.git
    targetRevision: main
    path: apps/backend   # 只关注这个子目录
  destination:
    server: https://kubernetes.default.svc
    namespace: backend

同理还有 frontend 应用。这样做的好处是:如果我只改 apps/backend 下的文件,ArgoCD 只会刷新 backend 的应用,不会去碰 frontend。但前提是仓库里没有子模块,或者子模块不影响这个路径。

四、解决策略:去掉子模块,用“构建时拉取”或“源码替换”

既然子模块这么麻烦,最简单的方案就是“别用子模块”。但公共代码总要共享,我们可以换几种方式。

4.1 策略一:把公共库内容直接并入主仓库

如果公共库不是特别大,更新频率不高,直接把公共代码拷贝到主仓库的某个目录里。比如,把 common-lib 的内容放到 shared/ 目录下,然后用软链接或配置引用。这样主仓库变成一个完整的“自包含”仓库,ArgoCD 同步时不需要访问任何外部依赖,速度飞快。缺点是公共库的更新需要手动同步到每个主仓库,容易造成版本不一致。

4.2 策略二:使用 ArgoCD 的 Repository 与 “比较差异” 能力

其实 ArgoCD 本身支持多个仓库。你完全可以不把公共库作为子模块,而是把公共库单独作为一个 Git 仓库,然后在 Application 里通过 helm 或者 kustomize 引用它。比如我把公共的 YAML 模板放在另一个仓库,然后在主仓库里用 Kustomize 的 remote 功能?Kustomize 原生不支持远程,但 ArgoCD 可以通过 plugin 或者 sidecar 实现。

更简单的方式是:使用“构建时拉取”,也就是在 CI 阶段把公共库的内容拉下来,和主仓库内容合并,然后再提交到一个专门的部署仓库。ArgoCD 只监控那个部署仓库,不监控源码仓库。这样 ArgoCD 完全不知道子模块的存在。

4.3 策略三:如果坚持用子模块,那就调整 ArgoCD 参数

有些人因为历史原因改不了子模块结构,那我们可以从 ArgoCD 侧做优化。ArgoCD 有一个关键参数叫 timeoutretry 策略。默认同步超时是 180 秒,如果子模块拉取慢,可以适当调大。另外,如果子模块仓库和主仓库是同一个 Git 服务器,尽量保证网络好。

更关键的是,尽量让 ArgoCD 使用 Cron 定时同步,而不是 Webhook 触发。Webhook 触发会让每一次代码提交都立刻触发同步,如果子模块拉取慢,就会积压一堆同步请求,然后互相锁。可以改成:


# 关闭 webhook 事件,设置默认 5 分钟检查一次
kubectl edit configmap argocd-cm -n argocd

在 data 里加:


data:
  # 禁止自动 webhook 通知(取决于你的 Git 服务端设置)
  resource.customizations.health.argoproj.io_Application: |
    ...

其实具体参数是 argocd-cm 里的 repository.credentialssourceRepositories,但这些并不直接影响子模块。真正有用的是在 Application 里增加 syncPolicy.retry


apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-app
spec:
  syncPolicy:
    retry:
      limit: 5   # 重试 5 次
      backoff:
        duration: 20s
        factor: 2
        maxDuration: 300s

这样就算子模块偶尔拉失败,ArgoCD 也会重试,不会直接挂掉。

4.4 策略四:使用 Git 子模块的 “相对URL” 和 “浅克隆”

如果你必须使用子模块,可以尝试优化子模块本身的拉取效率。

  • 使用相对 URL,这样 ArgoCD 在克隆时能自动匹配子模块的协议,避免跨协议访问。
  • 让子模块仓库支持“浅克隆”。但 ArgoCD 对子模块的拉取默认使用完整克隆,除非你在主仓库里配置 submodule.<name>.shallow = true。不过 ArgoCD 不一定支持这个配置,需要测试。

更有效的办法是:把子模块从一个大的仓库换成只包含必要文件的小仓库。很多公共库仓库很大,但业务只用到其中两个文件,子模块却要把整个仓库拉下来。如果能在子模块里只留下必需文件,同步会快很多。

五、详细示例:一个完整的 ArgoCD 单仓库多路径应用

我们用一个具体的例子,把整个流程串起来。假设我们要部署一个电商应用,分 frontendbackend,公共配置放在 shared 目录,但不用子模块。我们直接把公共文件复制到主仓库。

5.1 主仓库结构


# 项目目录结构
ecommerce-config/
├── apps/
│   ├── frontend/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── kustomization.yaml
│   └── backend/
│       ├── deployment.yaml
│       ├── service.yaml
│       └── kustomization.yaml
└── shared/
    ├── configmap.yaml
    └── common-env.yaml

5.2 制作 Kustomize 基础配置

shared 目录里放公共的 ConfigMap 和环境变量。比如:


# shared/common-env.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: common-env
  labels:
    app: ecommerce
data:
  LOG_LEVEL: "info"
  CACHE_SIZE: "256"
  RATE_LIMIT: "1000"

5.3 在前端应用里引入公共配置

apps/frontend/kustomization.yaml 中,使用 resources 引入 shared 目录的文件:


# apps/frontend/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# 直接引用上一级的 shared 目录
resources:
  - ../../shared/common-env.yaml
  - deployment.yaml
  - service.yaml

# 可以覆盖公共配置中的某个字段
patches:
  - target:
      kind: ConfigMap
      name: common-env
    patch: |-
      - op: replace
        path: /data/LOG_LEVEL
        value: "debug"

这样,前端应用就有了公共配置,但主仓库里没有子模块,ArgoCD 同步时可以轻松地递归读取整个目录树,不需要额外网络请求。

5.4 创建 ArgoCD Application

前端应用的 YAML 如下:


# frontend-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/ecommerce-config.git
    targetRevision: main
    # 只监控 frontend 目录,但资源里引用了 shared
    path: apps/frontend
  destination:
    server: https://kubernetes.default.svc
    namespace: frontend
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    retry:
      limit: 3
      backoff:
        duration: 10s
        factor: 2
        maxDuration: 120s

后端应用类似,路径换成 apps/backend。这里的关键是:shared 目录是通过相对路径被 kustomize 引用的,ArgoCD 在渲染时会把 shared 下的文件也读进来,但它不需要把 shared 当作独立 Git 子模块来拉取,速度飞快。

5.5 验证与同步

按顺序执行命令:


# 应用前端配置
kubectl apply -f frontend-application.yaml

# 应用后端配置
kubectl apply -f backend-application.yaml

# 查看同步状态
kubectl get applications -n argocd

如果一切正常,两个应用都会显示 Synced。此时你修改 shared/common-env.yaml 里的某个字段,两个应用都会检测到变化,因为它们都引用了这个文件。不会出现“子模块锁”问题,也不会有额外的 Git 操作。

六、应用场景与优缺点对比

6.1 子模块方案的应用场景

子模块适合那些对公共代码版本非常敏感的场景。比如一个底层库,多个服务必须精确使用同一个 commit,不能有偏差。子模块能保证“主仓库锁住那个 commit,谁来了都是这个版本”。它适合团队小、基础设施访问外部网络稳定、同步频率不高的场景。

但它的缺点也很明显:

  • ArgoCD 同步慢,每次都要拉子模块。
  • 子模块状态容易不一致,导致“漂移”检测困难。
  • 多个 Application 共用同一个子模块时,锁竞争严重。
  • 出于安全考虑,很多内网环境禁止直接访问外网 Git,子模块拉取直接失败。

6.2 单仓库多路径(无子模块)的应用场景

这种方案适合配置管理以“应用为中心”的场景。每个应用有独立目录,公共内容放在 shared 目录,由 Kustomize 或 Helm 引用。优点是:

  • 同步快,不需要额外网络请求。
  • 结构清晰,改动只影响相关应用。
  • 没有锁冲突,因为只有一个仓库,ArgoCD 克隆一次,之后只在本地文件系统中访问。

缺点是:

  • 公共代码重复拷贝或版本混乱(如果不同应用需要不同版本)。
  • 仓库会随着服务数量增大而膨胀。
  • 如果公共代码大量更新,每次所有应用都会触发同步,可能造成“风暴”。

6.3 注意事项

  • 如果你的主仓库很大,ArgoCD 每次刷新都会克隆整个仓库。建议使用 --depth=1 浅克隆来减少数据量。ArgoCD 在 argocd-cm 中可以通过 reposerver.allow.client.cert 等方式指定,但更直接的是在 Application 的 source 里设置 repoURL 时,确保 Git 仓库是专用的配置仓库,不要混合大型二进制文件。
  • 使用 Kustomize 的 resources 引用路径时,注意不能跨仓库,只能在同一个仓库内。这是符合我们策略的。
  • 如果非要跨仓库共享公共库,可以用 ArgoCD 的 “多源应用” 功能(ApplicationSet 或者 multi-source)。在 ArgoCD 2.6+ 中,可以指定多个源:

# 多源应用示例
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: multi-source-app
spec:
  project: default
  sources:
    - repoURL: https://github.com/example/commom-lib.git
      targetRevision: main
      path: base
    - repoURL: https://github.com/example/app-config.git
      targetRevision: main
      path: apps/backend
  destination:
    server: https://kubernetes.default.svc
    namespace: backend

这个功能避免了子模块,同时还能共享公共库,就是配置起来稍微复杂一些。

七、文章总结

Git 子模块本身是个好工具,但它跟 ArgoCD 的“自动同步”模型天生有点冲突。ArgoCD 希望 Git 仓库是自包含的,拉下来就能完整渲染出所有资源。子模块破坏了这种自包含性,导致同步效率低下,甚至因为锁竞争造成故障。

与其想办法优化子模块,不如换个思路:把公共配置直接放进同一个仓库,利用 Kustomize 或 Helm 的相对引用,结合 ArgoCD 的多路径能力,实现既灵活又高效的 GitOps 流程。如果实在需要跨仓库共享,别忘了 ArgoCD 本身支持多个源,这比子模块更可靠。

每个团队都会面临“共享代码”和“构建效率”之间的权衡。我的建议是:如果项目规模不大,优先用单仓库自包含模式;如果规模大了,用多仓库多源,别再碰子模块。让 ArgoCD 少一些“额外的牵挂”,你的同步过程才能快、稳、准。