Monorepo vs Polyrepo:依赖图、构建编排、CI 策略
Monorepo 还是 Polyrepo——这是后端架构的长期争论。这篇是两种布局的真实成本对比,以及怎么用 PlantUML 画出 service / package 依赖图、Bazel/Nx/Turborepo 的构建编排、CI 增量构建策略。
三种仓库布局 Polyrepo(每个 service 一个 repo) 1 2 3 4 git@github.com:org/user-service.git git@github.com:org/order-service.git git@github.com:org/payment-service.git git@github.com:org/shared-lib.git
Monorepo(所有 service 一个 repo) 1 2 3 4 5 6 7 8 9 10 11 git@github.com:org/platform.git ├── services/ │ ├── user/ │ ├── order/ │ └── payment/ ├── libs/ │ ├── common-utils/ │ ├── auth-client/ │ └── db-helper/ └── tools/ └── ci-scripts/
Multi-repo(中间地带,几个相关 service 一个 repo) 1 2 3 4 5 git@github.com:org/checkout-platform.git ├── services/ │ ├── cart/ │ ├── checkout/ │ └── payment/
怎么选
维度
Polyrepo
Monorepo
代码可见性
差——跨 repo 看代码费劲
好——IDE 全文搜
跨 service 改
难——多个 PR 协调
易——单 PR
构建复杂度
简单——各自 build
复杂——依赖编排
CI 速度
简单——只 build 自己
复杂——增量构建
团队自治
高——各自节奏
低——同步节奏
权限管理
细——每个 repo ACL
粗——CODEOWNERS
Git 性能
好——小 repo
差——大 repo git blame 慢
适合规模
<10 service、>200 service
10-100 service
经验法则 :
<10 service → Polyrepo 简单
10-50 service 强耦合 → Monorepo (Google/FB/Meta 模式)
>100 service → Multi-repo(按业务域聚合)
Monorepo 依赖图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @startuml skinparam componentStyle rectangle skinparam defaultTextAlignment center title "Monorepo Service Dependency Graph" package "Frontend" { component "Web App" as web component "Mobile App" as mobile } package "Backend Services" { component "User Service" as user component "Order Service" as order component "Payment Service" as payment component "Inventory Service" as inv component "Notification Service" as notif } package "Shared Libraries" { component "common-utils" as cu component "auth-client" as ac component "db-helper" as db component "logging" as log } package "Platform" { component "API Gateway" as gw component "Config Service" as cfg } ' Frontend 依赖 web --> gw : HTTPS web --> ac : TS SDK mobile --> gw : HTTPS mobile --> ac : TS SDK ' Backend service 依赖 gw --> user : HTTP gw --> order : HTTP gw --> payment : HTTP order --> user : "verify user" order --> inv : "check stock" order --> payment : "charge" payment --> user : "get billing addr" order --> notif : "send email" user --> notif : "send welcome email" ' Shared lib 依赖 user --> cu order --> cu payment --> cu inv --> cu notif --> cu user --> db order --> db payment --> db inv --> db user --> ac gw --> ac ' cfg 被所有 service 依赖,但不画出来避免图过乱 user ..> cfg : config order ..> cfg : config payment ..> cfg : config @enduml
画依赖图的技巧 :
实线 = 强依赖(编译时/直接 import)
虚线 ..> = 弱依赖(配置/网络/间接)
package 分组 = 按业务域
平台组件在底部 ——被多个 service 依赖
检测循环依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import re, pathlib, networkx as nxdeps = {} for service_dir in pathlib.Path("services" ).iterdir(): name = service_dir.name deps[name] = set () puml = (service_dir / "deps.puml" ).read_text() for dep in re.findall(r'(\w+) --> (\w+) :' , puml): if dep[0 ] == name: deps[name].add(dep[1 ]) G = nx.DiGraph(deps) cycles = list (nx.simple_cycles(G)) assert not cycles, f"Circular dependencies detected: {cycles} "
CI 跑这个脚本——有循环依赖直接 fail 。
构建工具:Bazel Bazel 是 Google 内部 Blaze 的开源版,用 BUILD 文件描述依赖 :
1 2 3 4 5 6 7 8 9 10 11 12 13 load("@rules_go//go:def.bzl" , "go_library" ) go_library( name = "user_service" , srcs = glob(["**/*.go" ]), deps = [ "//libs/common-utils:go_default_library" , "//libs/db-helper:go_default_library" , "//libs/auth-client:go_default_library" , ], visibility = ["//visibility:public" ], )
依赖图 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @startuml title "Bazel Dependency Graph (excerpt)" node "services/user\nBazel target" as user_target { component "go_library:\nuser_service" } node "libs/common-utils\nBazel target" as cu_target { component "go_library:\ncommon_utils" } node "libs/db-helper\nBazel target" as db_target { component "go_library:\ndb_helper" } node "libs/auth-client\nBazel target" as ac_target { component "go_library:\nauth_client" } user_target --> cu_target user_target --> db_target user_target --> ac_target @enduml
Bazel 的依赖增量构建 :
1 2 3 4 5 6 7 bazel build //services/user:user_service bazel build //services/user:user_service --remote_cache=https://bazel-cache.company.com
构建工具:Nx (Nx.dev) Nx 是 monorepo 构建工具,适合 JavaScript/TypeScript :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "npmScope" : "@platform" , "tasksRunnerOptions" : { "default" : { "runner" : "@nrwl/workspace/tasks-runners/default" , "options" : { "cacheableOperations" : [ "build" , "test" , "lint" ] , "parallel" : 3 } } } , "implicitDependencies" : { "package.json" : { "scripts" : { "build" : "*" } } } }
1 2 3 4 5 6 7 nx affected:build --base=main --head =HEAD nx build user-service --skip-nx-cache=false
Nx dependency graph 自动生成 :
构建工具:Turborepo Turborepo 是 Vercel 出的,适合 Next.js / Vite 项目 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "pipeline" : { "build" : { "dependsOn" : [ "^build" ] , "outputs" : [ "dist/**" , ".next/**" ] } , "test" : { "dependsOn" : [ "^build" ] , "outputs" : [ "coverage/**" ] } , "lint" : { } , "dev" : { "cache" : false } } }
1 2 3 4 5 6 turbo run build --filter=...[origin/main] turbo run build --remote-cache-signature
CI 策略 Polyrepo CI 1 2 3 4 5 6 7 8 9 10 11 name: Build on: [push ]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run build - run: npm test
简单 ——一个 repo 一个 CI。
Monorepo CI(全量构建) 1 2 3 4 5 6 7 8 9 10 11 name: Build All on: [push ]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: nx run-many --target=build --all - run: nx run-many --target=test --all
慢 ——50 service × 5 min build = 4 小时。
Monorepo CI(增量构建) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 name: Build Affected on: push: branches: [main ] pull_request: jobs: affected: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 - run: npm ci - run: nx affected -t build --base=origin/main --head=HEAD - run: nx affected -t test --base=origin/main --head=HEAD - run: nx affected -t lint --base=origin/main --head=HEAD
关键 :fetch-depth: 0——需要 git 历史才能算 affected。Nx 的 affected 算法 :从 base..head diff 文件,反向追踪依赖图 ,构建所有依赖链上的 service。
举例 :改了 libs/common-utils,依赖它的 10 个 service 都要重建 → 10 × 5 min = 50 min。
Monorepo CI(增量 + 远程缓存) 1 2 3 4 - run: nx affected -t build --base=origin/main --head=HEAD env: NX_CACHE_DIRECTORY: .nx/cache NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_TOKEN }}
远程缓存 :CI 跑过的 build 缓存到 Nx Cloud,本地 nx build 也复用。改同样代码不重 build 。
Polyrepo → Monorepo 迁移 实战步骤 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @startuml title Polyrepo to Monorepo Migration start :选工具(Nx / Bazel / Turborepo); :创建新 monorepo repo; note right 保留旧 repo 一段时间 (Polyrepo 仍在生产) end note while 还有 service 没迁移 :用 git subtree / git filter-repo\n把单个 service 目录搬进 monorepo; :改 import path; :加 BUILD / project.json / package.json; :部署 service(指向 monorepo); :验证生产无问题; endwhile :旧 repo archive; :更新文档/新人 onboarding; stop @enduml
关键 :渐进迁移 ,不要 big-bang。每迁移一个 service 验证一次生产 。
实战踩坑
Monorepo 不分目录 ——所有 service 平铺,几万个文件乱糟糟 。严格按 services/ libs/ tools/ 分目录 。
依赖图没可视化 ——新人不知道哪个 service 依赖哪个。Nx 跑 nx dep-graph 生成 SVG,放 wiki 。
共享 lib 改了就全坏 ——libs/common-utils 改一行,所有 service 都 break。所有 lib 改动必须经过 CI + 所有 service 测试 。
CI 不增量构建 ——全量 build 跑几小时。必须用 Nx affected / Turborepo –filter / Bazel remote cache 。
权限混乱 ——所有 service 都能看到所有代码,审计难。用 CODEOWNERS 文件 :1 2 3 /services/payment/ @payments-team /libs/auth-client/ @security-team /libs/db-helper/ @platform-team
Git 性能差 ——git log 在大 repo 慢。用 git log -- <path> 看单文件,或 tig 加速 。
IDE 卡顿 ——VSCode 打开 monorepo 第一次索引慢。排除不需要的目录 :"files.exclude": {"**/node_modules": true, "**/dist": true}。
决策树 1 2 3 4 5 6 7 8 9 10 团队多大? ├─ <5 人 → Polyrepo,简单 ├─ 5-30 人, 强耦合 → Monorepo ├─ 30-100 人, 多业务线 → Multi-repo └─ >100 人 → 多 monorepo,每个 monorepo 一个业务域 构建速度? ├─ 总 build <10 分钟 → 不用增量 ├─ 总 build 10-60 分钟 → Turborepo / Nx └─ 总 build >1 小时 → Bazel + remote cache
最小可行 monorepo :Nx + npm workspaces + nx affected + GitHub Actions 跑增量。大团队/性能要求高 升级到 Bazel。
最小可行 polyrepo + 共享 lib :单独 platform-shared-lib repo,其他 service 通过 git submodule / 版本 tag / npm 私有 registry 引用。
记住 :Monorepo 是工具,不是信仰。Google/Facebook 用 Monorepo 是因为他们有专门的 monorepo 团队维护 build system 。小团队硬上 monorepo 反而拖累效率。