系统设计文档怎么写:让评审者看见边界和风险

0阅读16分钟

从一次翻车说起

去年下半年,我参与了一个支付渠道切换的项目。技术方案写了四十多页,架构图画了三张,接口定义详细到每个字段的类型。评审会上大家都说「写得挺全的」,顺利通过。

上线第二周,第三方支付通道出现了一次 30 秒的超时抖动。我们的系统在超时后重复发起了扣款请求,对端没有做幂等校验,导致同一笔订单被扣了两次。用户在群里炸了锅,客服工单堆了 200 多条。

事后复盘,我发现那份四十页的技术方案里,「失败路径」只有一个自然段,写的是「异常情况做重试处理」。重试几次?间隔多久?超时后怎么办?对端不幂等怎么兜底?全都没有。

这件事让我重新审视系统设计文档到底该写什么。不是因为写得不够多,而是写的内容没有帮评审者看见真正的风险。

文档的读者决定了写法

在动手写之前,我会先问自己一个问题:这份文档的读者是谁?

Google 内部的设计文档有一个明确的原则——文档的受众不是作者自己,而是评审者。Tanya Reilly 在《The Staff Engineer's Path》里也强调,RFC 的核心目的是让决策过程透明化,让团队能在「信息充分」的前提下给出判断,而不是走过场。

Uber 的工程实践也印证了这一点。他们在 Scaling Engineering Teams via Writing Things Down 一文中提到,RFC 流程的核心价值不在于写出一篇完美的文档,而在于「把假设和计划提前暴露出来」,让团队能在动手之前发现那些藏在脑子里的分歧。

不同的读者关心的东西不一样:

读者角色核心关注点文档中应突出的部分
技术负责人方案是否合理、有无遗漏关键风险架构图、备选方案对比、非功能性约束
下游团队接口契约、变更兼容性数据流、接口定义、发布时序
SRE / 运维可观测性、降级手段、回滚方案失败路径、监控指标、回滚策略
产品经理是否解决业务问题、上线时间目标与非目标、里程碑、验收标准
未来的维护者为什么这样设计、取舍依据备选方案及淘汰原因、ADR 记录

理解读者之后,文档的详略取舍就有了判断标准。给技术负责人看的东西,不需要把每个字段的类型都列出来;给下游团队看的东西,不能只放一张架构图什么都不解释。

系统设计文档的核心章节

我总结的模板包含以下章节。不是每一篇都需要全部展开——简单功能压缩到半页也行,但「目标」「非目标」「失败路径」「验证计划」这四个,几乎不能跳过。

章节核心问题常见缺失后果
背景和目标为什么要做?做到什么程度算成?评审者无法判断方案的合理性
非目标和约束什么不做?有什么限制?范围蔓延,项目延期
架构图和数据流系统怎么组织?数据怎么走?评审者无法理解全局
接口契约上下游怎么对接?字段含义是什么?联调时反复扯皮
失败路径和降级出了问题怎么办?上线后故障扩大
备选方案为什么不选其他方案?重复讨论,决策不透明
验证和发布计划怎么证明做对了?怎么上线?上线后无法验证效果
开放问题还有什么没想清楚的?伪装成已决策,留下隐患

背景和目标

背景不是走过场。它要做的事情是:一个不了解上下文的工程师,读完这段就能理解为什么要做这件事。

腾讯云技术社区的一篇文章提到,背景部分最常见的毛病是「知识的诅咒」——作者默认读者知道所有的历史包袱和业务约束,写出来的背景对局外人来说毫无信息量。

好的背景段应该包含:当前系统是什么样的、哪里出了问题、为什么要现在改。

# 坏例子:模糊的背景描述
## 背景
随着业务的快速发展,现有支付系统已经无法满足日益增长的需求,
需要进行升级优化,以提升系统的稳定性和性能。

# 好例子:具体的背景描述
## 背景
当前支付系统通过单一通道(XX 支付)处理所有扣款请求,日均交易量
120 万笔。近三个月该通道出现 4 次 P1 故障,累计影响时长 6 小时,
直接导致资损 ¥38 万。业务侧要求在 Q3 完成多通道接入,主通道故障时
能在 30 秒内自动切换到备用通道。

目标要可验证。「提升系统性能」不是目标,「P99 延迟从 800ms 降到 200ms 以内」才是目标。

