3120 字
约 10 分钟
20
S06-A08 · 仓库形态:多仓与单仓的切换阈值

一个需求动了前端、BFF、订单、公共模型库——仓库拆了六个,于是开六个 PR,合并要排序,联调等最后一环。结论往往是"合回去"。

这个结论对一半。合仓消掉六个 PR,同时把权限、发布节奏、构建时间、体积绑到一根绳上。阈值不是"PR 超过几个",而是这四件事里最先到底的那件。


1. 两边的痛不在同一时间尺度

拆仓的痛是每天可见的,合仓的痛是延迟发作的。所以直觉永远偏向"合"。

成本项 多仓 单仓 谁先发作
跨模块改动 每仓一个 PR,人工排序 一次提交 多仓:即刻
依赖对齐 需显式发布中间版本 源码直连,编译期校验 多仓:即刻
构建耗时 只跑本仓 随仓增长 单仓:半年后
权限 按仓硬隔离 CODEOWNERS 软约束 单仓:审计时
独立发版 天然独立 需切分发布单元 单仓:首次热修
体积 各自小 单调增长 单仓:两年后

2. 六维对比:只有一维不可补偿

维度 单仓 多仓 可补偿性
依赖管理 源码依赖,天然一致 版本号 + 发布 可:BOM、version catalog
原子变更 一次提交即完整状态 只能缩短中间态 不可补偿
CI 成本 需增量与缓存 只跑本仓 可:路径过滤、远程缓存
权限 软约束 硬隔离 可:CODEOWNERS + 环境审批
版本发布 需切分单元 天然独立 可:发布流水线、变更集
工具链 一套版本 各自漂移 可,但漂移是反向的痛

判据只有一条:改动是否经常需要"跨模块同时正确"。若一次变更的正确性依赖 A、B 两份语义同时生效,只有单仓(或源码级联编)能满足,其余五维都能花钱补。若模块本就"发一次、各跑各的、靠契约通信",原子性用不上,你只是在为用不上的能力付账。


3. 多仓如何逼近原子变更

手段一 临时分支指向:B 的依赖写成 branch-SNAPSHOT
  → 不可重现、缓存互相污染
手段二 先发布后引用:A 发版,B 再升依赖
  → 中间态期间线上跑"旧 A + 新 B"这种从未验证的组合
手段三 源码内联:git submodule 固定 commit / Gradle includeBuild
  → 指针要人肉同步;复合构建只解决编译,不解决发布

三种都能编译通过,都留下中间态窗口:窗口内任何提交组合都可能从未被整体验证。单仓把窗口压到零,多仓只能压短——能力差异,不是工程量差异。

判据:跨模块改动的平均中间态时长若超过一个发布周期,多仓已在常态化跑未验证组合。此时要么合仓,要么把两个模块的发布绑成一次。


4. 拆仓库与拆服务不是一回事

要解决的问题 该动的边界 动错会怎样
跨模块改,PR 太多 仓库边界 拆服务:多一次网络调用,问题还在
团队互相阻塞发布 仓库 + 发布单元 只拆服务:发布仍耦合
模块耦合深,改一处崩一片 代码/服务边界 只拆仓:耦合原封不动
合规要求生产变更隔离 仓库 + 分支保护 只拆服务:同权限,审批虚设
构建超阈值 构建图与增量策略 拆仓:症状缓解,两年复发

口诀:仓库边界决定原子性,服务边界决定失败域。 用一边解另一边的问题,得到的是成本。

反直觉的是:单仓不妨碍拆服务(一个仓可有十个部署单元),两个仓也可能是同一服务的两个模块。先定拆服务、后定拆仓;顺序反了就是用仓库形态偿还架构债,债还在。


5. 单仓的三种规模形态

形态 边界 工具支持 升级信号
单应用单模块 一个部署产物 单工程即可 出现第二个产物
多模块单应用 多模块共打包 父 pom + dependencyManagement 某模块被别的仓引用
多项目单仓 多部署单元 + 共享库 version catalog、任务图、路径过滤 团队 >2、构建超阈值
repo/
├── settings.gradle.kts        # 决定"哪些参与构建"
├── gradle/libs.versions.toml  # 版本单一事实源
├── build-logic/               # 构建约定插件
├── platform/                  # 只发 BOM,不发代码
├── libs/common/               # 共享库
├── services/order/            # 独立部署单元
└── .github/CODEOWNERS

升第三形态的判据不是模块数,而是是否出现"独立部署 + 独立发布节奏"。某模块一旦单独交付客户,成本就从代码组织转到流水线。


6. CI 成本是非线性的

线性假设"仓库大一倍、构建慢一倍"不成立:全量慢一倍,触发频率与重试次数同时上升,乘起来超线性。

变更类型 全量 路径过滤 + 远程缓存
改某服务业务代码 全仓编译 + 全仓测试 只编该服务及下游
改共享库一个方法 全仓 所有依赖方重编(无法回避)
只改文档 全仓 跳过编译,仅跑 lint
改构建脚本 全仓 缓存失效,退化为全量

