单体、微服务、Serverless:架构要匹配问题
《工程化决策树》第 2 篇
架构选型不是给项目评成熟度,而是在业务不确定性与分布式成本之间作取舍。

一、三个人维护了一套为未来准备的系统
一个 3 人创业团队在做 SaaS 产品,前端是 React 和 Next.js,后端拆成用户、订单、支付、通知 4 个服务;基础设施用了 Kubernetes、Istio、Kafka、Prometheus、Grafana 和 Jaeger,每个服务还有独立的 PostgreSQL。
这套组合并非技术上不可行,问题是它解决的还不是团队当时最紧迫的问题。产品上线 4 个月,累计约 200 个用户,日活不到 20。需求仍在快速变化,3 个人中却有 1 个人长期维护基础设施。改一次订单流程要跨服务联调,通知服务的同步故障还曾阻断下单。
根据他们当时的云账单,每月成本约 8000 元。我建议先合回一个部署单元和一个数据库,保留清晰模块边界。重构后账单降到约 2000 元,原来维护基础设施的人也重新参与业务开发。之后两个月,调整后的架构承载了产品当期增长;用户增长受产品和市场等多种因素影响,不能归因于这次重构。
这些人数、用户量和成本只属于这个项目,不是"低于某个日活必须用单体"的行业阈值。这个案例暴露的是错位:团队主要风险还是需求能否被验证,架构却在提前支付分布式协作和运行成本。
二、三个选项,三组约束
单体:减少运行边界
单体把代码放在一个部署单元里,通常也从一个主要数据库开始。它的优势是本地调用、事务、调试和部署路径短,适合业务边界尚未稳定,或者团队能在一个交付节奏内协作的场景。
单体不等于把所有逻辑写进一个目录。没有模块边界、依赖任意穿透的单体会逐渐难以修改;按领域划分模块、限制依赖方向的模块化单体,仍然可以保持一个部署单元。
它的压力通常来自三处:整个应用一起发布,局部负载难以独立扩缩,多个团队在同一代码和数据边界上互相阻塞。当这些压力尚未出现,提前拆服务不会自动带来收益。
微服务:用分布式成本换独立性
微服务把业务边界变成独立部署和运行边界,适合需要独立发布、独立扩缩容、故障隔离或团队自治的模块。
收益成立有个前提:边界确实相对稳定,而且独立性带来的价值高于以下成本。
| 新增成本 | 团队要回答的问题 |
|---|---|
| 网络通信 | 超时、重试、幂等和降级如何处理 |
| 数据边界 | 跨服务查询与一致性由谁负责 |
| 发布数量 | 多服务如何部署、兼容和回滚 |
| 故障定位 | 日志、指标和链路如何关联 |
| 所有权 | 谁维护服务,谁响应故障 |
服务拆开只完成了物理分离。没有所有权、监控和回滚,团队得到的往往是更多故障面,而不是自治。
Serverless:把执行模型交给平台
Serverless 不是只能附着在单体旁边的"小插件"。如果业务天然事件驱动、请求可以短时无状态执行,团队接受托管平台的部署和运行方式,它完全可以成为后端主体。
判断它能否做主体,要逐项检查:
- 执行模型: 任务能否拆成平台支持的请求、事件或工作流?运行时限和状态外置是否可接受?
- 可观测性: 分散的函数、队列和托管服务能否形成统一的日志、指标与追踪?
- 延迟: 冷启动、跨服务调用和地域距离是否满足用户路径要求?不同平台和配置下表现差异很大。
- 成本曲线: 调用、执行时长、内存、流量和托管服务如何共同计费?应以实际负载测算,不能套一个通用"反转点"。
- 供应商耦合: 身份、消息、工作流和数据库越依赖专有能力,迁移成本越高;耦合可以接受,但要有意识地选择。
- 团队能力: 团队是否理解异步、幂等、重试、权限和云端调试,而不只是会写一个函数?
持续稳定负载、长连接、重计算和强本地调试需求,可能让容器或常驻进程更合适;流量波动、事件驱动和托管能力丰富的业务,则可能从 Serverless 获益。最终仍要看工作负载,而不是"主体"或"插件"的标签。
三、先看不确定性,再看协作压力
团队人数、日活和代码量都只是代理信号。对架构更直接的两个问题是:业务边界有多不确定,以及独立交付的压力有多大。