# 坏例子:不可验证的目标
## 目标
- 提升系统性能
- 增强系统稳定性
- 优化用户体验

# 好例子:可验证的目标
## 目标
- 支付接口 P99 延迟从 800ms 降至 200ms 以内
- 通道切换时间 < 30s,切换过程零丢单
- 系统可用性从 99.9% 提升至 99.95%(月度)

非目标和约束

非目标(Non-Goals)是我认为最容易被忽略、也最有价值的章节。

它的作用是在评审时挡住范围蔓延。当你明确写出「本期不做国际化」「不改造底层存储引擎」,评审者就不会在会议上提出「你们是不是也应该考虑一下多语言」这类发散性问题。

# 非目标示例
## 非目标
- 本期不做多币种支持,仅在人民币通道内做多通道容灾
- 不改造现有对账系统,对账逻辑保持 T+1 不变
- 不替换数据库,继续使用 MySQL 5.7

## 约束
- 必须在现有 K8s 集群内完成,不申请新机器
- 不能使用公司尚未引入的中间件
- 发布窗口限于每周四凌晨,单次发布不超过 30 分钟

架构图和数据流

架构图是设计文档里被看得最多的部分。但很多人犯的错误是只放一张图,不做任何文字说明。

我见过的最差情况:一张包含 15 个方框和无数箭头的架构图,没有任何图例,没有说明哪个是新增的、哪个是已有的。评审者花了 10 分钟试图理解图中哪个方框代表「新增的路由服务」。

画架构图的原则:

  1. 先画高层关系,再展开内部细节,不要一张图塞下所有信息
  2. 用不同颜色或线型标注「新增」「修改」「已有」
  3. 每张图配一段文字,解释核心数据流走向
  4. 说清楚关键节点的容错方式
流程图画布 · 115%
Mermaid 流程图加载中...

这张图展示了多通道支付的核心链路。新增的路由服务和熔断器用虚线标注,数据流从客户端进入网关后,由路由服务根据通道健康状态决定走主通道还是备用通道。当通道超时或返回失败时,熔断器会触发降级,请求进入重试队列等待后续调度。

数据流和接口契约

数据流图和架构图是两个维度的东西。架构图说「有哪些组件、它们是什么关系」,数据流图说「一个请求从进来到出去,经过了哪些处理步骤、每个步骤做了什么」。

对于涉及多个系统交互的设计,我会用时序图把关键流程画出来。这不仅帮助评审者理解逻辑,也能提前暴露出接口定义中的歧义。

流程图画布 · 115%
Mermaid 流程图加载中...

这张时序图展示了正常流程和通道切换的时序。有几个关键设计决策藏在里面:

  1. 路由服务在调用通道之前先查熔断器状态,而不是直接调用——这是为了防止在已知通道异常时继续浪费请求
  2. 主通道超时后不重试主通道,直接切换——因为超时通常意味着通道侧出了问题,重试大概率还是超时
  3. 写入数据库在拿到成功响应之后——保证数据一致性,不会出现「通道没扣成功但本地记录了成功」的情况

接口契约部分,我通常会列出关键的请求和响应结构,重点标注:

  • 哪些字段是必选的,哪些是可选的
  • 枚举值的具体含义
  • 向后兼容的承诺(新增字段可以,删除或修改语义不行)
# 坏例子:模糊的接口定义
## 接口:查询用户权限
- 请求:userId(用户 ID)
- 响应:权限列表

# 好例子:明确的接口定义
## 接口:查询用户权限
- 路径:GET /api/v1/users/{userId}/permissions
- 请求参数:
  - userId(路径参数,必填,string):用户唯一标识
- 响应字段:
  - roles(array<string>,必填):用户角色列表,如 ["admin", "editor"]
  - permissions(array<string>,必填):权限点列表,如 ["user:read", "user:write"]
  - source(enum,可选,v1.2 新增):权限来源
    - "role_inherited":通过角色继承
    - "direct_granted":直接授权
    - 注意:此字段为只读,调用方不可依赖其做业务逻辑判断

接口定义中最容易被忽视的是「版本兼容性」。一个字段从 string 改成 enum、一个返回码从 200 改成 202、一个可选字段变成必填——这些看似微小的变更,都可能让下游系统崩溃。

我会在接口契约部分明确标注:本次变更中哪些是新增字段、哪些是修改字段、哪些是废弃字段。对于 breaking change,必须单独列出影响范围和迁移方案。

