1. 引言:架构不是画图,而是做决策
很多人对软件架构的第一印象是"画架构图"------分层图、部署图、时序图。但真正做过几年系统的人都会发现:图只是结果,架构的本质是一连串决策。
一个系统之所以长成今天这个样子,往往不是因为"最优解",而是因为在某个时间点、某些约束下,团队做出了某个当时合理的选择。架构师的核心工作,就是在信息不完备的情况下,持续做出可解释、可回溯、可演进的决策。
本文围绕五个主题展开:
- 架构的核心原则:抽象、分解、组合
- 架构决策中的权衡艺术:CAP、一致性、可用性
- 架构评估方法:ATAM 与轻量级评估实践
- 架构决策记录(ADR)的编写与维护
2. 抽象、分解与组合:架构设计的三块基石
几乎所有架构方法论,最终都能还原到三个动作:抽象 、分解 、组合。它们不是三个步骤,而是三种反复交替使用的思维工具。
三者之间的关系可以这样理解:抽象决定"看到什么",分解决定"切成几块",组合决定"如何拼回去"。它们构成一个循环,而不是一条直线:
2.1 抽象:决定"忽略什么"
抽象的本质不是"概括",而是有意识地丢弃细节。一个好的抽象,让你在讨论业务时不必关心数据库索引,在讨论存储时不必关心业务规则。
判断抽象是否合格,可以用三个问题自检:
- 边界清晰:这个抽象对外暴露的契约是否稳定?内部实现变化时,调用方是否需要跟着改?
- 语义完整:它是否表达了一个完整的业务概念,而不是一堆零散方法的集合?
- 可替换:如果换一种实现(比如从 MySQL 换成 PostgreSQL),上层代码是否需要改动?
java
// 抽象不足:上层直接依赖具体实现细节
public class OrderService {
private final MySQLOrderDao dao = new MySQLOrderDao();
public void create(Order order) {
dao.insertWithRetry(order, 3); // 重试逻辑泄漏到业务层
}
}
// 抽象合理:业务层只关心"保存订单"这个语义
public interface OrderRepository {
void save(Order order);
}
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void create(Order order) {
repository.save(order);
}
}
注意:抽象不是越多越好。过度抽象会带来"抽象税"------每多一层间接,就多一层理解成本和调试难度。判断标准很简单:这个抽象是否已经被至少两处使用,或者是否明确预期会被替换。
2.2 分解:按"变化频率"切分
分解的关键不是"切得细",而是沿着变化的方向切。经典的分层(表现层 / 应用层 / 领域层 / 基础设施层)之所以有效,是因为各层的变更节奏不同:UI 天天改,领域模型相对稳定,基础设施按需替换。
常见的分解维度:
| 维度 | 适用场景 | 风险 |
|---|---|---|
| 按技术分层 | 中小型系统、团队按技术栈分工 | 业务逻辑散落各层,改一个需求动五个文件 |
| 按业务能力(限界上下文) | 中大型系统、多团队协作 | 边界划分错误会导致跨上下文频繁耦合 |
| 按读写分离 | 读多写少、查询复杂 | 一致性处理复杂,同步延迟需业务容忍 |
| 按变更频率 | 核心域与支撑域差异明显 | 需要较强的领域建模能力 |
一个实用的经验:先按业务能力切,再在能力内部按技术分层。反过来(先分层再按业务切)往往会在业务复杂度上升后付出重构代价。
下图对比了两种分解顺序的差异:
2.3 组合:让部件之间"松耦合、高内聚"
分解之后必须组合,而组合的质量取决于接口设计。好的组合有三个特征:
- 依赖方向单一:依赖指向稳定的一方,而不是双向依赖。
- 通信方式明确:同步调用、异步消息、事件通知,各自适用不同场景,不能混用。
- 失败可隔离:一个部件故障不应导致整体雪崩。
上图体现了一个常见原则:强一致需求走同步,弱一致需求走异步。订单扣库存必须同步确认,而加积分、发通知完全可以异步,失败后重试即可。
3. 架构决策中的权衡艺术:CAP、一致性与可用性
如果说抽象、分解、组合是"怎么设计",那么权衡就是"为什么这样设计"。架构决策的本质是在约束下做取舍,而取舍的前提是理解取舍的代价。
3.1 CAP 定理的正确理解
CAP 常被误读为"三选二"。更准确的表述是:在网络分区(P)发生时,系统只能在一致性(C)和可用性(A)之间做选择。分区不是可选项,而是分布式系统的客观现实。
因此真正的问题不是"选 CA 还是 CP",而是:
- 分区发生时,我宁可返回错误,还是返回可能过期的数据?
- 分区恢复后,我如何修复不一致?
| 系统类型 | 分区时的选择 | 典型场景 |
|---|---|---|
| CP | 拒绝写入,保证数据正确 | 支付、账务、库存扣减 |
| AP | 接受写入,事后对账修复 | 社交动态、商品浏览、日志采集 |
分区发生时的决策路径可以概括为:
3.2 一致性模型:不是"强"或"弱"二选一
实际系统里的一致性是一个光谱,常见的有:
- 线性一致性:所有读都能看到最新写入,代价最高。
- 顺序一致性:所有节点看到相同的操作顺序,但不保证实时。
- 因果一致性:有因果关系的操作保持顺序,无因果的可以乱序。
- 最终一致性:只要不再写入,最终所有副本一致。
选择一致性模型时,问自己一个问题:业务能否容忍"读到旧数据"?能容忍多久? 这个"多久"就是你的 SLA 边界。
一致性模型可以按"约束强度"排成一条光谱,越往右代价越低、容忍的旧数据窗口越大:
3.3 可用性的真实成本
可用性每提升一个 9,成本往往呈指数上升:
| 可用性 | 年停机时间 | 典型手段 |
|---|---|---|
| 99% | 约 3.65 天 | 单机 + 备份 |
| 99.9% | 约 8.76 小时 | 主从 + 健康检查 |
| 99.99% | 约 52.6 分钟 | 多可用区 + 自动故障转移 |
| 99.999% | 约 5.26 分钟 | 多地域 + 全链路容灾 |
关键洞察:可用性不是越高越好,而是要与业务价值匹配。一个内部报表系统的 99.9% 已经足够,而支付链路的 99.99% 是底线。盲目追求高可用,往往换来的是复杂度失控和故障排查困难。
3.4 权衡的落地方法
面对具体决策,可以用一个简单的四步法:
- 列出候选方案:至少两个,避免"唯一解"思维。
- 明确约束:团队规模、交付时间、预算、合规要求。
- 量化影响:每个方案在性能、成本、复杂度、可维护性上的代价。
- 记录理由:为什么选它,什么条件下会重新评估。
第 4 步最容易被忽略,却恰恰是架构可演进的关键------这正是 ADR 要解决的问题。
这四步与后续的评估、记录环节串起来,就形成了一条完整的决策闭环:
4. 架构评估方法:ATAM 与轻量级评估实践
架构设计完成后,如何判断它"够不够好"?架构评估的价值不在于打分,而在于在投入大量开发资源之前,暴露风险。
4.1 ATAM:架构权衡分析方法
ATAM(Architecture Tradeoff Analysis Method)由 SEI 提出,核心思想是:通过质量属性场景,识别架构中的风险点、敏感点和权衡点。
它的关键概念:
- 质量属性场景:把模糊的"高性能"变成可验证的描述,例如"在 5000 QPS 下,订单查询 P99 延迟不超过 200ms"。
- 敏感点:某个质量属性对某个架构决策高度敏感的地方。
- 权衡点:同时影响多个质量属性的决策点,改一处会牵动多处。
- 风险与非风险:可能导致质量属性不达标的决策,以及被明确排除的隐患。
ATAM 的完整流程较重(通常需要数天、多角色参与),适合大型关键系统。其核心步骤可简化为:
4.2 轻量级评估:小团队的实用做法
大多数团队没有条件跑完整 ATAM,但可以借用它的思想做轻量评估。一个 2 小时就能完成的实践:
- 列出 5 个最重要的质量属性(性能、可用性、安全、可维护性、成本)。
- 每个属性写 1 个具体场景,包含刺激源、环境、响应、度量。
- 对照架构逐条走查:当前设计能否满足?靠什么机制满足?
- 标记三类结论:满足、有风险、未知。
- 对"有风险"和"未知"项排优先级,纳入后续迭代。
这种做法的价值在于:它把"我觉得没问题"变成了"我们验证过哪些、还有哪些没验证"。
4.3 评估中最常见的三个误区
- 只评估功能,不评估质量属性:功能能跑通不代表架构合理。
- 场景写得不可度量:"系统要快"无法评估,"P99 < 200ms"才能评估。
- 评估一次就结束:架构会演进,评估应随重大变更重新进行。
5. 架构决策记录(ADR)的编写与维护
架构决策最大的浪费,不是做错了决策,而是做对了却没人记得为什么。半年后新人问"为什么这里用消息队列而不是直接调用",如果没人答得上来,团队就会重复踩坑,或者盲目推翻重来。
5.1 ADR 是什么
ADR(Architecture Decision Record)是对一个重要架构决策的简短记录,通常包含:背景、决策、备选方案、后果。它的核心特征是:
- 一决策一记录:不写"架构设计文档",只写"某个决策"。
- 不可变:决策变更时新增一条记录,而不是修改旧的。
- 轻量:一页以内,几分钟能读完。
5.2 ADR 模板
markdown
# ADR-0007:订单服务与库存服务采用同步调用
## 状态
已接受(2026-10-20)
## 背景
订单创建时需要扣减库存。当前库存服务与订单服务分属不同团队,
存在网络调用。业务要求超卖率必须为 0。
## 决策
订单服务在创建订单时同步调用库存服务的扣减接口,
调用失败则订单创建失败,不做异步补偿。
## 备选方案
1. 异步消息扣减 + 定时对账:实现简单,但存在超卖窗口。
2. 库存预占 + 异步确认:一致性更好,但需要额外的预占状态管理。
## 后果
- 正面:超卖率为 0,业务规则清晰。
- 负面:库存服务不可用时订单无法创建,可用性受库存服务制约。
- 缓解:库存服务做多副本部署,并设置合理的超时与熔断。
## 重新评估条件
若库存服务可用性长期低于 99.95%,或订单峰值超过当前容量 3 倍,
需重新评估是否引入预占机制。
5.3 维护 ADR 的实用建议
- 存放位置 :与代码同仓库(如
docs/adr/),随代码一起评审和版本管理。 - 编号规则 :
ADR-0001递增,永不复用编号。 - 触发时机:引入新技术、改变服务边界、确定关键数据模型、做出"以后很难改"的决定时。
- 评审方式:把 ADR 作为代码评审的一部分,决策与实现同步落地。
- 定期回顾:每季度扫一遍"重新评估条件",判断是否有 ADR 需要更新状态。
一个常见的反模式是:把 ADR 写成设计文档。ADR 不是用来描述系统全貌的,它只回答一个问题------"我们为什么这样选"。写得太长,反而没人看。
6. 总结
回到开头那句话:架构不是画图,而是做决策。本文的五个主题其实是一条完整的链路:
- 抽象、分解、组合回答了"如何设计";
- CAP 与一致性权衡回答了"为什么这样设计";
- 架构评估回答了"设计得够不够好";
- ADR回答了"如何让决策可追溯、可演进"。
如果只能记住一句话,那就是:好的架构不是没有权衡,而是权衡被清晰地记录、被持续地验证。