架构评审清单
架构评审针对系统级决策——模块划分、技术选型、数据流、扩展性、可靠性。它和设计评审的区别在于:设计评审关注「这个功能怎么做」,架构评审关注「系统怎么组织才能长期健康」。
要点
- 架构评审针对系统级决策:模块划分、技术选型、数据流、扩展性
- 评审前准备:系统现状图、问题陈述、目标架构、迁移路径、技术选型对比
- 重点检查:模块边界、数据流向、单点故障、可观测性、部署回滚
- 针对当前规模设计,但保留清晰的扩展路径
1. 适用时机
- 新系统/新服务启动
- 现有系统的重大重构(拆分、合并、迁移)
- 引入新的中间件或基础设施(数据库、消息队列、缓存层)
- 系统扩展性出现瓶颈,需要重新设计
- 跨多个团队的协同设计
不需要架构评审的场景:
- 单模块内部的设计(属于设计评审)
- 纯配置调整
- 第三方服务的常规使用(不是引入新类型的服务)
2. 评审前准备
- 系统现状图:当前系统的模块划分、数据流、关键依赖
- 问题陈述:现有架构哪里不够用?瓶颈在哪?
- 目标架构:期望达到的目标状态,画清楚
- 迁移路径:从现状到目标,分几步走?每步的风险?
- 技术选型:选了什么技术?为什么?对比过哪些替代方案?
- 容量估算:QPS、数据量、存储、带宽的预期规模
- 成本估算:基础设施成本、人力成本、迁移成本
3. 核心检查项
3.1 模块与边界
- 模块职责单一,没有「一个模块干所有事」
- 模块之间边界清楚,依赖方向单向(避免循环依赖)
- 关键接口有明确契约(输入输出类型、错误码、SLA)
- 没有「只有作者知道怎么改」的模块
- 共享逻辑抽到合适的位置(公共库 vs 各自复制)
Hono 示例:模块边界
// 好的模块边界
// apps/api/src/
// ├── modules/
// │ ├── orders/
// │ │ ├── orders.router.ts # 路由层
// │ │ ├── orders.service.ts # 业务层
// │ │ ├── orders.repository.ts # 数据层
// │ │ └── orders.types.ts # 类型定义
// │ └── users/
// │ └── ...
// └── shared/
// ├── middleware/ # 共享中间件
// └── utils/ # 共享工具
// orders.router.ts - 路由层只做路由装配
import { Hono } from 'hono'
import { OrdersService } from './orders.service'
export const ordersRouter = new Hono()
ordersRouter.post('/', async (c) => {
const body = await c.req.json()
const service = new OrdersService(c.env)
const order = await service.create(body)
return c.json(order, 201)
})
// orders.service.ts - 业务层处理业务逻辑
export class OrdersService {
private env: Env
constructor(env: Env) {
this.env = env
}
async create(input: CreateOrderInput): Promise<Order> {
// 业务逻辑:校验库存、计算价格、创建订单
// 不直接操作数据库,通过 repository
}
}
// orders.repository.ts - 数据层只操作数据库
export class OrdersRepository {
private db: D1Database
constructor(db: D1Database) {
this.db = db
}
async findById(id: string): Promise<Order | null> {
return this.db.prepare('SELECT * FROM orders WHERE id = ?')
.bind(id)
.first()
}
}循环依赖检查
// 坏的依赖关系:循环依赖
// orders.service.ts 依赖 users.service.ts
// users.service.ts 依赖 orders.service.ts
// 解决方法:引入事件或接口层
// 1. 事件驱动
import { EventBus } from '../shared/event-bus'
export class OrdersService {
async create(input: CreateOrderInput) {
const order = await this.createOrder(input)
// 发布事件,而不是直接调用 UserService
await this.eventBus.publish('order.created', {
orderId: order.id,
userId: order.userId
})
}
}
// 2. 接口层
interface UserNotifier {
notifyOrderCreated(userId: string, orderId: string): Promise<void>
}
export class OrdersService {
private notifier: UserNotifier
constructor(notifier: UserNotifier) {
this.notifier = notifier
}
async create(input: CreateOrderInput) {
const order = await this.createOrder(input)
await this.notifier.notifyOrderCreated(order.userId, order.id)
}
}3.2 数据流与存储
- 数据流向清楚,没有「不知道从哪来、不知道到哪去」的数据
- 数据所有权明确(谁写入、谁读取、谁拥有真实来源)
- 一致性模型清楚(强一致还是最终一致?哪些场景需要哪种?)
- 数据存储选型合理(关系型 vs 文档型 vs KV vs 时序 vs 向量)
- 数据量增长预期清楚,存储方案能撑住
Hono 示例:数据所有权
// 数据所有权:每个实体只有一个服务可以写入
// orders-service - 拥有订单数据
export class OrdersService {
async createOrder(input: CreateOrderInput) {
// 只有这个服务可以写入 orders 表
return this.db.prepare('INSERT INTO orders ...')
}
async getOrder(id: string) {
return this.db.prepare('SELECT * FROM orders WHERE id = ?')
}
}
// users-service - 拥有用户数据
export class UsersService {
async getUser(id: string) {
return this.db.prepare('SELECT * FROM users WHERE id = ?')
}
}
// 跨服务查询:通过 API 调用,而不是直接访问数据库
export class OrdersService {
async getOrderWithUser(orderId: string) {
const order = await this.getOrder(orderId)
// 通过 API 调用获取用户信息
const user = await this.httpClient.get(`/users/${order.userId}`)
return { ...order, user }
}
}3.3 技术选型
- 选型理由基于具体约束,不是「因为流行」或「简历上好看」
- 团队有能力维护这个技术栈(不是只有一个人懂)
- 社区活跃度、商业支持、许可证风险已评估
- 与现有技术栈集成成本可接受
- 考虑了退出成本(如果选错了,换掉有多难?)
技术选型决策记录
## 决策:选择 Cloudflare D1 作为主数据库
### 背景
- 应用部署在 Cloudflare Workers
- 需要关系型数据库存储订单、用户数据
- 预期数据量:< 1000 万行
### 考虑的方案
1. **Cloudflare D1**
- 优势:与 Workers 深度集成、零运维、按用量计费
- 劣势:单region、只读副本有限
- 成本:$0.50/GB/月
2. **PlanetScale (MySQL)**
- 优势:功能丰富、多 region
- 劣势:需要额外运维、冷启动延迟
- 成本:$39/月起
3. **Supabase (PostgreSQL)**
- 优势:功能丰富、实时订阅
- 劣势:冷启动延迟、需要管理连接池
- 成本:$25/月起
### 决策
选择 D1。
### 理由
1. 应用已在 Workers,D1 集成最紧密
2. 数据量 < 1000 万行,D1 容量足够
3. 不需要多 region 写入
4. 成本最低
5. 团队有 SQLite 经验
### 退出成本
如果未来需要多 region,可以迁移到 PlanetScale,
数据导出为 SQL 文件即可。3.4 扩展性
- 水平扩展路径清楚(加机器能线性提升性能吗?)
- 单点故障已识别并有应对
- 有状态组件的扩展方案(数据库分片、缓存集群)
- 热点数据/热点用户的处理方案
- 未来 3-6 个月的扩展需求能支持
Hono 示例:水平扩展
// Cloudflare Workers 自动水平扩展
// 但需要注意:无状态设计
import { Hono } from 'hono'
const app = new Hono()
// 好的设计:无状态
app.get('/orders/:id', async (c) => {
const orderId = c.req.param('id')
// 从 D1 读取,任何实例都能处理
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first()
return c.json(order)
})
// 坏的设计:有状态(内存缓存)
const cache = new Map() // 每个 Worker 实例有自己的缓存
app.get('/users/:id', async (c) => {
const userId = c.req.param('id')
// 不同实例的缓存不共享
if (cache.has(userId)) {
return c.json(cache.get(userId))
}
const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first()
cache.set(userId, user) // 只在这个实例有效
return c.json(user)
})
// 正确做法:使用 KV 或 Cache API
app.get('/users/:id', async (c) => {
const userId = c.req.param('id')
// 使用 KV 作为缓存
const cacheKey = `user:${userId}`
let user = await c.env.KV.get(cacheKey, 'json')
if (!user) {
user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first()
// 缓存 5 分钟
await c.env.KV.put(cacheKey, JSON.stringify(user), {
expirationTtl: 300
})
}
return c.json(user)
})3.5 可靠性与容错
- 下游依赖失败时,本模块如何处理(超时、降级、熔断)
- 重试策略合理(指数退避、最大重试次数、幂等保证)
- 异步操作有失败处理(死信队列、告警、补偿)
- 关键数据有备份和恢复方案
- 故障演练计划(定期测试容错机制是否真的有效)
Hono 示例:重试与降级
import { Hono } from 'hono'
const app = new Hono()
// 带重试的外部调用
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(5000) // 5 秒超时
})
if (response.ok) {
return response
}
// 5xx 错误才重试
if (response.status >= 500) {
throw new Error(`HTTP ${response.status}`)
}
// 4xx 错误不重试
return response
} catch (error) {
if (i === maxRetries - 1) {
throw error
}
// 指数退避:1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('Max retries exceeded')
}
// 降级策略
app.get('/products/recommendations', async (c) => {
try {
// 尝试调用推荐服务
const recommendations = await fetchWithRetry(
'https://recommendation-service.internal/api/recommend'
)
return c.json(await recommendations.json())
} catch (error) {
// 降级:返回热门推荐
console.error('Recommendation service failed:', error)
const fallback = await c.env.KV.get('recommendations:fallback', 'json')
return c.json(fallback || [])
}
})3.6 安全边界
- 网络边界清楚(哪些对外、哪些对内、哪些是 DMZ)
- 认证和授权机制统一(不是每个服务各搞一套)
- 敏感数据识别清楚,加密存储和传输
- 攻击面最小化(不暴露不必要的接口、端口、信息)
- 审计日志完整(能追溯关键操作)
3.7 可观测性
- 关键业务指标有监控(订单量、注册数、转化率)
- 系统指标有监控(CPU、内存、QPS、延迟、错误率)
- 分布式追踪接入(能看到一个请求在所有服务间的流转)
- 日志规范统一(结构化、分级、trace id 贯穿)
- 告警规则合理(不漏报、不乱报)
3.8 部署与运维
- 部署方式清楚(蓝绿、金丝雀、滚动更新)
- 回滚方案明确(回滚到哪个版本、需要多久、数据怎么办)
- 配置管理统一(环境变量、密钥、功能开关)
- 发布流程自动化(CI/CD 覆盖、无手工步骤)
- 运维文档完整(部署、扩缩容、故障处理)
3.9 演进与迁移
- 从现状到目标的迁移路径分阶段,每段可独立交付
- 新旧系统能并行运行一段时间(灰度切换)
- 数据迁移方案完整(旧数据怎么处理、双写还是迁移)
- 回退路径存在(迁移失败能退回原系统)
- 迁移成本可接受(不是「推倒重来」)
4. 架构评审的常见问题
架构评审容易陷入两种极端:
- 过度设计:为了「未来可能」的需求,引入复杂架构。评审时问「这个复杂度现在就需要吗?」
- 过度简化:为了「先上线」,忽略关键的非功能需求。评审时问「上线三个月后会遇到什么问题?」
好的架构评审应该:
- 针对当前规模设计,但保留清晰的扩展路径
- 复杂度的引入要有具体场景支撑,不是「预防性」的
- 取舍明确记录,未来维护者能理解当时的决策背景
5. 通过标准
- 系统边界和模块职责清晰
- 关键决策有对比和取舍记录
- 单点故障已识别,有应对措施
- 数据流向和一致性模型清楚
- 可观测性方案完整
- 部署、回滚、迁移方案可行
- 评审意见已记录,有跟进人和截止时间
6. 维护建议
架构评审清单需要随技术栈演进而更新:
- 引入新类型的基础设施(如向量数据库、边缘计算)时,补充相关检查项
- 发生过架构相关的事故后,把根因对应的检查项加进来
- 团队规模变化时,调整模块划分和职责相关的要求
- 每半年复审一次,删除过时的检查项