失败路径和降级策略

回到开头那个翻车的故事。失败路径是我最重视的章节,也是评审者最容易挑战的部分。

对于每一个外部依赖,我都会问:如果它挂了会怎样?如果它响应慢会怎样?如果它返回错误数据会怎样?

# 坏例子:一句话带过失败处理
## 异常处理
- 网络异常时自动重试
- 记录错误日志
- 必要时报警

# 好例子:逐条分析失败场景
## 失败路径分析

### 场景 1:主通道超时(> 5s)
- 触发条件:单次请求耗时超过 5 秒
- 处理方式:立即切换到备用通道发起请求
- 超时请求不自动重试,由对账系统 T+1 补偿
- 报警级别:P2,连续 3 次超时升级为 P1

### 场景 2:备用通道不可用
- 触发条件:备用通道健康检查连续失败 3 次
- 处理方式:熔断备用通道,所有请求走主通道
- 同时暂停新订单,进入「排队等待」状态
- 报警级别:P1,通知 oncall 人工介入

### 场景 3:双通道同时故障
- 处理方式:所有支付请求写入本地消息队列
- 客户端返回「支付处理中」,不做最终失败判定
- 每 5 分钟探测通道恢复情况,恢复后按顺序消费队列
- 超过 30 分钟未恢复,升级为 P0,启动人工兜底

备选方案

只写最终方案,评审者看不到你的取舍过程。至少列一个被放弃的方案,说明为什么没选。

这个章节的价值不仅在评审时,更在半年后——当有人问「当初为什么不用方案 B」的时候,你可以直接指向文档,而不用靠记忆还原当时的上下文。

## 备选方案

### 方案 A(采纳):应用层路由 + 熔断降级
- 在支付服务内部实现路由逻辑
- 优点:改动范围小,不依赖新中间件
- 缺点:路由逻辑和业务代码耦合

### 方案 B(放弃):独立路由微服务
- 新建一个支付路由服务,所有请求先到路由再到通道
- 放弃原因:引入新服务增加运维成本,当前流量
  级别(120 万 / 天)不需要独立部署
- 预期 QPS 峰值 200,独立服务的资源利用率过低

### 方案 C(放弃):Service Mesh 层面路由
- 在 Sidecar 层面做通道切换
- 放弃原因:公司 Service Mesh 尚在灰度阶段,
  支付核心链路不能依赖未稳定的基础设施

验证和发布计划

没有验证计划的设计文档等于一份建议书。我会把验证拆成三层:

验证层级验证内容执行时机
开发自测单元测试覆盖核心路由逻辑和熔断状态机代码提交前
集成测试模拟通道超时、失败、恢复场景预发布环境
灰度验证1% 流量切到新方案,观察 24 小时生产环境
全量切换确认灰度指标达标后全量放开生产环境
回滚演练关闭新路由,流量回到原通道,验证无损上线后 1 周内

发布计划要具体到步骤和检查点:

## 发布计划

### Step 1:部署路由服务和熔断器(周四 00:00)
- 仅部署,不切流量
- 检查:服务健康检查通过,日志无异常

### Step 2:1% 灰度(周四 00:30)
- 按用户 ID 尾号取模,1% 流量走新路由
- 观察指标:成功率、P99 延迟、通道切换次数
- 检查:无报警,指标波动 < 5%

### Step 3:全量切换(周五 00:00)
- 100% 流量切到新路由
- 保留旧通道配置 72 小时,不回滚

### Step 4:清理(下周三)
- 移除旧通道直连代码
- 更新运维文档

真实案例拆解

案例一:订单系统的「非目标」缺失

一个电商平台要重构订单系统。技术方案写得很详细,包括分库分表方案、读写分离架构、订单状态机改造。

问题出在没有写非目标。评审会上,有人问:「退款系统一起改吗?」作者说「这次不改退款」。又有人问:「商家的发货通知逻辑变不变?」作者说「不变」。整场评审花了 40 分钟在讨论「到底改什么」,而不是「方案本身有没有风险」。

更严重的是,因为非目标没有明确写下来,开发过程中不断有人把相关需求「顺便」塞进去。最后项目延期了两个月,超出原始范围 60%。