后两行是关键:增量在"改共享库"和"改构建逻辑"上必然退化,而这两类恰是最危险的变更。增量需要三件同时到位:路径到模块的映射、模块到产物的缓存键、脚本变更的失效规则。

rules:
  - changes: ["services/order/**", "libs/common/**"]   # 共享库触发所有依赖方
    when: on_success
  - changes: ["docs/**", "**/*.md"]
    when: manual

取舍:写宽等于没做增量,写窄会漏构建、把问题推到线上。窄规则的风险静默,宽规则的代价可见——有争议时选宽。


7. 权限与版本模型

CODEOWNERS 是软约束:只决定谁被要求评审,不决定谁能写。合规看的是"能否证明变更经授权人批准",CODEOWNERS + 必需评审 + 分支保护三者齐备才成立;缺一项,单仓就弱于多仓。

# .github/CODEOWNERS
/services/order/    @team-order
/libs/common/       @team-platform @team-order @team-billing

代价:多仓付在建仓时,单仓付在每次提权时。团队少时单仓划算,外部协作多或有硬合规多仓更便宜。共享路径必须多 owner,否则共享库变某个团队的独裁。

统一版本几乎不产生内部组合(心智负担低、回滚即整体),代价是一个补丁迫使全仓发版;独立版本影响面小,代价是 N 个模块的 M 种组合,需要 BOM 与调解规则兜住。

依赖地狱的因果链:独立版本 → 版本空间多维化 → 组合未被整体验证 → 冲突在集成或运行期暴露。不是有人乱升版本,是维度变了。折中是分层:平台层统一,业务独立,服务之间走契约。


8. 体积、迁移与判据

体积趋势:代码与历史对象线性增长,克隆随之变慢;混入大二进制会跳变。

du -sh .git
git count-objects -vH                    # size-pack 与对象数
git rev-list --objects --all | wc -l     # 历史对象总量级

信号是".git 明显大于工作区,克隆进入新人第一天的等待"。先做 partial clone 与归档再谈拆仓——直接拆会把一个大历史切成几个都不小的历史。

迁移成本集中在五处:历史(无法真正合并,只能归档或断代)、CI(触发、缓存键、并行度、制品路径全变)、密钥(作用域从仓级变宽,最危险,等于权限静默扩张)、发布(单元从"仓"变"仓内路径")、习惯(分支名、评审人全改)。共同误判是"以为只改个路径"。

条件 倾向 判据
团队 >2 且发布无关 拆 发布耦合已成日常阻塞
硬合规、生产变更需隔离 拆 审计要求仓级隔离
构建超阈值且增量已失效 先查增量 多数"构建慢"是缓存或测试不可并行
体积进入克隆瓶颈 先瘦身 归档、LFS、partial clone
三五个强耦合小团队 不拆 跨仓 PR 成本远大于构建成本
发布必须同步 不拆 拆了也要绑发布,白拆

9. 混合形态与度量

混合形态分三层:平台层(构建约定、BOM、公共库)单仓;业务层(多服务共享源码)同仓;交付层(需单独交付)独立仓。代价是两套 CI 两套规范,硬规矩是平台层变化必须被所有层以同一版本消费,否则退化成"多仓 + 一个没人跟的平台仓"。

指标 采集 报警
平均 PR 跨度 一个需求涉及的仓/模块数 持续 >3 且同批模块 → 该合
构建 P90 按变更类型分组 单模块变更接近全量 → 增量失效
依赖更新延迟 发版到下游升级的中位天数 超一个发布周期 → 漂移
中间态时长 多 PR 需求首末合并之差 超一个发布周期 → 原子性缺口

前两个是合仓的收益,后两个是成本;放一起看比任何原则都好用。


10. 排错:五种误判

现象 真实原因 处置
公共库改了,下游没编就上线 依赖 SNAPSHOT 或未升版本 加"公共库变更须构建全依赖方"门禁
合仓后构建不可接受 共享库或脚本变更使增量退化 先修缓存键与路径映射
拆仓后仍要等别的团队 发布耦合在流水线 拆发布单元,非拆仓
单仓里 A 改了 B 的代码 CODEOWNERS 未覆盖或未启用必需评审 补 owner + 分支保护
合仓后审计不过 密钥作用域变大 环境级审批 + 分环境凭证

11. 边界:形态选择本就不重要的场景

场景 说明
两三人团队 沟通成本低于任何形态差异
完全独立的多个产品 无原子变更需求,同仓只是物理共处
平台工程能力强 CI 与权限被工具吸收
代码量几千行 成本尚未出现

仓库形态是"原子变更需求"与"治理成本"之间的一个解,不是正确答案。两侧量级都小时,选择价值接近零。


12. 自测

  1. 六维里哪一维不可补偿,另外五维为何可补偿?
  2. 三种模拟手段各自留下什么中间态窗口?
  3. "一个需求六个 PR"能否作为合仓理由?
  4. 统一版本下"一个补丁迫使全仓发版",如何折中?
  5. 体积信号看工作区还是 .git,用什么命令判量级?
  6. 合仓时最易被忽略、会静默扩张权限的成本项是哪一项?
S06-A08 · 仓库形态:多仓与单仓的切换阈值
https://www.clxhxhhr.top/posts/6236/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。