架构决策记录 ADR (Architecture Decision Records)
目录
ADR概述
什么是ADR
架构决策记录(Architecture Decision Record, ADR)是一种轻量级的文档形式,用于记录架构设计过程中的重要决策及其背景、原因和影响。
ADR的价值
┌──────────────────────────────────────────────────────────┐
│ ADR的价值 │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 知识保留 │───────▶│ 决策透明 │ │
│ │ 记录为什么 │ │ 团队共识 │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 避免重复 │ │ 新人上手 │ │
│ │ 决策失忆 │───────▶│ 快速理解 │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 技术债务 │ │ 决策审计 │ │
│ │ 可追溯 │───────▶│ 合规证明 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘何时创建ADR
- 选择技术栈或框架时
- 架构模式选择时
- 重要的设计决策时
- 技术债务决策时
- 安全或性能权衡时
ADR模板
标准ADR模板
# ADR-XXXX: [简短标题]
## 状态
[提议中 | 已接受 | 已拒绝 | 已废弃 | 已替代]
如果已替代,链接到替代的ADR: [ADR-YYYY](#)
## 背景 (Context)
描述需要做决策的背景和问题:
- 业务背景是什么?
- 技术现状如何?
- 面临什么问题或挑战?
- 有什么约束条件?
## 决策 (Decision)
我们将要采取的决策是什么?
- 用简洁明确的语言描述决策
- 使用主动语态:"我们将使用X技术"
## 理由 (Rationale)
为什么做这个决策?
- 解释选择的原因
- 说明考虑的权衡因素
- 引用相关数据或研究
## 后果 (Consequences)
这个决策会带来什么影响?
### 正面影响
- 好处1
- 好处2
### 负面影响
- 代价1
- 代价2
### 风险和缓解措施
- 风险1 → 缓解方案
- 风险2 → 缓解方案
## 替代方案 (Alternatives)
考虑过哪些其他方案?为什么没选它们?
### 方案A: [名称]
- 优点:
- 缺点:
- 为什么没选:
### 方案B: [名称]
- 优点:
- 缺点:
- 为什么没选:
## 参考资料 (References)
- [相关文档]
- [技术文章]
- [POC代码]
## 元数据
- 作者: [姓名]
- 日期: YYYY-MM-DD
- 评审者: [姓名列表]
- 相关ADR: [链接]
- 相关Issue: [链接]ADR编号规范
ADR-[YYYY]-[NNN]
YYYY: 年份
NNN: 序号(从001开始)
示例:
- ADR-2024-001
- ADR-2024-002决策流程
决策过程
┌────────────────────────────────────────────────────┐
│ 架构决策流程 │
└────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ 1. 识别问题 │
│ - 问题定义 │
│ - 目标设定 │
│ - 约束识别 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 2. 收集信息 │
│ - 技术调研 │
│ - 竞品分析 │
│ - 团队讨论 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 3. 提出方案 │
│ - 候选方案 │
│ - 优劣分析 │
│ - POC验证 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 4. 评估权衡 │
│ - 多维度评分 │
│ - 风险评估 │
│ - 成本分析 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 5. 做出决策 │
│ - 选择方案 │
│ - 记录ADR │
│ - 团队评审 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 6. 执行跟踪 │
│ - 实施方案 │
│ - 效果验证 │
│ - 持续改进 │
└─────────────────┘决策权衡矩阵
# decision_matrix.py
from dataclasses import dataclass
from typing import List, Dict
from enum import Enum
class CriteriaWeight(Enum):
"""评估维度权重"""
CRITICAL = 5 # 关键
HIGH = 4 # 高
MEDIUM = 3 # 中
LOW = 2 # 低
MINIMAL = 1 # 最低
@dataclass
class Criteria:
"""评估维度"""
name: str
weight: CriteriaWeight
description: str
@dataclass
class Alternative:
"""候选方案"""
name: str
description: str
scores: Dict[str, int] # 维度名称 -> 得分(1-10)
pros: List[str]
cons: List[str]
estimated_cost: float # 估算成本
estimated_time: int # 估算时间(天)
class DecisionMatrix:
"""决策矩阵"""
def __init__(self, title: str):
self.title = title
self.criteria: List[Criteria] = []
self.alternatives: List[Alternative] = []
def add_criteria(self, criteria: Criteria):
"""添加评估维度"""
self.criteria.append(criteria)
def add_alternative(self, alternative: Alternative):
"""添加候选方案"""
self.alternatives.append(alternative)
def calculate_weighted_score(self, alternative: Alternative) -> float:
"""计算加权得分"""
total_score = 0
total_weight = 0
for criteria in self.criteria:
score = alternative.scores.get(criteria.name, 0)
weight = criteria.weight.value
total_score += score * weight
total_weight += weight * 10 # 满分10
return (total_score / total_weight * 100) if total_weight > 0 else 0
def rank_alternatives(self) -> List[tuple]:
"""对方案进行排序"""
ranked = []
for alt in self.alternatives:
score = self.calculate_weighted_score(alt)
ranked.append((alt, score))
return sorted(ranked, key=lambda x: x[1], reverse=True)
def generate_report(self) -> str:
"""生成决策报告"""
report = f"""
决策矩阵分析报告: {self.title}
{'='*60}
评估维度:
"""
for c in self.criteria:
report += f" - {c.name} (权重: {c.weight.value}): {c.description}\n"
report += f"\n方案排名:\n"
ranked = self.rank_alternatives()
for i, (alt, score) in enumerate(ranked, 1):
report += f"\n{i}. {alt.name} (得分: {score:.1f}/100)\n"
report += f" 描述: {alt.description}\n"
report += f" 成本: ${alt.estimated_cost:,.0f} | 时间: {alt.estimated_time}天\n"
# 详细得分
report += " 各维度得分:\n"
for criteria in self.criteria:
raw_score = alt.scores.get(criteria.name, 0)
weighted = raw_score * criteria.weight.value
report += f" - {criteria.name}: {raw_score}/10 (加权: {weighted})\n"
report += f" 优点:\n"
for pro in alt.pros:
report += f" + {pro}\n"
report += f" 缺点:\n"
for con in alt.cons:
report += f" - {con}\n"
# 推荐
if ranked:
best = ranked[0][0]
report += f"\n推荐方案: {best.name}\n"
report += f"推荐理由: 综合评分最高,在关键维度表现优秀\n"
return report
def export_markdown_table(self) -> str:
"""导出为Markdown表格"""
# 表头
header = "| 方案 | " + " | ".join(c.name for c in self.criteria) + " | 总分 | 成本 | 时间 |\n"
separator = "|------|" + "|".join(["------"] * len(self.criteria)) + "|------|------|------|\n"
# 数据行
rows = ""
ranked = self.rank_alternatives()
for alt, total_score in ranked:
row = f"| {alt.name} |"
for criteria in self.criteria:
score = alt.scores.get(criteria.name, 0)
row += f" {score}/10 |"
row += f" {total_score:.1f} | ${alt.estimated_cost:,.0f} | {alt.estimated_time}d |\n"
rows += row
return header + separator + rows
# 使用示例
def decision_matrix_example():
# 创建决策矩阵:选择消息队列
matrix = DecisionMatrix("消息队列技术选型")
# 添加评估维度
matrix.add_criteria(Criteria(
"性能", CriteriaWeight.CRITICAL,
"吞吐量和延迟表现"
))
matrix.add_criteria(Criteria(
"可靠性", CriteriaWeight.CRITICAL,
"消息不丢失,高可用"
))
matrix.add_criteria(Criteria(
"运维成本", CriteriaWeight.HIGH,
"部署和维护的复杂度"
))
matrix.add_criteria(Criteria(
"生态", CriteriaWeight.MEDIUM,
"社区活跃度和工具支持"
))
matrix.add_criteria(Criteria(
"团队熟悉度", CriteriaWeight.HIGH,
"团队对技术的掌握程度"
))
# 添加候选方案
matrix.add_alternative(Alternative(
name="Kafka",
description="分布式流处理平台,高吞吐量",
scores={
"性能": 10,
"可靠性": 9,
"运维成本": 6,
"生态": 10,
"团队熟悉度": 8
},
pros=[
"超高吞吐量(百万级QPS)",
"持久化存储,支持消息回溯",
"生态完善,工具丰富",
"适合大数据场景"
],
cons=[
"运维复杂,需要ZooKeeper",
"学习曲线陡峭",
"延迟相对较高(毫秒级)"
],
estimated_cost=50000,
estimated_time=30
))
matrix.add_alternative(Alternative(
name="RabbitMQ",
description="传统消息队列,支持多种协议",
scores={
"性能": 7,
"可靠性": 9,
"运维成本": 8,
"生态": 9,
"团队熟悉度": 9
},
pros=[
"成熟稳定,企业级支持",
"支持多种消息模式",
"管理界面友好",
"延迟低(微秒级)"
],
cons=[
"吞吐量相对较低",
"集群模式复杂",
"Erlang技术栈小众"
],
estimated_cost=30000,
estimated_time=15
))
matrix.add_alternative(Alternative(
name="RocketMQ",
description="阿里开源的分布式消息中间件",
scores={
"性能": 9,
"可靠性": 9,
"运维成本": 7,
"生态": 7,
"团队熟悉度": 5
},
pros=[
"高吞吐量,低延迟",
"可靠性高,事务消息支持",
"国内社区活跃",
"Java技术栈,易集成"
],
cons=[
"国际化程度不如Kafka",
"文档相对较少",
"团队不熟悉"
],
estimated_cost=40000,
estimated_time=25
))
# 生成报告
print(matrix.generate_report())
print("\n" + "="*60 + "\n")
print("Markdown表格:\n")
print(matrix.export_markdown_table())
if __name__ == "__main__":
decision_matrix_example()真实案例
案例1: 选择API网关
# ADR-2024-001: 选择API网关技术
## 状态
已接受 (2024-01-15)
## 背景
我们的微服务架构包含30+个服务,目前存在以下问题:
1. **认证分散**: 每个服务独立实现认证,代码重复
2. **调用混乱**: 前端直接调用各个服务,域名管理困难
3. **限流缺失**: 没有统一的流量控制,容易被打垮
4. **监控盲区**: 无法统一监控API调用情况
5. **协议不统一**: 部分用HTTP,部分用gRPC,前端适配困难
**约束条件**:
- 团队主要使用Go和Java
- 要求99.9%的可用性
- 峰值QPS预计5万+
- 预算有限,倾向开源方案
## 决策
我们将使用 **Kong** 作为API网关,采用数据库模式部署(PostgreSQL)。
具体实施:
- 使用Kong作为统一入口
- 通过Kong插件实现认证、限流、日志
- 保留内部服务间gRPC调用
- 使用Prometheus插件实现监控
## 理由
1. **功能完善**: Kong提供了我们需要的所有功能(认证、限流、监控、协议转换)
2. **性能优异**: 基于OpenResty/Nginx,单实例可支持数万QPS
- 压测结果:单实例2万QPS,P99延迟 < 10ms
3. **插件生态**: 丰富的官方和社区插件,可快速扩展功能
- 已有100+插件
- 支持Lua/Go自定义插件
4. **团队能力**: 团队有Nginx使用经验,学习成本可控
5. **商业支持**: 有企业版可选,提供技术支持和额外功能
## 后果
### 正面影响
1. **统一管理**: 所有外部API通过Kong统一管理
2. **简化开发**: 服务无需实现认证等横切关注点
3. **提升性能**: 通过缓存、限流等策略保护后端
4. **可观测性**: 统一的日志和监控入口
### 负面影响
1. **新的单点**: Kong成为关键路径,需要高可用部署
- 缓解:部署至少3个实例,使用负载均衡器
2. **运维复杂度**: 需要维护Kong和PostgreSQL
- 缓解:使用Kubernetes部署,自动化运维
3. **学习成本**: 团队需要学习Kong配置和插件开发
- 缓解:安排培训,准备文档
4. **数据库依赖**: 使用数据库模式,增加了组件依赖
- 缓解:PostgreSQL使用主备部署,定期备份
### 风险和缓解措施
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| Kong故障导致全站不可用 | 高 | 中 | 多实例部署+健康检查+快速回滚 |
| 性能不足 | 中 | 低 | 提前压测,预留扩容空间 |
| 插件bug | 中 | 中 | 充分测试,准备回退方案 |
| 数据库故障 | 高 | 低 | 主备+备份+监控告警 |
## 替代方案
### 方案A: Nginx + Lua脚本
**优点**:
- 性能最优,无额外抽象层
- 团队熟悉Nginx
- 部署简单,无数据库依赖
**缺点**:
- 需要大量自研开发
- 配置管理困难
- 动态更新支持弱
**为什么没选**:
开发成本太高,需要从零实现认证、限流等功能,预计需要3个月时间。Kong已经提供了这些能力。
### 方案B: Spring Cloud Gateway
**优点**:
- 与现有Spring Boot服务集成好
- Java技术栈,团队熟悉
- 功能完善
**缺点**:
- 性能不如Nginx based方案
- 需要JVM,资源消耗较大
- Go服务集成需要额外工作
**为什么没选**:
我们服务不全是Java,有较多Go服务。且压测显示Spring Cloud Gateway在高并发下性能不足。
### 方案C: Traefik
**优点**:
- Cloud Native设计,K8s集成好
- 配置简单,自动服务发现
- 支持多种协议
**缺点**:
- 插件生态不如Kong
- 在大规模场景下的实践案例较少
- 限流等高级功能需要企业版
**为什么没选**:
我们需要的高级功能(精细化限流、自定义认证)在开源版本不完善,而企业版价格较高。
### 方案D: 自研网关
**优点**:
- 完全定制化
- 无License限制
- 性能可优化到极致
**缺点**:
- 开发周期长(6个月+)
- 维护成本高
- 缺乏生态和社区
**为什么没选**:
时间和资源不允许,且自研网关不是公司核心竞争力。
## 实施计划
### 第一阶段(2周)
- [x] 搭建Kong开发环境
- [x] POC验证核心功能
- [x] 性能压测
- [x] 编写部署文档
### 第二阶段(3周)
- [ ] 生产环境部署(3实例+PostgreSQL主备)
- [ ] 配置JWT认证插件
- [ ] 配置限流插件
- [ ] 配置Prometheus监控
### 第三阶段(4周)
- [ ] 迁移用户服务API
- [ ] 迁移订单服务API
- [ ] 迁移支付服务API
- [ ] 灰度发布,监控指标
### 第四阶段(持续)
- [ ] 陆续迁移其他服务
- [ ] 优化性能配置
- [ ] 开发自定义插件
## 成功指标
- Kong可用性 ≥ 99.9%
- P99延迟 < 50ms(含网关处理时间)
- 支持峰值5万QPS
- 零安全事故
- API统一管理覆盖率 100%
## 参考资料
- [Kong官方文档](https://docs.konghq.com/)
- [Kong vs 其他网关对比](https://konghq.com/learning-center/api-gateway/comparison)
- [内部POC报告](./poc-reports/kong-poc-2024-01.md)
- [性能测试报告](./test-reports/kong-performance-test.md)
## 元数据
- **作者**: 张三 ([email protected])
- **日期**: 2024-01-15
- **评审者**: 李四(架构师), 王五(安全专家), 赵六(运维负责人)
- **状态**: 已接受
- **相关Issue**: ARCH-123, ARCH-124
- **实施负责人**: 张三
- **预计完成时间**: 2024-03-15案例2: 数据库分库分表方案
# ADR-2024-005: 订单表分库分表方案
## 状态
已接受 (2024-02-20)
## 背景
订单表(orders)已经达到5000万行,查询性能急剧下降:
**当前问题**:
1. 查询慢:普通查询3-5秒,分页查询10秒+
2. 写入慢:插入订单耗时500ms+
3. 备份困难:全量备份需要6小时
4. 容量瓶颈:单表即将达到MySQL推荐上限
**业务特征**:
- 订单每日新增50万
- 预计3年内达到5亿订单
- 主要查询:
- 按order_id查询(90%)
- 按user_id查询(8%)
- 按时间范围查询(2%)
**约束条件**:
- 不能停服
- 历史数据需要保留
- 需要支持跨分片查询
## 决策
采用 **按user_id分库 + 按时间分表** 的方案:
- **分库策略**: 8个数据库,user_id % 8
- **分表策略**: 每月一张表,orders_202401, orders_202402...
- **中间件**: 使用ShardingSphere-JDBC
- **冷热分离**: 6个月以上数据归档到历史库┌─────────────────────────────────────────────┐ │ 分库分表架构 │ ├─────────────────────────────────────────────┤ │ │ │ ┌──────────┐ │ │ │ App │ │ │ └────┬─────┘ │ │ │ │ │ ┌────▼────────────┐ │ │ │ ShardingSphere │ │ │ └─────┬───────────┘ │ │ │ │ │ ┌─────┴─────┬─────────┬────────┐ │ │ │ │ │ │ │ │ ┌──▼───┐ ┌───▼──┐ ┌───▼──┐ ┌──▼───┐ │ │ │ DB0 │ │ DB1 │ │ DB2 │ │ DB7 │ │ │ │ ┌──┐ │ │ ┌──┐ │ │ ┌──┐ │ │ ┌──┐ │ │ │ │ │01│ │ │ │01│ │ │ │01│ │ │ │01│ │ │ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ │ │ │02│ │ │ │02│ │ │ │02│ │ │ │02│ │ │ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ │ │ │12│ │ │ │12│ │ │ │12│ │ │ │12│ │ │ │ │ └──┘ │ │ └──┘ │ │ └──┘ │ │ └──┘ │ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ └─────────────────────────────────────────────┘
## 理由
1. **user_id分库**: 90%的查询都包含user_id,数据亲和性好
2. **时间分表**: 订单有明显的时间属性,老订单查询少
3. **性能提升**:
- 单表行数:5000万 → 600万(假设月均)
- 查询时间:5秒 → 100ms(预估)
- 写入时间:500ms → 50ms(预估)
4. **扩展性**:
- 垂直:单库可升级配置
- 水平:可增加到16库、32库
5. **运维友好**:
- 备份:分库并行备份,1小时完成
- 归档:按月清理老表
- 监控:分库独立监控
## 后果
### 正面影响
1. 查询性能大幅提升
2. 数据增长可持续支撑
3. 备份恢复时间大幅缩短
4. 单点故障影响范围缩小
### 负面影响
1. **跨库事务困难**
- 缓解:避免跨库事务,使用最终一致性
2. **跨库查询性能差**
- 缓解:禁止不带user_id的查询,使用ES做全局搜索
3. **运维复杂度上升**
- 缓解:自动化脚本创建分表,监控告警
4. **数据迁移风险**
- 缓解:双写+校验+分批切换
### 风险和缓解措施
- **风险1**: 数据迁移过程中数据不一致
- 缓解:双写验证,自动对账脚本
- **风险2**: user_id分布不均匀导致数据倾斜
- 缓解:分析user_id分布,必要时调整分片算法
- **风险3**: 全局ID生成冲突
- 缓解:使用雪花算法,数据中心ID区分
## 替代方案
### 方案A: 只分表不分库
**优点**: 实施简单,无需多数据源
**缺点**: 单库瓶颈仍存在,扩展性有限
**为什么没选**: 无法解决单库连接数和IO瓶颈
### 方案B: 按order_id分库
**优点**: 数据分布均匀
**缺点**: 按user_id查询需要扫描所有库
**为什么没选**: 不符合查询模式
### 方案C: 迁移到NoSQL(MongoDB)
**优点**: 天然支持分片,扩展性好
**缺点**:
- 需要重写所有SQL
- 事务支持弱
- 团队不熟悉
**为什么没选**: 改造成本太高,风险大
## 实施计划
### 阶段1: 准备(2周)
- [ ] 搭建ShardingSphere开发环境
- [ ] 编写分库分表配置
- [ ] 开发双写逻辑
- [ ] 编写数据校验脚本
### 阶段2: 验证(2周)
- [ ] 在测试环境完整迁移
- [ ] 压力测试
- [ ] 数据一致性验证
### 阶段3: 迁移(4周)
- [ ] 开启双写(新数据写入分片)
- [ ] 历史数据分批迁移
- [ ] 实时对账验证
- [ ] 灰度切换读流量
### 阶段4: 收尾(1周)
- [ ] 全量切换
- [ ] 关闭双写
- [ ] 清理老表
- [ ] 优化监控
## 成功指标
- 查询P99 < 100ms
- 写入P99 < 50ms
- 数据一致性 100%
- 迁移过程零故障
## 参考资料
- [ShardingSphere文档](https://shardingsphere.apache.org/)
- [分库分表实践案例](internal-doc/sharding-best-practice.md)
- [数据迁移方案](internal-doc/data-migration-plan.md)
## 元数据
- **作者**: 李四
- **日期**: 2024-02-20
- **评审者**: 张三, 王五, DBA团队
- **相关ADR**: ADR-2024-003 (分布式ID生成)案例3: 缓存策略选择
# ADR-2024-008: 商品详情页缓存策略
## 状态
已接受 (2024-03-10)
## 背景
商品详情页是流量最大的页面,日均PV 1000万,数据库压力巨大。
**现状**:
- 直接查询MySQL,P99响应时间800ms
- 高峰期数据库连接数打满
- 数据库主从延迟导致数据不一致
**需求**:
- 响应时间 < 100ms
- 支持百万级QPS
- 数据实时性要求:1分钟内
## 决策
采用 **三级缓存架构**:┌────────────────────────────────────────────┐ │ 三级缓存架构 │ ├────────────────────────────────────────────┤ │ │ │ 请求 ──▶ CDN (L1) │ │ │ Miss │ │ ▼ │ │ Nginx本地缓存 (L2) │ │ │ Miss │ │ ▼ │ │ Redis (L3) │ │ │ Miss │ │ ▼ │ │ MySQL │ │ │ └────────────────────────────────────────────┘
**策略**:
1. **L1 - CDN**: 静态资源+页面HTML,TTL 5分钟
2. **L2 - Nginx**: 热点商品,TTL 1分钟,LRU淘汰
3. **L3 - Redis**: 全量商品,TTL 10分钟
4. **更新**: 商品修改时主动清除缓存
## 理由
1. **性能**: 三级缓存确保99%请求不打DB
2. **成本**: CDN承载静态资源,节省带宽
3. **实时性**: 1分钟TTL满足业务需求
4. **高可用**: 缓存失效时可降级查DB
## 后果
### 正面影响
- DB压力下降95%
- 响应时间从800ms降至50ms
- 支持10倍流量增长
### 负面影响
- 缓存击穿风险 → 使用Singleflight防止
- 缓存不一致 → 主动清除+TTL兜底
- 运维复杂度 → 统一配置管理
## 元数据
- **作者**: 王五
- **日期**: 2024-03-10
- **状态**: 已实施案例4: 技术债务决策
# ADR-2024-012: 暂不重构遗留支付模块
## 状态
已接受 (2024-04-05)
## 背景
支付模块是5年前开发的遗留代码,存在以下问题:
- 代码混乱,耦合严重
- 测试覆盖率不足30%
- 使用过时的框架
- 每次修改都可能引入bug
技术团队强烈要求重构,预计需要3个月。
## 决策
**暂不重构**,而是:
1. 冻结功能,只修bug
2. 增加集成测试
3. 文档化关键逻辑
4. 列入下半年规划
## 理由
1. **时间紧迫**: Q2有重要业务目标,无法抽调人力
2. **风险高**: 支付是核心模块,重构风险极大
3. **可控**: 当前虽不优雅但运行稳定
4. **ROI低**: 重构3个月vs每月1-2个bug修复
## 后果
### 正面影响
- 资源集中于业务目标
- 避免重构引入新bug的风险
### 负面影响
- 技术债务继续累积
- 团队士气可能受影响
### 缓解措施
- 明确重构时间(Q3),给团队希望
- 暂时增加代码审查强度
- 记录所有workaround,方便重构时参考
## 替代方案
### 方案A: 立即重构
**为什么没选**: 时间不允许,风险太高
### 方案B: 渐进式重构
**为什么没选**: 支付模块耦合太严重,难以渐进
### 方案C: 微服务拆分
**为什么没选**: 比重构更复杂,时间更长
## 重新评估条件
如果出现以下情况,重新评估决策:
- 支付bug频率超过5个/月
- 出现严重生产事故
- Q2业务目标提前完成
## 元数据
- **作者**: 张三
- **日期**: 2024-04-05
- **复审日期**: 2024-07-01
- **状态**: 已接受(附条件)案例5: 安全架构决策
# ADR-2024-015: 零信任网络架构
## 状态
已接受 (2024-05-01)
## 背景
公司发生了一次内网渗透事件,暴露了传统"城堡-护城河"安全模型的缺陷:
- 内网默认信任,一旦突破边界即可横向移动
- 缺少细粒度访问控制
- 无法识别异常内部流量
**新的安全要求**:
- 最小权限原则
- 持续验证
- 微隔离
## 决策
实施 **零信任网络架构 (Zero Trust)**:┌──────────────────────────────────────────────┐ │ 零信任架构 │ ├──────────────────────────────────────────────┤ │ │ │ 每个请求都需要: │ │ 1. 身份认证 (mTLS) │ │ 2. 设备认证 (Device Trust) │ │ 3. 上下文验证 (IP/Time/Behavior) │ │ 4. 权限检查 (RBAC/ABAC) │ │ 5. 持续监控 (Audit Log) │ │ │ │ ┌─────┐ verify ┌──────────┐ │ │ │User │───────────▶│ Policy │ │ │ └─────┘ │ Engine │ │ │ └──────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Service A │ Service B │ DB │ │ │ └──────────────────────────────┘ │ │ │ └──────────────────────────────────────────────┘
**实施**:
1. 服务间通信启用mTLS (Istio)
2. 实施RBAC策略 (OPA)
3. 部署网络策略 (NetworkPolicy)
4. 集中审计日志 (ELK)
## 理由
1. **安全性**: 即使突破边界也无法横向移动
2. **合规**: 满足等保2.0要求
3. **可见性**: 所有访问可审计
4. **灵活性**: 细粒度权限控制
## 后果
### 正面影响
- 安全性大幅提升
- 满足合规要求
- 异常行为可快速发现
### 负面影响
- 性能开销:mTLS增加5-10ms延迟
- 复杂度:证书管理、策略配置
- 学习成本:团队需要培训
### 实施计划
**阶段1 (2个月)**:
- 部署Service Mesh (Istio)
- 启用mTLS
**阶段2 (1个月)**:
- 配置RBAC策略
- 集成身份提供商
**阶段3 (1个月)**:
- 部署审计日志系统
- 配置告警规则
## 元数据
- **作者**: 赵六 (安全团队)
- **日期**: 2024-05-01
- **评审者**: 架构组、安全组、运维组
- **预算**: $150,000
- **预计完成**: 2024-09-01工具支持
ADR命令行工具
# adr_cli.py
import os
import re
from datetime import datetime
from pathlib import Path
class ADRManager:
"""ADR管理工具"""
def __init__(self, adr_dir: str = "./docs/adr"):
self.adr_dir = Path(adr_dir)
self.adr_dir.mkdir(parents=True, exist_ok=True)
def create_adr(self, title: str, status: str = "提议中") -> str:
"""创建新ADR"""
# 生成ADR编号
adr_number = self._get_next_number()
filename = f"ADR-{adr_number:04d}-{self._slugify(title)}.md"
filepath = self.adr_dir / filename
# 生成内容
content = self._generate_template(adr_number, title, status)
# 写入文件
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"创建ADR: {filepath}")
return str(filepath)
def list_adrs(self, status_filter: str = None) -> list:
"""列出所有ADR"""
adrs = []
for file in sorted(self.adr_dir.glob("ADR-*.md")):
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取元数据
number_match = re.search(r'ADR-(\d+)', file.name)
title_match = re.search(r'# ADR-\d+: (.+)', content)
status_match = re.search(r'## 状态\s+(.+)', content)
if all([number_match, title_match, status_match]):
status = status_match.group(1).strip()
if status_filter and status != status_filter:
continue
adrs.append({
'number': int(number_match.group(1)),
'title': title_match.group(1).strip(),
'status': status,
'file': str(file)
})
return adrs
def supersede_adr(self, old_number: int, new_title: str):
"""替代旧ADR"""
# 创建新ADR
new_file = self.create_adr(new_title, "已接受")
# 更新旧ADR状态
old_files = list(self.adr_dir.glob(f"ADR-{old_number:04d}-*.md"))
if old_files:
old_file = old_files[0]
with open(old_file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取新ADR编号
new_number = self._get_current_number()
# 更新状态
content = re.sub(
r'## 状态\s+(.+)',
f'## 状态\n已替代\n\n被 [ADR-{new_number:04d}]({Path(new_file).name}) 替代',
content
)
with open(old_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"ADR-{old_number:04d} 已标记为已替代")
def generate_index(self) -> str:
"""生成ADR索引"""
adrs = self.list_adrs()
index = "# 架构决策记录索引\n\n"
# 按状态分组
by_status = {}
for adr in adrs:
status = adr['status']
if status not in by_status:
by_status[status] = []
by_status[status].append(adr)
# 生成索引
for status in ['已接受', '提议中', '已废弃', '已拒绝', '已替代']:
if status in by_status:
index += f"\n## {status}\n\n"
for adr in by_status[status]:
filename = Path(adr['file']).name
index += f"- [ADR-{adr['number']:04d}]({filename}): {adr['title']}\n"
# 写入索引文件
index_file = self.adr_dir / "README.md"
with open(index_file, 'w', encoding='utf-8') as f:
f.write(index)
print(f"生成索引: {index_file}")
return index
def _get_next_number(self) -> int:
"""获取下一个ADR编号"""
current = self._get_current_number()
return current + 1
def _get_current_number(self) -> int:
"""获取当前最大ADR编号"""
max_number = 0
for file in self.adr_dir.glob("ADR-*.md"):
match = re.search(r'ADR-(\d+)', file.name)
if match:
number = int(match.group(1))
max_number = max(max_number, number)
return max_number
def _slugify(self, text: str) -> str:
"""转换为文件名友好格式"""
# 简化版,实际应该更完善
text = text.lower()
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[-\s]+', '-', text)
return text[:50] # 限制长度
def _generate_template(self, number: int, title: str, status: str) -> str:
"""生成ADR模板"""
today = datetime.now().strftime('%Y-%m-%d')
return f"""# ADR-{number:04d}: {title}
## 状态
{status}
## 背景 (Context)
描述需要做决策的背景和问题。
## 决策 (Decision)
我们将要采取的决策。
## 理由 (Rationale)
为什么做这个决策。
## 后果 (Consequences)
### 正面影响
-
### 负面影响
-
### 风险和缓解措施
-
## 替代方案 (Alternatives)
### 方案A:
- 优点:
- 缺点:
- 为什么没选:
## 参考资料 (References)
-
## 元数据
- 作者:
- 日期: {today}
- 评审者:
- 相关ADR:
- 相关Issue:
"""
# CLI接口
if __name__ == "__main__":
import sys
manager = ADRManager()
if len(sys.argv) < 2:
print("用法:")
print(" python adr_cli.py new <标题> # 创建新ADR")
print(" python adr_cli.py list [状态] # 列出ADR")
print(" python adr_cli.py supersede <编号> <新标题> # 替代旧ADR")
print(" python adr_cli.py index # 生成索引")
sys.exit(1)
command = sys.argv[1]
if command == "new" and len(sys.argv) >= 3:
title = " ".join(sys.argv[2:])
manager.create_adr(title)
elif command == "list":
status_filter = sys.argv[2] if len(sys.argv) >= 3 else None
adrs = manager.list_adrs(status_filter)
for adr in adrs:
print(f"ADR-{adr['number']:04d} [{adr['status']}] {adr['title']}")
elif command == "supersede" and len(sys.argv) >= 4:
old_number = int(sys.argv[2])
new_title = " ".join(sys.argv[3:])
manager.supersede_adr(old_number, new_title)
elif command == "index":
manager.generate_index()
else:
print("未知命令")
sys.exit(1)使用示例
# 创建新ADR
python adr_cli.py new "选择消息队列技术"
# 列出所有ADR
python adr_cli.py list
# 列出特定状态的ADR
python adr_cli.py list "已接受"
# 替代旧ADR
python adr_cli.py supersede 5 "改用RocketMQ作为消息队列"
# 生成索引
python adr_cli.py index最佳实践
ADR编写原则
- 简洁明了 - 不要写长篇大论,关键信息突出
- 决策为主 - 重点记录"是什么"和"为什么",而非"怎么做"
- 及时记录 - 决策做出时立即记录,不要事后补充
- 保持不变 - ADR一旦接受就不应修改,新决策创建新ADR
- 团队共识 - ADR应该经过团队讨论和评审
ADR管理建议
- 集中存储 - 所有ADR放在代码仓库的docs/adr目录
- 版本控制 - ADR作为代码一部分,使用Git管理
- 定期评审 - 季度回顾ADR,更新状态
- 索引目录 - 维护README索引,方便查找
- 模板规范 - 统一使用标准模板
常见错误
- 记录实现细节 - ADR关注决策,不是实现文档
- 追求完美 - 不需要把所有细节都写清楚
- 害怕犯错 - 决策可以被替代,记录错误决策也有价值
- 堆积不写 - 决策后立即记录,不要拖延
- 只有架构师写 - 任何有决策的人都可以写ADR
总结
ADR是一种轻量级但非常有价值的架构文档实践。通过记录架构决策的背景、理由和后果,可以:
- 保留架构知识,避免团队失忆
- 让新人快速理解系统演进历史
- 为技术债务提供可追溯的证据
- 促进团队对架构的共同理解
关键要点:
- 简洁高效 - 模板简单,快速记录
- 决策导向 - 关注"为什么"而非"怎么做"
- 持续维护 - 纳入日常工作流程
- 工具支持 - 使用脚本或工具简化管理
- 团队文化 - 让写ADR成为团队习惯
进阶阅读:
💬 讨论
使用 GitHub 账号登录后即可参与讨论