修复方法:在文档开头用一节明确列出「本期改什么」和「本期不改什么」,每个「不改」的项标注原因(是技术约束、业务优先级还是风险控制)。后续评审和开发中如果有人提出范围内的需求,直接指向这个列表。

案例二:数据同步的失败路径盲区

一个数据团队要做用户行为数据从 Kafka 到数据仓库的实时同步。文档里详细描述了 Flink 作业的并行度配置、Checkpoint 策略、数据 schema 设计。

但对「数据延迟」这个关键失败模式,只写了一句「正常情况下延迟在秒级」。

上线后发现:上游 Kafka 分区扩缩容时,Flink 消费组 rebalance 会导致 3-5 分钟的消费暂停。这段时间内数据仓库的数据是过期的,但下游的实时报表没有标记「数据可能不完整」,运营基于过期数据做了决策,导致一次严重的投放失误。

修复方法:在失败路径章节增加「消费延迟」场景,定义延迟告警阈值(30 秒、3 分钟、10 分钟三级),下游报表在延迟超过 1 分钟时自动打上「数据延迟」水印。同时增加 Flink rebalance 事件的监控,在 rebalance 期间自动暂停下游数据消费。

案例三:接口文档和实现不同步

一个中台团队对外提供用户权限查询接口。设计文档里写的是:「返回用户的角色列表和权限点列表」。

实现时,开发同学加了一个「权限来源」字段,标注权限是通过角色继承还是直接授权的。这个字段没有在文档中说明,也没有在评审中提及。

三个月后,接入方基于这个「隐藏字段」做了大量业务逻辑。当中台想修改这个字段的语义时,发现有 5 个下游系统依赖它,改动影响面远超预期。

修复方法:接口变更必须在设计文档中同步,并经过评审。即使只是「加了一个字段」,也要走文档更新流程。长期来看,接口契约应该用 OpenAPI Schema 管理,CI 流程中自动检查实现和文档的一致性。

文档质量自检清单

写完文档之后,我会按这个清单过一遍。不是每一条都必须满足,但如果某一条明显缺失,通常意味着文档还需要补充。

写之前

  • 明确了文档的读者是谁,不同读者关心的重点不同
  • 确认了问题的背景和影响范围,不是凭感觉觉得「该做」
  • 和至少一个相关方(产品 / SRE / 下游团队)对齐了需求理解

写的时候

  • 目标可验证——每个目标都有具体的数字或可判断的标准
  • 非目标至少列出 3 条,明确标注「不改」的原因
  • 架构图有文字说明,不是一张裸图
  • 每个外部依赖都分析了「如果它挂了会怎样」
  • 备选方案至少列了一个,写清楚为什么放弃
  • 接口变更考虑了向后兼容,标注了 breaking change
  • 发布计划具体到步骤、时间窗口和回滚操作

写完之后

  • 找一个不了解项目的人读一遍,看能否理解核心决策
  • 检查数据引用是否有来源,避免「性能提升明显」这类模糊表述
  • 确认所有开放问题都有标注,不伪装成已决策
  • 文档长度适中——如果超过 20 页,考虑是否需要拆分

一些实操建议

关于详略

不是每个系统都需要二十页设计文档。一个内部工具的 CRUD 改造,半页就够。一个涉及资金链路的核心系统改造,可能五十页都嫌少。

判断标准不是系统复杂度本身,而是「做错了之后代价有多大」。代价越大,文档越详细。

变更影响建议文档量必写章节可选章节
单服务内部重构1-2 页目标、方案、验证备选方案
跨服务调用变更3-5 页目标、架构图、接口契约、失败路径备选方案、发布计划
核心链路改造10-20 页全部章节性能测试方案、灰度策略
基础设施级变更20+ 页全部章节 + ADR回滚演练记录、容量规划

关于评审效率

评审会不是用来读文档的。如果评审者在会上才开始读文档,那文档的提前分发就没做到位。

我的经验是:提前 2 天把文档发出去,要求评审者在会前留下评论。评审会的重点放在「有争议的问题」上,而不是从头到尾讲一遍。

还有一个技巧:在文档开头写一个 TL;DR,用 5 行以内概括核心决策和需要评审者判断的问题。这能显著提高评审者提前阅读的概率。

