起因
我们有 5 个微服务都用一份"共享 utility 代码"(如 protobuf 定义 /
通用 model / shared logger)。三种主流方式:
A. 各项目 copy-paste(同步靠人工,最差)
B. 抽成 npm/pip 包发包(增加发版流程)
C. Git submodule 共享一个 repo
D. Git subtree 把共享 repo 嵌入主 repo
E. monorepo 把所有项目放一起
各自适用场景不同。
A. Git submodule
cd myservice
git submodule add https://github.com/myorg/shared-libs.git lib/shared
git commit -m 'add shared submodule'
.gitmodules 文件 + lib/shared 目录(指向 shared-libs 的某个 commit)。
# clone 主 repo 时要 init submodule
git clone --recurse-submodules https://github.com/myorg/myservice.git
# 或事后
cd myservice
git submodule update --init --recursive
更新 submodule:
cd lib/shared
git pull origin main
cd ../..
git add lib/shared
git commit -m 'bump shared submodule to abc123'
主 repo 记录 "我指向 shared 的哪个 commit"。
优点:
- 共享 repo 独立版本控制
- 每个项目可固定某个版本(不同步升级)
缺点:
- 命令繁琐(
git submodule update经常忘) - 新成员第一次 clone 总踩坑(忘
--recurse-submodules) - 改 submodule 内代码后要 push 共享 repo + 更新主 repo 引用 = 两个
PR
B. Git subtree
cd myservice
git subtree add --prefix=lib/shared https://github.com/myorg/shared-libs.git main --squash
效果:把 shared-libs 整个内容 copy 到 lib/shared/ 目录,作为本 repo
的真实 commit(不是引用)。
# 拉 shared 上游更新
git subtree pull --prefix=lib/shared shared-remote main --squash
# 把本地 shared 改动推回上游
git subtree push --prefix=lib/shared shared-remote feature-x
优点:
- 普通 clone 拿到所有文件(无 submodule 双步骤)
- 本地直接改 shared 代码 + commit(无需先 push 上游)
- 简单:只是普通 git 文件
缺点:
git log看到 shared 的所有历史(噪音)- subtree push 偶尔合并冲突复杂
- 大多数开发者不熟,命令易记错
C. monorepo
把所有服务和 shared 放一个 repo:
myorg-monorepo/
├── services/
│ ├── api/
│ ├── worker/
│ └── frontend/
├── packages/
│ ├── shared-models/
│ └── shared-utils/
└── tools/
shared 代码就是 packages/ 下的子目录,import 直接相对路径或 workspace
(pnpm / yarn workspace / npm workspace / Cargo workspace / Go
workspace / etc)。
优点:
- 改 shared + 改用它的 service 一个 commit + 一个 PR
- 所有服务的 history / branch / CI 在同处
- 重构跨服务时方便(IDE refactor 全 monorepo 生效)
缺点:
- repo 越来越大(clone 慢;shallow clone 缓解)
- CI 要 path-based filter(不能"改 README 跑全部测试")
- 工具支持差异(Bazel / Nx / Turborepo 等 monorepo build 工具学习成本)
D. 单独发包
shared 抽成 npm / pip 包发到内部 registry:
# shared 项目
npm publish --registry https://npm.internal.example.com
# 使用方
npm i @myorg/shared
优点:
- 标准化语言生态
- 严格版本管理
- 不同服务可用不同 shared 版本
缺点:
- 改 shared 要发版 → 更新 → 测试 → 多步骤
- 内部 registry 要维护
- 不适合 quick iteration
决策矩阵
| 场景 | 推荐 |
|---|---|
| 5-10 个服务 + 1 个公司 + 同一团队 | monorepo |
| 开源项目想用别人的 OSS lib | submodule(少改) |
| 想 vendor 别人代码 + 偶尔修改 | subtree |
| 多公司 / 跨组织共享 lib | 单独发包 |
| 多团队 + 服务有独立发版周期 | 单独发包 |
| 想保持简单 + 改动频繁 | monorepo |
实际我的经验:绝大多数场景 monorepo 是最佳选择。submodule 是
"避免 monorepo 的 hack",长期维护成本高。
monorepo 工具
JavaScript / TypeScript:
# package.json
{
"private": true,
"workspaces": ["packages/*", "services/*"]
}
# 用 npm workspaces / pnpm workspace / yarn workspaces
pnpm install # 装所有
pnpm -r build # 在所有包里跑 build
pnpm --filter api dev # 只对 api 包
更高级:Turborepo / Nx,加 task caching + 增量 build。
Python:用 uv workspace:
# pyproject.toml
[tool.uv.workspace]
members = ["packages/*", "services/*"]
Go:go workspace:
go work init
go work use ./service-a ./service-b ./pkg/shared
Rust:Cargo workspace 在 Cargo.toml [workspace] 里。
CI 优化(monorepo 必备)
GitHub Actions path filter:
on:
pull_request:
paths:
- 'services/api/**'
- 'packages/shared-models/**'
- '.github/workflows/api.yml'
jobs:
test-api:
...
只在 api 或它依赖的 shared 改动时跑 api 测试。
或者用 dorny/paths-filter action 做动态:
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
api: 'services/api/**'
web: 'services/web/**'
shared: 'packages/shared-*/**'
- if: steps.changes.outputs.api == 'true' || steps.changes.outputs.shared == 'true'
run: pnpm --filter api test
避免改一行触发全部 CI。
我们的真实迁移
从 6 个独立 repo + Git submodule 到 monorepo:
| submodule before | monorepo after | |
|---|---|---|
| 新人 clone 步骤 | 5 步 | 1 步 |
| 跨 service 改动 | 6 个 PR | 1 个 PR |
| build 时间(CI 全跑) | 18min | 12min(with cache: 4min) |
| repo 大小 | 6 × 50MB | 280MB |
| 全部签新版本 | 几小时 | 单 PR + tag |
monorepo 显著简化。
踩过的坑
submodule 类
-
改 submodule 内代码忘 push:在 lib/shared 改了 + 主 repo
commit 更新引用 + 推主 repo。同事 clone 后 lib/shared 拿到的
commit 在 origin 不存在 → fail。先 push submodule,再 push 主 repo。 -
git checkout切分支 submodule 不变:默认 git 不自动更新
submodule 到目标分支记录的版本。git config submodule.recurse true
让所有命令自动 recurse。
subtree 类
- subtree push 偶尔失败:主 repo 内 commit 太散,subtree 算 patch
出错。先 squash 合并相关 commit 再 push。
monorepo 类
- 某 service 重 build 全 monorepo:必须做 path-based filter
-
build cache。否则 monorepo 反而比独立 repo 慢。
-
依赖循环:service A 用 shared B,shared B 用 service A 的某
helper(不该)→ 循环。monorepo workspace tool 应该报错;强制依赖
方向 packages/ → services/,不反向。
登录后参与评论。