深入拆解:從 redis-app.yaml 注冊(cè)到緩存重建的完整鏈路)
在給 Redis 部署做 GitOps 規(guī)范化的時(shí)候我把redis-deployment倉(cāng)庫(kù)推上去之后突然意識(shí)到一個(gè)問(wèn)題ArgoCD 到底是怎么知道這個(gè)倉(cāng)庫(kù)有改動(dòng)的我的第一反應(yīng)是ArgoCD 輪詢(xún) Git 倉(cāng)庫(kù)比對(duì) hash但仔細(xì)一想又不對(duì)——我明明有兩個(gè)倉(cāng)庫(kù)在參與這件事my-argocd-manifests管 Application 定義和redis-deployment管實(shí)際 Manifest。那 ArgoCD 到底輪詢(xún)哪一個(gè)這篇文章把我從懷疑到查源碼、再到上集群實(shí)證的完整過(guò)程記錄下來(lái)。結(jié)論可能會(huì)顛覆你對(duì) ArgoCD 的直覺(jué)ArgoCD 不是輪詢(xún)一個(gè)倉(cāng)庫(kù)而是兩層各自獨(dú)立的輪詢(xún)循環(huán)它也不是靠保存 hash 對(duì)比來(lái)檢測(cè)改動(dòng)Git 實(shí)時(shí)生成才是真相。一、 背景我的 ArgoCD 到底長(zhǎng)什么樣先交代我的實(shí)際環(huán)境后面所有例子都是真實(shí)資源ArgoCD 控制面阿里云 K3s 集群App-of-Apps 模式root-bootstrapApplication 管著my-argocd-manifests.git的argocd-apps/目錄目標(biāo)集群tencent-dp1-cluster橫跨騰訊云vm-0-2-debian控制面、OCI ARMfree-arm-vm、本地 NUC 三個(gè)節(jié)點(diǎn)子應(yīng)用fastapi-svc、quarkus-svc、kong-gateway-infra、kong-ingress-controller、gateway-api-crds共 5 個(gè)待接入redis-deployment.gitk8s/redis.yaml打算把 Redis 部署到 free-arm-vm經(jīng) Kong Gateway 6379 Stream 對(duì)外我當(dāng)時(shí)給 README 寫(xiě)ArgoCD 輪詢(xún)的是每個(gè) Application 自己的 source.repoURL不是統(tǒng)一倉(cāng)庫(kù)這句話(huà)時(shí)被自己?jiǎn)栕×四莚oot-bootstrap輪詢(xún)my-argocd-manifests又算什么兩個(gè)倉(cāng)庫(kù)都會(huì)被輪詢(xún)嗎還是有一個(gè)是假的二、 雙層輪詢(xún)發(fā)現(xiàn)層與同步層各干各的答案是兩個(gè)倉(cāng)庫(kù)都會(huì)被輪詢(xún)但輪詢(xún)的主體不同職責(zé)完全不同。這是 ArgoCD App-of-Apps 模式最核心、也最容易搞混的機(jī)制。第一層root-bootstrap發(fā)現(xiàn)層—— 輪詢(xún) my-argocd-manifests# argocd-apps/root-bootstrap-app.yamlspec:source:repoURL:https://github.com/nvd11/my-argocd-manifests.gittargetRevision:HEADpath:argocd-apps它的職責(zé)只有一件事盯住argocd-apps/目錄看里面有沒(méi)有新的 Application 文件。我往里加一個(gè)redis-app.yamlroot-bootstrap 下一輪輪詢(xún)發(fā)現(xiàn)新文件就在集群里創(chuàng)建一個(gè)redis的 Application 對(duì)象。注意這一層不解析、不連接、不驗(yàn)證 redis-deployment.git。repoURL 對(duì)它來(lái)說(shuō)只是 Application 對(duì)象里的一個(gè)字段它照抄進(jìn)對(duì)象里就完事。第二層子 Application同步層—— 輪詢(xún)各自 source.repoURL# 我 README 里規(guī)劃的 redis-app.yaml示意spec:source:repoURL:https://github.com/nvd11/redis-deployment.gitpath:k8s一旦redisApplication 對(duì)象被創(chuàng)建application-controller 就為它單獨(dú)開(kāi)一個(gè)輪詢(xún)循環(huán)去輪詢(xún) redis-deployment.git 的k8s/目錄把 Deployment/PVC/Service/TCPIngress 同步到目標(biāo)集群。我現(xiàn)有的fastapi-svc-app.yaml就是活證據(jù)——它的source.repoURL指向my-shared-helm-charts.git而不是my-argocd-manifests。如果 ArgoCD 只輪詢(xún) my-argocd-manifestsfastapi 的鏡像永遠(yuǎn)不可能被同步。目標(biāo)集群 tencent-dp1-clusterArgoCD 控制面 阿里云 K3sGit: redis-deployment.gitGit: my-argocd-manifests.git第一層輪詢(xún) 180s發(fā)現(xiàn)新 Application 文件創(chuàng)建/更新對(duì)象第二層輪詢(xún) 180s檢測(cè) k8s/ 變更自動(dòng) syncargocd-apps/ 目錄Application 定義文件k8s/ 目錄實(shí)際 Manifestroot-bootstrapApplicationredisApplication 對(duì)象Redis Podfree-arm-vm兩個(gè)關(guān)鍵結(jié)論兩個(gè)循環(huán)完全解耦不是父級(jí)掃完通知子級(jí)的串行。父級(jí)只管 Application 對(duì)象的增刪改子級(jí)只管資源同步。改redis-app.yaml的 syncPolicy → 父級(jí)發(fā)現(xiàn)改k8s/redis.yaml的鏡像 → 父級(jí)完全無(wú)感是子級(jí)自己的事。首次部署 Redis 最壞要等兩個(gè) 3 分鐘父級(jí) 3 分鐘撿到新文件創(chuàng)建對(duì)象子級(jí)對(duì)象從創(chuàng)建那一刻才開(kāi)始自己的輪詢(xún)又是最多 3 分鐘。所以第一次部署從 push 到 Redis 起來(lái)最壞 ~6 分鐘。三、 子級(jí)輪詢(xún)?cè)趺粗?repo 改了—— 先 ls-remote再?zèng)Q定要不要重新生成這是我最開(kāi)始搞混的地方。我一直以為 ArgoCD 把 repo 的 hash 存下來(lái)每次輪詢(xún)比對(duì) hash 變了沒(méi)。查了源碼之后發(fā)現(xiàn)機(jī)制比這精細(xì)而且hash 對(duì)比不是你想的那回事。第一步git ls-remote輕量探測(cè)遠(yuǎn)程 HEAD每次輪詢(xún)默認(rèn) 180srepo-server 對(duì)遠(yuǎn)程倉(cāng)庫(kù)執(zhí)行g(shù)it ls-remote HEAD。注意這不是 clone是只讀一次遠(yuǎn)程 ref開(kāi)銷(xiāo) KB 級(jí)。源碼util/git/git.go_,errclient.LsRemote(HEAD)第二步拿遠(yuǎn)程 SHA 對(duì)比緩存repo-server 的 manifest 緩存Rediskey 里帶著TargetRevisionTTL 默認(rèn) 3 分鐘ARGOCD_RECONCILIATION_TIMEOUT。比對(duì)邏輯遠(yuǎn)程 HEAD SHA 緩存里的 SHA ? ├─ 相同 → 直接用緩存的 manifest不重新生成省 CPU/網(wǎng)絡(luò) └─ 不同 → fetch 重新生成 manifest → 更新緩存第三步Application 對(duì)象持久記錄 revisionhash 不只活在緩存里每次 sync 之后還會(huì)寫(xiě)進(jìn) Application 對(duì)象狀態(tài)kubectl get app redis-ojsonpath{.status.sync.revision}這是 UI 上 “Synced to xxxx” 的數(shù)據(jù)來(lái)源也相當(dāng)于上次同步到哪個(gè) commit的錨點(diǎn)。但真相是輪詢(xún)本身是無(wú)條件的我差點(diǎn)被自己的提問(wèn)帶偏——ArgoCD 不是發(fā)現(xiàn) hash 變了才去處理而是每 180s 無(wú)條件觸發(fā)一次 reconcile 流程ls-remote 只是流程里的第一步。hash 對(duì)比的作用是決定要不要重新生成 manifest而不是決定要不要輪詢(xún)。每 180s → ls-remote 拿遠(yuǎn)程 SHA → 對(duì)比緩存 SHA ├─ 相同 → 復(fù)用緩存 manifest → 和集群 live state 對(duì)比 └─ 不同 → fetch 重新生成 manifest → 和集群 live state 對(duì)比 最終: diff 非空 automated → 觸發(fā) sync → 更新 .status.sync.revision這順便解釋了為什么改 README 不觸發(fā)部署SHA 變了有提交重新生成 manifest但 diff 為空README 不影響 k8s/ 產(chǎn)物所以不 sync。邏輯閉環(huán)。四、 緩存到底存在哪—— 三層物理位置各不相同繼續(xù)往下挖問(wèn)題變成緩存在哪。答案不是一個(gè)地方是三層1. Git 倉(cāng)庫(kù)本地 clone —— repo-server 容器 /tmp這是最關(guān)鍵的一層也是檢測(cè) diff的真正基準(zhǔn)。每個(gè)被引用的 repo 在 repo-server 容器里有一份 clone路徑由 URL 消毒而來(lái)// util/git/client.goroot:filepath.Join(os.TempDir(),r.ReplaceAllString(normalizedGitURL,_))// 例: /tmp/https_github.com_nvd11_redis-deployment.git我上集群實(shí)證過(guò)后面會(huì)詳細(xì)講 repo-server 在哪它的/tmp掛的是emptyDirvolumes:-name:tmpemptyDir:{}volumeMounts:-name:tmpmountPath:/tmp2. Manifest 生成緩存 —— Redisrevision → manifest 生成結(jié)果緩存在 ArgoCD 自帶的 Redis 里TTL 3 分鐘。3. revision 錨點(diǎn) —— K8s etcd.status.sync.revision存在 Application 對(duì)象里底層是 etcd跨重啟持久。K8s etcdargocd-redis Podrepo-server Podaws-moon-proxy 節(jié)點(diǎn)生成 manifest 的基準(zhǔn)diff 對(duì)比/tmp emptyDirGit clone 緩存例: /tmp/https_github.com_nvd11_xxx.gitManifest 緩存key 含 TargetRevisionTTL 3minApplication.status.sync.revision上次成功 sync 的 commit SHA五、 緩存丟了怎么辦—— 自動(dòng)重建檢測(cè)零損失這是我問(wèn)自己的第二個(gè)問(wèn)題緩存要是丟了是不是就 detect 不了 diff 了結(jié)論很干脆不會(huì)。緩存是性能優(yōu)化不是正確性依賴(lài)。Git 是唯一事實(shí)源。源碼util/git/client.go的Init()寫(xiě)得很直白func(m*nativeGitClient)Init()error{_,err:git.PlainOpen(m.root)iferrnil{returnnil}// clone 在 → 直接用if!errors.Is(err,git.ErrRepositoryNotExists){returnerr}log.Infof(Initializing %s to %s,m.repoURL,m.root)erros.RemoveAll(m.root)// 殘留清掉erros.MkdirAll(m.root,0o755)// 重建目錄repo,err:git.PlainInit(m.root,false)// 重新 git init...}加上checkoutRevision的完整鏈路reposerver/repository/repository.go輪詢(xún)觸發(fā) → gitClient.Init() ├─ clone 在 → 直接復(fù)用 └─ clone 丟 → PlainInit 重建空倉(cāng)庫(kù) → IsRevisionPresent(revision)? 否 → gitClient.Fetch() ← 從遠(yuǎn)程重新拉 → gitClient.Checkout(revision) ← checkout 目標(biāo) revision → CommitSHA() 拿當(dāng)前 commit hash緩存層丟了會(huì)怎樣恢復(fù)方式Git 本地 clone/tmp不影響自動(dòng)重建Init()→Fetch()→Checkout()Redis manifest 緩存不影響重新生成每次 reconcile 實(shí)時(shí)生成.status.sync.revision不影響檢測(cè)只影響 UI 顯示下次 sync 自動(dòng)更新就算把 repo-server 的 /tmp 和 Redis 全清了它頂多慢幾秒重新 clone檢測(cè)照樣精確。Git 永遠(yuǎn)可以被重新拉取這就是 GitOps “Git 為源 緩存可棄” 的可靠性根基。六、 repo-server 到底在哪—— 它是獨(dú)立 Pod不在ArgoCD 主節(jié)點(diǎn)上這個(gè)問(wèn)題我也踩了認(rèn)知坑。我一直默認(rèn) repo-server 跟 ArgoCD 控制器在一起直到 SSH 上阿里云 master 查了一下$ kubectl get pods-nargocd-owide NAME READY NODE argocd-application-controller-01/1 free-amd-vm argocd-repo-server-797fc85c8f-x6csk1/1 aws-moon-proxy ← 在這 argocd-redis-9dbc65c5c-lcb281/1 aws-moon-proxy argocd-server-58d6c7fbb9-sg9xt1/1 free-amd-vm2ArgoCD 是一組微服務(wù)不是單個(gè)進(jìn)程。repo-server 是獨(dú)立 Deployment負(fù)責(zé)所有 Git 操作clone/fetch/生成 manifest通過(guò) gRPC 被 application-controller 調(diào)用。它可以調(diào)度到任何節(jié)點(diǎn)——我集群里它就跑在aws-moon-proxyAWS 節(jié)點(diǎn)上。而且它徹底無(wú)狀態(tài)/tmp掛 emptyDirPod 重建 clone 全清 自動(dòng)重建。官方部署不掛 PV就是因?yàn)榫彺娌恢档贸志没?。七?從推代碼到 Redis 起來(lái)完整時(shí)序把前面所有機(jī)制串起來(lái)一次完整的 GitOps 部署是這樣的以 redis-deployment 為例目標(biāo)集群repo-serverredis Applicationroot-bootstrapredis-deployment.gitmy-argocd-manifests.git開(kāi)發(fā)者目標(biāo)集群repo-serverredis Applicationroot-bootstrapredis-deployment.gitmy-argocd-manifests.git開(kāi)發(fā)者對(duì)象創(chuàng)建后才有自己的輪詢(xún)循環(huán)push redis-app.yaml輪詢(xún) 180s發(fā)現(xiàn)新文件創(chuàng)建 redis Application 對(duì)象觸發(fā) reconcilegit ls-remote HEAD遠(yuǎn)程 SHASHA 對(duì)比緩存fetch checkout (緩存缺失時(shí))生成 manifestdiff 非空 → 自動(dòng) syncRedis Pod 運(yùn)行在 free-arm-vm八、 總結(jié)這套機(jī)制給工程實(shí)踐帶來(lái)的三條啟示App-of-Apps 的兩層別搞混父級(jí)管應(yīng)用是否存在App 定義層子級(jí)管資源是否一致Manifest 層。repoURL 是父子之間的橋梁字段但兩者各自獨(dú)立輪詢(xún)、互不阻塞。加文件 加應(yīng)用刪文件 prune 刪應(yīng)用不需要任何 UI 注冊(cè)。輪詢(xún)與緩存是兩回事輪詢(xún)是每 180s 無(wú)條件觸發(fā)的 reconcilels-remote 只是第一步緩存/tmp clone、Redis、etcd revision只決定要不要重新生成 manifest不影響要不要檢測(cè)。所以緩存丟了檢測(cè)能力零損失。Git 是唯一事實(shí)源其他都可棄repo-server 無(wú)狀態(tài)、emptyDir、自動(dòng)重建這套設(shè)計(jì)讓 ArgoCD 在任何節(jié)點(diǎn)故障/緩存清空后都能自愈。這也是為什么 GitOps 敢把部署交給一個(gè)會(huì)忘事的進(jìn)程——它忘了沒(méi)關(guān)系Git 記得一切。題外話(huà)本文從ArgoCD 到底輪詢(xún)哪個(gè) repo這個(gè)看似簡(jiǎn)單的問(wèn)題出發(fā)最后查到了util/git/client.go的Init()源碼。這種問(wèn)題 → 懷疑 → 查源碼 → 上集群實(shí)證的路徑比直接看文檔收獲大得多。文檔只告訴你它會(huì)自動(dòng)同步源碼才告訴你它憑什么敢自動(dòng)同步。