| 业务不确定性 | 协作 / 扩缩压力 | 更常见的起点 | 需要保留的警惕 |
|---|---|---|---|
| 高 | 低 | 单体或模块化单体 | 快速反馈,但不要让模块任意耦合 |
| 高 | 高 | 模块化单体,必要处用托管能力 | 团队边界可能仍在变,避免全盘拆分 |
| 低 | 局部升高 | 按瓶颈拆分 | 一次只验证一个拆分收益 |
| 低 | 高 | 微服务,或 Serverless / 容器组合 | 平台能力、数据治理和所有权必须跟上 |
"压力"也不只有流量。两个模块需要不同发布节奏、故障必须隔离、合规要求数据分区,或多个团队在同一仓库频繁互相等待,都可能构成拆分理由。
反过来,流量上升不等于立即拆服务。缓存、索引、查询优化、异步处理、只读副本和局部独立部署,有时能用更低成本解决瓶颈。先定位问题,再改变边界。
四、模块化单体为什么常是推荐起点
我通常建议需求仍在验证的团队从模块化单体开始:一个部署单元,代码按业务域分开,模块之间通过明确接口协作,数据库访问也有所有权边界。
text
应用
├── accounts 账户与权限
├── orders 订单规则与状态
├── payments 支付编排
└── notifications 通知策略
约束:
- 模块不直接调用另一个模块的内部实现
- 跨模块行为通过公开接口或领域事件表达
- 数据表有明确所有者,跨模块写入需要经过所有者
这样做的价值不只是"以后好拆"。即使永远不拆,边界也能降低修改时的认知负担。真出现局部瓶颈时,团队还有相对清楚的候选边界。
但"先单体、后拆分"只是推荐起点,不是宇宙定律。下面这些条件可能让项目从一开始就选择分布式或 Serverless:
- 已知的合规或安全边界要求物理隔离。
- 工作负载天然分布在不同地域、运行环境或硬件上。
- 多个成熟团队已经有稳定域边界和平台能力,需要独立发布。
- 业务高度事件驱动,托管服务能显著减少非核心运维。
- 现有组织或遗留系统决定了集成边界,合并反而成本更高。
关键不是先后顺序,而是能否说清楚:为什么需要这个边界,以及团队是否承担得起它。
五、从瓶颈出发的决策树
text
当前最贵的问题是什么?
│
├── 需求变化快、领域边界不稳定
│ └── 优先单体或模块化单体,缩短反馈路径
│
├── 事件突发、运维能力有限
│ └── 评估 Serverless 是否匹配执行模型与成本曲线
│
└── 已出现明确瓶颈
├── 能否在现有边界内解决?
│ ├── 能 → 先做索引、缓存、异步或局部扩容
│ └── 不能 → 继续判断
├── 候选模块能否独立拥有数据与发布节奏?
│ ├── 不能 → 先整理模块与数据边界
│ └── 能 → 继续判断
└── 团队能否部署、观测、回滚并处理一致性?
├── 不能 → 先补齐工程能力
└── 能 → 拆出该瓶颈,验证收益后再决定下一步
这棵树刻意不从"团队有多少人"开始。人数增加会放大协作压力,却不能证明微服务一定比单体好;少量工程师也可能因为合规隔离或特殊计算负载,合理维护少数独立服务。
每次拆分都应该带着可验证的目标,例如缩短某模块发布等待、隔离某类故障、降低某个负载的扩容成本。目标达成后再继续,避免把"拆完"本身当作成功。
六、回到那个三人团队
那个 SaaS 项目合回单体后,并没有把 4 个模块重新揉成一团。我们保留用户、订单、支付和通知的代码边界,只把同步网络调用改回进程内接口,把数据库收回同一个实例管理。
| 观察项 | 调整前 | 调整后 |
|---|---|---|
| 主要精力 | 业务开发与基础设施维护并行 | 更多时间回到业务验证 |
| 订单变更 | 跨服务修改与联调 | 在模块边界内修改和测试 |
| 故障面 | 网络、消息和多服务发布共同参与 | 单一部署路径,依赖更少 |
| 云资源 | 当时账单约 8000 元 / 月 | 当时账单约 2000 元 / 月 |
后来流量增长时,团队先处理了热数据缓存、读写压力和重查询模块,没有立即恢复全套微服务。这个选择在当时够用,但不能推出"某个日活以下都不需要微服务"。如果它是金融交易、实时协作或重计算产品,同样的用户量可能产生完全不同的压力。
我从这次经历里保留的判断是:先让监控告诉你瓶颈在哪里,再让边界跟着瓶颈变化。不要让想象中的未来流量,提前决定今天所有的部署单元。
七、选型前的检查清单
| 维度 | 要回答的问题 | 答不上来时先做什么 |
|---|---|---|
| 业务阶段 | 主要风险是需求不成立,还是现有系统无法承载? | 先确认风险,不急着比较技术名词 |
| 领域边界 | 模块是否有稳定职责和数据所有权? | 先在代码内建立模块边界 |
| 瓶颈证据 | 性能、发布、故障或合规压力具体在哪里? | 补指标、追踪和实际测量 |
| 独立收益 | 拆分后哪项指标或协作问题会改善? | 写出可验证目标 |
| 交付能力 | 多个单元能否自动部署、兼容升级和回滚? | 先补 CI/CD 与发布策略 |
| 运行能力 | 能否关联日志、指标、链路和告警? | 先建立可观测性 |
| 数据一致性 | 跨边界操作如何保证幂等、补偿与对账? | 先设计失败路径 |
| 成本模型 | 正常、峰值和增长负载分别花多少钱? | 用实际工作负载测算 |
| 团队所有权 | 谁维护平台,谁响应每个服务的故障? | 明确责任与值守能力 |
| 退出条件 | 何时需要换方案,迁移代价是什么? | 记录决策和复评信号 |
如果这些问题没有答案,先补证据通常比先画服务边界更有价值。
下一篇讨论代码组织:分层不是把文件放进 Controller、Service、Repository 三个目录,而是让变化被限制在可以理解的范围内。
下一篇:Controller 为什么会变胖:先拆职责,再谈分层
《工程化决策树》第 2 篇 · lytao123