评审流程本身也可以规范化。我通常遵循以下步骤:

  1. 作者写完文档 → 自评一遍(用本文的检查清单)
  2. 指定 1-2 个预审人 → 提前阅读,留下评论
  3. 作者回复评论 → 修改文档或标记为「开放问题」
  4. 评审会(30 分钟内)→ 只讨论有分歧的部分
  5. 会后纪要 → 记录决策和待办,更新到文档中
# 坏例子:评审会变成朗读会
- 会上从头到尾讲 PPT,评审者低头看手机
- 会后没有纪要,一周后没人记得讨论了什么
- 下次评审同样的问题又被提出来

# 好例子:高效的评审流程
- 文档提前 2 天发出,预审人留下书面评论
- 会上只讨论评论中标记的分歧点
- 会后 1 小时内发出纪要,包含决策和 action items
- 开放问题记录在文档末尾,跟踪到人

关于工具

文档放在哪里不重要,重要的是团队能不能找到它、能不能看到历史版本、能不能在上面留评论。

Google Docs、飞书文档、Confluence、Notion、甚至 Markdown 文件放在 Git 仓库里,都可以。关键是形成习惯——每个涉及架构变更的需求,都有一篇对应的设计文档,链接记录在项目管理工具里。

工具类型代表产品优势劣势适合场景
在线协作文档飞书文档、Google Docs实时协作、评论方便、搜索友好版本管理粗糙,无法 diff快速迭代期、跨团队协作
Wiki 系统Confluence、Notion结构化组织、权限管理搜索体验差、格式限制多知识库沉淀、跨团队查阅
Markdown + GitGitHub、GitLab版本管理精确、可 code review协作门槛高、不支持实时编辑技术方案、API 文档
ADR 工具Log4brains、ADR Tools轻量、标准化、可追溯功能单一、不适合长文档架构决策记录

常见反模式

写了几年设计文档,也看了别人写的不少,我总结出几种最常见的反模式:

「百科全书」型

作者生怕漏掉任何细节,把整个系统的历史、所有模块的接口定义、数据库的完整 schema 全部塞进一篇文档。结果就是 60 页的文档,评审者根本看不完,评审会上也没人真正讨论核心问题。

设计文档不是系统规格说明书。它的目标是帮助评审者在有限时间内做出判断,而不是成为一本百科全书。不相关的细节会分散注意力,该砍就砍。

「技术炫技」型

明明是一个简单的 CRUD 需求,偏偏要引入微服务、事件驱动、CQRS。文档里充斥着「最终一致性」「领域事件」「Saga 模式」这类术语,但没有解释为什么现有架构不够用、为什么需要这么复杂的方案。

一个困难的问题可能会有一个简单的答案。方案复杂度和问题复杂度应该匹配。如果你要用一个复杂的方案,文档里必须解释清楚「简单方案为什么不行」。

「橡皮图章」型

文档在评审之前就已经决定了。评审会只是走个形式,所有人都知道方案已经和领导对过了。这种情况下,文档的存在只是为了留下「我们做过评审了」的记录。

这其实是最浪费的一种模式。写文档的人不会认真写,评审的人不会认真看,出了问题也没人翻文档——因为大家都知道那份文档和实际方案对不上。

反模式核心问题修复方法
百科全书型信息过载,评审者找不到重点按读者需求裁剪,核心决策前置
技术炫技型方案复杂度超过问题复杂度先论证「简单方案为什么不行」
橡皮图章型评审走形式,决策在评审前已定确保评审者有真正的否决权
知识诅咒型默认读者知道所有背景假设读者是不了解项目的新同事
模糊表述型目标和数据缺乏具体数字每个目标都要有可验证的指标

小结

系统设计文档不是为了证明作者想得很多。它的价值在于:让评审者能判断方案是否可落地,让后来的维护者能理解当时的取舍,让团队在出问题时有据可查。

好的设计文档不需要文采,需要的是结构清晰、风险暴露、决策有据。写完之前问自己一句:如果评审者只给我 5 分钟,他能不能知道我要做什么、为什么这样做、风险在哪里?

如果能,这份文档就及格了。

回到开头的支付系统翻车。如果当时那份四十页的文档里,「失败路径」不是「异常情况做重试处理」这八个字,而是逐条分析超时、重试、幂等、对账的完整链路——那次事故大概率可以避免。

文档不是万能的。但一份好的设计文档,至少能让团队在动手之前把最明显的坑踩掉。剩下的问题,留到实现和线上去发现。

参考资料

评论 0

0 / 1000