27.1 单体架构的适用性
一句话
单体不是“落后的架构”,而是把复杂度集中在进程内:事务、调用、调试都便宜,代价是部署耦合与团队协作摩擦;判断该不该用单体,看“变更频率 × 团队规模”是否超过一个进程能承受的边界。
单体到底指什么
先把定义钉死,否则讨论会滑向“代码写了多少个文件”这种无关问题:
| 维度 | 单体 | 微服务 |
|---|---|---|
| 部署单元 | 1 个(一个进程/一个镜像) | N 个,各自独立 |
| 数据库 | 通常 1 个(或少数几个) | 每服务独立库 |
| 调用方式 | 进程内函数调用 | 跨网络 RPC |
| 事务 | 本地 ACID | 分布式事务 / 柔性事务 |
| 发布 | 整体发布(策略见 55.1 发布策略) | 独立发布 |
“单体”不等于“大泥球”:一个分层清晰、模块边界明确的单体(模块化单体)仍然是单体,但它和“所有代码互相 import”的泥球是两回事(分层见 26.1 分层架构,包设计见 26.6 模块化与包设计)。
适用性判据:五个问题
| 问题 | 倾向单体 | 倾向拆分 |
|---|---|---|
| 团队多大? | 1–2 个团队(≤ 15 人) | 多个团队,各自有独立目标 |
| 发布多频繁? | 每天几次、整体发布可接受 | 每天几十次,且各模块节奏不同 |
| 领域边界清楚吗? | 边界还没想清(拆了会反复改) | 边界稳定,能独立演进 |
| 事务要求强吗? | 跨表强一致、资金类 | 可接受最终一致 |
| 需要独立伸缩吗? | 各模块负载相近 | 某个模块流量是别的 10 倍 |
🔑 最容易被忽略的是第一条与第二条:单体的真正瓶颈不是代码量,而是“多条变更流挤一个发布通道”。下面的排队模型能把它算出来。
把“发布通道”算成一笔账
假设:每个团队每天发布 2 次,每次发布占用 20 分钟窗口(构建 + 测试 + 审批 + 观察),所有人共用一个发布通道。这是典型的 M/M/1 排队:
λ = 团队数 × 2 / 1440(次/分钟)
μ = 1/20(次/分钟)
ρ = λ/μ = 团队数 / 36
package main
import "fmt"
// 共享发布通道:M/M/1
// 每次发布占用 20 分钟;每团队每天发布 2 次
func main() {
const deployPerTeam = 2.0
const serviceMin = 20.0
fmt.Println("=== 共享发布通道:M/M/1(服务时间 20 分钟/次)===")
fmt.Printf("%-6s %-10s %-8s %-12s %-10s\n", "团队数", "发布/天", "利用率ρ", "平均排队(min)", "状态")
for _, teams := range []float64{1, 3, 5, 8, 12, 24} {
lambdaPerDay := teams * deployPerTeam
lambda := lambdaPerDay / (24 * 60) // 每分钟到达率
mu := 1 / serviceMin // 每分钟服务率
rho := lambda / mu
if rho >= 1 {
fmt.Printf("%-6.0f %-10.0f %-8.2f %-12s %-10s\n", teams, lambdaPerDay, rho, "∞", "队列发散")
continue
}
w := rho / (mu - lambda) // M/M/1 平均排队时间
state := "顺畅"
if rho > 0.7 {
state = "接近饱和"
}
fmt.Printf("%-6.0f %-10.0f %-8.2f %-12.1f %-10s\n", teams, lambdaPerDay, rho, w, state)
}
fmt.Println("\n(ρ 超过 0.7 后排队时间按 1/(1-ρ) 放大;到 1.0 时队列无界增长)")
}
本机实测(macOS,Apple M1,Go 1.27):
=== 共享发布通道:M/M/1(服务时间 20 分钟/次)===
团队数 发布/天 利用率ρ 平均排队(min) 状态
1 2 0.03 0.6 顺畅
3 6 0.08 1.8 顺畅
5 10 0.14 3.2 顺畅
8 16 0.22 5.7 顺畅
12 24 0.33 10.0 顺畅
24 48 0.67 40.0 顺畅
(ρ 超过 0.7 后排队时间按 1/(1-ρ) 放大;到 1.0 时队列无界增长)
这张表的读法:
| 团队数 | 平均排队 | 含义 |
|---|---|---|
| 12 | 10 分钟 | 还能忍,但已经开始“等窗口” |
| 24 | 40 分钟 | 每次发布平均等 40 分钟,发布变成瓶颈 |
| 36 | ρ = 1 | 队列无界:发布永远排不完,只能靠加班/插队 |
| > 36 | ρ > 1 | 数学上不可持续,实际表现是“发布冻结”“大 trains” |
⚠️ 这个模型是简化(假设泊松到达与指数服务时间),真实的发布更规律、窗口也更短,但结论的量级是对的:一个共享发布通道理论上能支撑“每天 72 次”(ρ=1 的临界点)的发布,再多就必须拆通道(独立发布)或缩窗口(更快的流水线)。
单体的真实优势:不是情怀,是四笔成本
| 成本项 | 单体 | 微服务 |
|---|---|---|
| 事务 | 本地 ACID,一条 BEGIN/COMMIT | 分布式事务/柔性事务(17.5 BASE 与柔性事务) |
| 调用 | 函数调用(纳秒级) | RPC(毫秒级,+序列化、超时、重试、熔断) |
| 调试 | 一个进程、一个栈、一条日志 | 跨服务 trace,需要链路追踪才能拼出因果 |
| 部署 | 一个制品,版本天然一致 | N 个制品 + 兼容矩阵 + 灰度编排 |
前三项是日常成本,每写一个功能都在付;第四项是发布成本,每次发布在付。拆分的收益(独立伸缩、独立发布、故障隔离)必须大于这四笔成本,才值得拆。
什么时候必须拆
| 信号 | 说明 |
|---|---|
| 某模块流量是其他模块的 10 倍 | 单体无法只扩这一个模块(37.1 垂直扩展与水平扩展) |
| 发布频率差异巨大(一天 50 次 vs 一月 1 次) | 共享发布通道成为瓶颈(上面的排队模型) |
| 故障隔离要求(一个模块 OOM 不能拖垮全站) | 进程级隔离 |
| 合规隔离(支付数据必须独立部署/独立审计) | 物理边界要求 |
| 团队规模超过沟通上限 | 康威定律开始反向塑造架构(74.1 康威定律及其逆定律) |
| 技术栈异构(某模块必须用 GPU/Python) | 进程内无法混跑 |
反过来说:没有这些信号时拆微服务,是在为“想象中的规模”付真实的成本。技术选型的评估维度见 4.3 技术选型的评估维度。
三种形态的对比
| 维度 | 大泥球单体 | 模块化单体 | 微服务 |
|---|---|---|---|
| 部署 | 1 个 | 1 个 | N 个 |
| 模块边界 | 无(互相 import) | 强制(包/模块依赖规则) | 网络边界 |
| 事务 | 本地 ACID | 本地 ACID(跨模块也可) | 分布式 |
| 发布耦合 | 高 | 中(仍是一个制品) | 低 |
| 演进路径 | 先拆模块,再考虑服务 | 可按模块逐步拆出服务 | 难回退 |
| 适合 | 早期原型 | 大多数业务系统的默认解 | 规模/组织真的需要时 |
🔑 我的判断:模块化单体应该是大多数团队(< 50 人、单一产品线)的默认选择,微服务是“组织与规模逼出来的解”,不是“技术先进的标志”。模块化单体的做法(包结构、依赖规则、模块间接口)见 27.2 模块化单体与 27.3 模块边界与依赖规则。
反模式
| 反模式 | 后果 |
|---|---|
| 把“单体”当“不设计模块”的借口 | 长成大泥球,之后拆不动(27.4 大泥球的形成路径) |
| 早期就拆微服务 | 分布式事务、链路追踪、N 倍运维成本,业务还没验证 |
| 单体但共享数据库给外部系统直连 | 数据库变成公共 API,无法演进 |
| 拆了服务却共享一个库 | 分布式单体:最差的组合(30.3 分布式单体反模式) |
| 用“发布窗口”硬扛团队增长 | 排队时间按 1/(1-ρ) 爆炸,发布冻结 |
| 单体不做限流/隔离 | 一个慢接口拖垮整个进程 |
自检
Q1 一个 30 人的团队、单一产品线、每天发布 3 次,正在纠结要不要拆微服务。用本节的判据给出建议,并说明理由。
▸ 答案:建议先做模块化单体,不拆。判据:① 团队规模(30 人,约 2–3 个小组)在共享发布通道的可承受范围内(模型里 24 个团队、每天 48 次发布时平均排队 40 分钟,30 人每天 3 次远低于饱和点);② 单一产品线、领域边界可能还在变,拆早了会反复改;③ 拆分的四笔成本(事务、调用、调试、部署)每天都要付,而收益(独立伸缩/发布/隔离)当前并不存在。如果出现“某模块流量 10 倍”“合规隔离”“发布频率差异巨大”这类信号,再按模块边界逐步拆。
Q2 为什么说“单体的瓶颈不是代码量,而是发布通道”?用排队模型解释 36 这个数字。
▸ 答案:代码量只影响构建时间与认知负荷,而发布冲突随团队数非线性增长。设每团队每天 2 次发布、每次占 20 分钟窗口,则 ρ = 团队数 × 2 / 1440 × 20 = 团队数 / 36;ρ 到 1 时队列无界,也就是 36 个团队就把一条发布通道压满。在此之前排队时间按 1/(1-ρ) 放大:24 个团队(ρ=0.67)平均已要等 40 分钟。所以“加团队”不会线性增加产出,超过饱和点后反而互相阻塞。
Q3 单体在事务上的优势具体是什么?拆成微服务后,这笔成本转移到哪里?
▸ 答案:单体内多个表的修改在一个本地事务里完成,ACID 由数据库保证,失败自动回滚,不需要补偿、对账、幂等。拆成微服务后,跨服务的一致性要靠分布式事务或柔性事务:TCC/Saga/本地消息表,每一笔都要处理中间态、幂等、补偿失败与对账(17.5),成本从“数据库的几行配置”变成“业务代码 + 运维流程 + 人工兜底”。
Q4 什么情况下“拆”是必须的,而不是可选的?至少举三个不可回避的信号。
▸ 答案:① 独立伸缩:某模块流量是其他模块的 10 倍,单体只能整体扩容,成本按倍数浪费;② 故障隔离:一个模块的 OOM/死循环会拖垮整个进程,业务要求“一个模块挂了不影响其他”;③ 合规/物理隔离:支付、医疗数据要求独立部署、独立审计、独立网络边界;④ 发布节奏差异:一天 50 次与一月 1 次的模块共用发布通道,排队模型直接发散;⑤ 技术栈异构:必须用 GPU/Python 的模块无法与主进程混跑。这些都是“架构约束”,不是“技术偏好”。
关联
- 前置:26.1 分层架构、26.6 模块化与包设计
- 并列:17.5 BASE 与柔性事务、55.1 发布策略
- 下游:37.1 垂直扩展与水平扩展、4.3 技术选型的评估维度
- 下游:27.2 模块化单体
- 下游:27.4 大泥球的形成路径(未写,无链接)