架构决策记录(ADR)最佳实践
那场会议已经进行了两个小时。我们正在讨论是用PostgreSQL还是MongoDB来做新服务。正反论据此起彼伏——关系完整性、灵活的模式、团队熟悉度、运维复杂性。
然后有人提到:"我们去年不是为用户服务讨论过完全相同的问题吗?"
沉默。确实如此。一年前,我们花了类似的时间讨论同样的问题,最终选择了PostgreSQL。但没人记得推理过程。领导那次讨论的工程师已经离开了公司。于是我们从零开始进行完全相同的辩论。
这就是我发现架构决策记录(ADR)的契机,从那以后,它为我们节省了无数重复讨论已定决策的时间。
什么是ADR?
ADR是一份记录单一架构决策的简短文档。不是涵盖系统所有方面的设计文档,只是一个决策:
- 我们决定了什么
- 为什么这样决定
- 考虑了哪些替代方案
- 结果会怎样
格式刻意保持轻量。一个ADR应该能放在一页纸上。如果更长,你可能在记录不止一个决策。
好的ADR的结构
在撰写了数十个ADR并阅读了数百个之后,我发现最好的ADR遵循一致的结构:
标题和编号
每个ADR都有一个顺序编号和简洁的标题:
ADR-0042: 订单服务使用PostgreSQL
编号很重要。它创建了决策的时间线,使ADR在讨论中易于引用:"正如我们在ADR-42中决定的..."
状态
ADR有生命周期状态:
- 已提议:仍在讨论中
- 已接受:决策已做出,我们正在遵循
- 已弃用:不再相关(新ADR取代了它)
- 已拒绝:我们考虑了但决定不采用
已拒绝状态特别有价值。有时你想记录为什么_没有_做某事,这样未来的团队就不会提出同样的建议。
背景
这里描述促使做出决策的情况。我们在解决什么问题?有什么约束?谁会受影响?
## 背景
订单服务需要持久化存储来存放订单数据。我们预计最初每天处理50,000个订单,
两年内增长到500,000个。订单有明确定义的结构,但可能需要随时间添加额外的
元数据字段。团队对关系数据库和文档数据库都有经验。
要具体说明约束条件。"我们需要ACID合规性"比"我们需要可靠性"有用得多。未来的读者需要理解塑造决策的力量。
决策
清楚地陈述决策。不是"我们可能考虑"或"我们应该探索"——而是我们实际决定了什么。
## 决策
我们将使用PostgreSQL 15作为订单服务的主数据库。
我们选择PostgreSQL因为:
- 金融订单数据需要ACID合规性
- JSON列提供元数据的模式灵活性
- 我们的基础设施团队有PostgreSQL运维经验
- 查询模式适合关系建模
注意这不仅仅是陈述决策,还简要解释了推理。下一节会更详细,但即使是决策部分也应该是自解释的。
考虑的替代方案
这是最被低估的部分。记录你没有选择的方案,往往与记录你选择的方案一样有价值。
## 考虑的替代方案
### MongoDB
优点:
- 原生JSON存储
- 更简单的水平扩展
- 灵活的模式演进
缺点:
- 一致性保证较弱
- 团队对运维不太熟悉
- 需要额外工具支持事务
### DynamoDB
优点:
- 完全托管,运维最少
- 出色的可扩展性
缺点:
- 对AWS的供应商锁定
- 查询模式限于分区键/排序键访问
- 高规模下成本不可预测
当新成员加入团队问"为什么我们不用MongoDB?"时,你就有了答案。不需要安排会议或找到做出原始决策的人。
后果
每个决策都有权衡。对此要诚实。
## 后果
### 积极方面
- 强大的数据完整性保证
- 团队可以利用现有PostgreSQL专业知识
- 成熟的运维特性
### 消极方面
- 模式迁移比文档存储需要更多规划
- 如果超出单节点容量,水平扩展更复杂
- 如果达到扩展限制,需要实现应用层分片
### 风险
- 如果订单量超过每天100万,可能需要重新审视
- JSON列查询效率不如原生文档存储
这一节是关于知识上的诚实。没有完美的决策。承认缺点建立信任,帮助未来的团队理解何时可能需要重新审视决策。
何时写ADR
并非每个技术选择都需要ADR。使用你的判断,但以下是一些指导:
应该写ADR的情况...
- 决策影响多个团队或服务
- 决策逆转成本高昂
- 你在多个可行选项之间选择
- 未来的团队成员可能质疑这个选择
- 决策解决了一个重大的技术辩论
不需要写ADR的情况...
- 选择是显而易见且无争议的
- 决策只影响一个人或一个文件
- 无需重大成本即可轻松逆转
- 这是团队一直遵循的标准模式
例如:"应该用哪个JSON库?"可能不需要ADR。"我们的公共API应该用GraphQL还是REST?"绝对需要。
我们项目中的真实ADR
以下是我们写过的一些实际ADR(为这篇文章做了简化):
ADR-0007: 服务间的事件驱动通信
背景:我们的微服务目前通过HTTP同步通信。这造成了紧耦合,当服务不可用时会产生级联故障。
决策:我们将采用事件驱动架构,使用Apache Kafka进行服务间的异步通信。
后果:
- 服务对故障的容错性提高
- 最终一致性代替强一致性
- 运维复杂性增加(Kafka集群管理)
- 团队需要事件溯源模式的培训
ADR-0015: 前端应用使用Monorepo
背景:我们有5个前端应用在不同的仓库中。共享代码需要发布包。版本不匹配导致开发体验差。
决策:使用Nx将所有前端应用合并到单个monorepo中。
拒绝的替代方案:
- 保持独立仓库加强包管理:拒绝,因为协调开销仍然存在
- 不使用Nx的单一仓库:拒绝,因为构建时间会过长
后果:
- 代码共享简化,一致性提高
- 单个PR可以更新共享代码和所有消费者
- 更大的仓库需要更好的构建缓存工具
- 应用间可能存在意外耦合
我犯过的常见错误
错误1:事后才写ADR
写ADR的最佳时机是在决策过程中,而不是几周后。事后写的话,你会忘记细微之处、考虑过的替代方案和具体的约束条件。
现在我们将ADR草稿作为决策过程的一部分来撰写。讨论发生在ADR文档中,而不是在消失的Slack线程中。
错误2:写得太长
如果你的ADR超过一页,你可能是:
- 记录了多个决策(拆分为多个ADR)
- 包含了实现细节(留给设计文档)
- 过度解释了显而易见的背景
简洁的纪律迫使你保持清晰。
错误3:不链接相关ADR
决策很少是孤立存在的。当我们选择Kafka进行事件驱动通信(ADR-0007)时,这影响了我们的数据库选择(ADR-0042),因为我们可以接受最终一致性。
交叉引用相关ADR:
## 相关决策
- 参见ADR-0007了解我们为什么接受最终一致性
- 取代了推荐MongoDB的ADR-0003
错误4:放弃这个实践
ADR随时间提供价值。一两个ADR帮助不大。经过多年积累的50多个ADR语料库变得极其有价值。它是架构演进的可搜索历史。
忙碌时停止写ADR是很大的诱惑。要抵抗它。花十分钟写一个ADR会在以后节省数小时的会议时间。
让ADR成为工作流的一部分
最难的部分不是写ADR——而是让它成为习惯。以下是有效的方法:
将它们存放在代码旁边
把ADR放在你的代码仓库中,通常在docs/adr/或docs/decisions/中。这样:
- 它们与描述的代码一起版本化
- 它们出现在代码审查中
- 它们不会在某个wiki中成为孤儿
模板化
创建一个大家都使用的markdown模板。这减少了摩擦并确保一致性:
# ADR-NNNN: 标题
## 状态
已提议 | 已接受 | 已弃用 | 已拒绝
## 背景
[促使做出这个决策的问题是什么?]
## 决策
[我们提议和/或正在做的变更是什么?]
## 考虑的替代方案
[我们考虑了哪些其他选项?]
## 后果
[由于这个变更,什么变得更容易或更困难?]
每季度审查ADR
设置日历提醒每季度审查你的ADR:
- 是否有决策不再相关?
- 情况是否发生了变化,使我们的假设无效?
- 是否有未记录的决策应该记录?
将ADR链接到架构图
这就是Archyl等工具发挥作用的地方。当你将ADR链接到C4图中的特定组件时,查看该组件的任何人都可以立即看到塑造它的决策。
"为什么这个服务和Kafka通信而不是直接连数据库?"点击关联的ADR就能找到答案。
总结
架构决策记录是那种看起来像额外工作直到你需要它们的实践之一。然后它们变得无价。
那场关于PostgreSQL和MongoDB的两小时会议?在有人找到原始ADR后十分钟就结束了。所有的背景都在那里——约束、替代方案、推理。我们很快确认了原始决策仍然适用,然后继续前进。
从小处开始。下次你做架构决策时,花十五分钟写一个ADR。把它放在你的仓库中。六个月后,当有人问"我们为什么那样做?"时,你就有了答案。