改一行程式碼,十幾個應用全部重打映像:MonoRepo 的慢,問題多半不在倉庫本身
一套財稅系統採 MonoRepo 管理十餘個應用,每次推送 main 分支就全量重建所有映像,本文從受影響構建與快取機制說明慢的根源與業界標準解法。
(本文回顧 2026 年 9 月初 V2EX 上的一則熱門討論串。)一位開發者在論壇上描述了他的困境:他為一套財稅系統採用 MonoRepo 架構,apps 目錄下放著十幾個應用,前端用 Next.js,後端用 FastAPI,程式碼放在 GitHub,映像則交由阿裏雲 ACR 構建。單獨構建任何一個應用只要一到兩分鐘,但每次推送到 main 分支,所有應用都會被重新構建一遍,而且應用數量還在增加。
這則貼文會被熱議,因為它踩中的是幾乎每個採用 MonoRepo 的團隊都會撞上的牆:代碼管理變簡單了,構建時間卻失控了。
先理解 MonoRepo 是在解決什麼問題
用一個生活比喻來說明。假設一棟大樓裡住著十幾戶人家,各自有各自的房間。Polyrepo 的做法是每戶人家各自買地蓋房子,產權清楚,但要借一罐醬油得跑三條街。MonoRepo 則是大家住同一棟樓,共用大廳與倉庫,互相借用東西方便,管理上卻需要一套規則,否則一戶裝修,整棟樓都停電。
這位開發者的痛點正在於此:明明只改了一戶的燈泡,物管卻把整棟樓所有房間都重新檢查了一遍。技術上,這叫「全量構建」。每次 main 分支有新提交,CI 系統不知道哪些東西變了,於是最保險的做法就是全部重打。十一個應用、每個一到兩分鐘,串行下來就是十幾分鐘起跳,隨著應用增加只會更糟。
慢的根源:不是倉庫太大,是構建系統不認識依賴關係
討論串裡有一個被反覆確認的結論值得單獨拿出來講:不要拆倉庫。Google、Meta、Shopify 這類公司內部都是巨型單一倉庫,幾萬個專案共存,但它們的構建系統從來不是「全倉重打」,而是沿著依賴圖找出真正受影響的專案。
什麼是依賴圖?再換個比喻。一道套餐裡有前菜、主餐、甜點,甜點的食譜引用了主餐的醬汁配方。如果廚師只改了醬汁配方,那麼主餐要重做,甜點也要重做,但前菜完全不用動。構建系統需要的就是這份「誰用了誰」的地圖。改了 apps/fis-api,那就重建它,以及依賴它的 apps/fis-open-api;改了某個共用的 packages/* 底層套件,才需要動到引用它的所有前端。
這套做法在業界有個名字:受影響構建,Nx、Turborepo 這類工具的核心價值就在於自動算出這份名單。對小團隊而言,其實不一定需要上這麼完整的工具鏈,用 GitHub Actions 的 paths-filter,根據哪些檔案路徑變動來決定觸發哪些構建任務,再配一份手寫的依賴對照表,就能解決八成的問題。
快取:被低估的另一隻手
討論串後段有一段被原提問者貼出的 AI 建議,其中一點很有價值:快取往往比縮小構建範圍更劃算。
Docker 映像的構建是分層的。如果 Dockerfile 先安裝依賴、再複製原始碼,那麼只要依賴清單沒變,這一層就能直接命中快取,跳過最耗時的套件安裝。BuildKit 支援把這些中間層快取推到映像倉庫,下次構建時先比對,沒變的部分幾秒鐘就帶過。換句話說,就算偶爾全量構建跑起來,只要分層寫得好、快取接得上,大部分應用也只是「走個過場」,真正的成本集中在被改動的那一兩個。
對正在做類似選型的團隊,可以帶走的三個判斷
第一,應用數量超過五個、還在成長的 MonoRepo,就應該開始規劃按需構建。等到構建時間突破二十分鐘才處理,遷移的痛苦會大得多。
第二,構建觸發邏輯值得當成基礎設施來維護,而非一次性腳本。commit message 規範、路徑過濾規則、依賴對照表,這些都會隨著應用增加而演進,放在獨立的 CI 設定倉裡集中管理,比散落在各應用目錄裡容易照顧。
第三,映像倉庫的觸發規則要自己掌握。把 ACR 當成單純的儲存與分發層,把「改了什麼、構建什麼」的判斷留在 CI 端,彈性最大。原提問者卡住的點,某種程度上正是把構建決策權交給了映像服務的預設行為。
回頭看這則貼文,它表面上是一個技術配置問題,實際上是一堂架構課:把東西放在一起管理,代價是你必須教會機器分辨「什麼真正變了」。做到這一點,MonoRepo 的規模紅利才拿得到;做不到,應用每多一個,團隊就多等兩分鐘。
主題