Monorepo vs Polyrepo:依赖图、构建编排、CI 策略

puml.online

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
# check_cycles.py
import re, pathlib, networkx as nx

deps = {}
for service_dir in pathlib.Path("services").iterdir():
name = service_dir.name
deps[name] = set()
# 读 .puml 或 package.json
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
# services/user/BUILD
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
# 改了一个文件,只重建受影响的 target
bazel build //services/user:user_service
# 自动跳过 unchanged targets

# 远程缓存(团队共享)
bazel build //services/user:user_service --remote_cache=https://bazel-cache.company.com
# 别人 build 过同样的 target,你直接拿缓存

构建工具: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
// nx.json
{
"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
# 增量构建(只 build 受影响的)
nx affected:build --base=main --head=HEAD
# 自动跳过未改的 service

# 远程缓存
nx build user-service --skip-nx-cache=false
# CI 跑一次,本地 nx 复用缓存

Nx dependency graph 自动生成:

1
2
nx dep-graph
# 输出 services/user -> libs/auth-client 等

构建工具:Turborepo

Turborepo 是 Vercel 出的,适合 Next.js / Vite 项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// turbo.json
{
"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]
# 只 build 受当前 branch 影响的 packages

# 远程缓存(免费 Vercel,企业自托管)
turbo run build --remote-cache-signature

CI 策略

Polyrepo CI

1
2
3
4
5
6
7
8
9
10
11
# .github/workflows/build.yml
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 # 需要完整 git 历史算 affected
- 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 反而拖累效率。

  • 标题: Monorepo vs Polyrepo:依赖图、构建编排、CI 策略
  • 作者: puml.online
  • 创建于 : 2026-07-30 17:40:00
  • 更新于 : 2026-08-14 21:34:29
  • 链接: https://puml.online/blog/plantuml-monorepo-polyrepo/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。