适用读者:要在评审会上回答"你的架构为什么这么设计、为什么能扛住"的人;准备系统设计面试的资深/架构岗。
前置:S17(分布式基础:注册发现、网关、消息、事务、ID)、S18-A01(边界)、S18-A02(结构)。
覆盖旧文:A-2412(290 点,含 2 份重复导出 →duplicate_ref)、A-652(187 点)、A-173(228 点)——合计 705 点。
扩展点:E-S18-021(容量估算)、E-S18-022(故障域与熔断/超时参数)、E-S18-023(ADR 模板)、E-S18-024(五步评审清单)、E-S18-025(幂等与外部回调)、E-S18-026(长任务异步化)。
上一篇:S18-A03 · MVC 到 DDD 的重构与迁移
下一篇:S18-X01 · 23 种设计模式速查表。
1. 一句话结论
"高可用"不是形容词,是四个可算的东西:容量(能接多少)、故障域(坏一块死多少)、一致性(能容忍多旧)、幂等(重试会不会重复)——四个都给出数字和归属,架构才算设计过。
A-2412 的开场就把这类问题的性质说清了:它考察"整体架构设计能力、技术广度、严谨程度","不太会强求……给出一个完美的答复",只要"一个听上去行止可行,逻辑自洽的回答,基本上就可以得到高分了"。换句话说:评审看的不是"你有没有堆组件",而是"你的推理链是否闭合"。
1.1 第一步永远是"把问题具体化"
原文给的做法是先圈范围:面向用户的高可用应用(电商商品体系、社区)/通用服务(网关、缓存、消息推送)/对 CAP 敏感的注册中心——不同定位,考虑的点完全不同。然后在评审时先把这三类特点摆出来,表明"我关注的不只是通用方案,还关注实际场景"。
这一步的价值在于:没有范围的"高可用"无法验收。所以本文后面每一节都要求产出"数字 + 归属"。
2. 容量估算:从日单量到实例数(E-S18-021)
A-2412 的实例题是"日均千万量级的高可用订单履约系统",它给的第一步估算非常朴素,也非常正确:
1kw / 1d = 10,000,000 / 86,400 ≈ 115.74 单/s → 按 116 单/s 设计
峰值按 200 单/s(原文口径:瞬时最大消费能力大于平均值)
复核(本文用 Python 重算,2026-11-06):10,000,000 / 86,400 = 115.74,取 116 的误差 0.22%。这个精度对容量设计完全够用——容量估算要的是量级正确,不是小数点。
2.1 一张可复用的容量表
| 量 | 公式 | 本例 | 说明 |
|---|---|---|---|
| 平均 QPS | 日量 / 86400 | 116 单/s | 只用于基线 |
| 峰值 QPS | 平均 × 峰均比(3–10) | 200–1160 | 电商大促取 10,内部系统取 3 |
| 单实例能力 | 压测得出(不是猜) | 假设 50 单/s/实例 | 必须实测,否则后面全是空中楼阁 |
| 实例数 | ⌈峰值 / (单实例 × 0.7)⌉ | ⌈200/35⌉ = 6 | 0.7 是安全水位 |
| 存储/天 | 条数 × 单条字节 | 5000 万 × 1KB ≈ 47.7 GB/天 | 日志/明细类 |
| 保留天数 | 可用容量 / 每天增量 | 1.8 TB / 47.7 GB ≈ 37.7 天 | 决定分区与归档策略 |
| 连接数 | 实例数 × 每实例池大小 | 6 × 20 = 120 | 要 ≤ DB max_connections × 0.8 |
这张表最容易被跳过的是"单实例能力"。没有压测数据时,正确做法是写"待压测,暂按 X 估算"并标注为风险项,而不是假装知道。
2.2 从容量推出架构(原文的三层拆法)
A-2412 的实例设计有三层,每层都有容量理由:
| 层 | 设计 | 容量/可用性理由 |
|---|---|---|
| 收单服务 | 独立集群 + MQ 异步拉单 | 削峰:让下游按 200 tps 消费,不必关心零点瞬时洪峰;故障后可回放消息 |
| 履约服务 | 拆成多个独立服务 + 统一状态机编排 | 各服务按自身能力弹性伸缩;状态机负责限流、背压、重试、流量分配 |
| 存储 | 按订单 ID 分库分表(+ 冷热分离) | 千万/天必然单表撑不住;冷热分离降成本 |
状态机编排这一段是全文最有价值的架构判断,原文的推理链值得完整保留:先画了"串行流式消费"的方案,随即自己指出"这个方案和高可用存在冲突——一个服务中断会影响整条链路",然后改成状态机调度,收益是:
- 减少服务间直接依赖,可做流量控制、速度调节、异常重试;
- 每个服务只关心自己的逻辑,通知状态机可同步可异步;
- 可按各服务承载能力动态分配流量;
- 部分场景可跳过某服务,也可并行调度。
这就是"从方案 A 到方案 B"的取舍展示——评审时最有说服力的不是"我选了 B",而是"我先画了 A,自己发现了 A 的缺陷,改成了 B"。
3. 故障域与爆炸半径(E-S18-022)
A-2412 给的高可用清单有 11 条,本质是四类动作:冗余、隔离、保护、观测。本文把它重排成可执行的参数表:
| 动作 | 具体手段 | 可配置参数(经验值,推断,需压测校准) | 归属 |
|---|---|---|---|
| 冗余 | 多实例、多可用区、主备/主从 | 副本数 ≥2;跨 AZ 至少 1 份 | 运维 |
| 隔离 | 按来源路由(天猫/京东各自集群)、按服务拆、按机房分 | 单集群流量上限 = 总容量 × 0.6 | 架构 |
| 超时 | 每个外部调用都要有超时 | timeout = 下游 P99 × 2,且 ≤ 上游剩余预算 |
调用方 |
| 重试 | 只对幂等操作重试,带退避 | 最多 1–2 次,指数退避 + 抖动 | 调用方 |
| 熔断 | 错误率/慢调用比例触发 | 10s 窗口错误率 > 50% 打开;半开放 5 个探测 | 调用方 |
| 限流 | 入口限流 + 出口限流 | 入口 = 峰值 × 1.2;单实例按水位 70% | 网关/服务 |
| 背压 | 队列满则拒绝而非无限堆积 | 队列长度 = 单实例 1s 处理量 × 3 | 服务 |
| 降级 | 非核心功能可关 | 至少 3 个开关:推荐、积分、评价 | 产品 + 架构 |
| 观测 | 监控 + 关键日志 + 预警 | 四个黄金信号:延迟/流量/错误/饱和度 | 运维 |
| 少依赖 | 尽量少引入第三方组件 | 每引入一个中间件,多一个故障域 | 架构 |
| 少交互 | 能异步不同步,能本地不远程 | 同步调用链 ≤ 3 跳 | 架构 |
"尽量少引入第三方组件"(原文 2.6)是这份清单里最反直觉、也最该被引用的一条:多数人谈高可用是"加组件",原文的结论是"外部依赖越少,稳定性越高"。这与 S18-A01 的"拆分过头=分布式单体"是同一个判断在两个层面的表达。
爆炸半径的算法(本文补充的量化方式):
爆炸半径 = 受影响请求占比 × 受影响时长
= (单故障域承载流量 / 总流量) × 故障恢复时间(MTTR)
例:按来源路由后,单集群承载 30% 流量,MTTR 10 分钟 → 影响 = 3 分钟等效全站不可用。把这个数字写进 ADR,比写"我们做了隔离"有力得多。
4. 一致性与幂等(E-S18-025)
A-2412 要求在回答高可用时"务必谈一下一致性的问题",并给了两个具体故障源:
- 时钟不一致:各机器时钟不同步 → 基于当前时间的业务逻辑出错 → 统一时钟源(NTP);
- 资源竞争:多节点争同一资源 → ABA 等问题 → 分布式锁 / DB 排他锁;
- 网络分区:尽量同机房内网通信,但"这其实牺牲了一定的可用性"(一个机房全崩则全不可用)——这就是 CAP 的取舍现场。
A-652 则从业务侧给了同一个问题的实例:支付回调幂等。两边合起来,幂等有四层手段:
| 层 | 手段 | 幂等键 | 失效场景 |
|---|---|---|---|
| 接口层 | 幂等 Token(客户端生成,服务端去重) | requestId |
客户端重试时换了 token |
| 业务层 | 状态机 + 唯一约束(where status=CREATED) |
业务单号 | 状态机被绕过 |
| 存储层 | 唯一索引 / 乐观锁 version | 主键/业务键 | 分库后唯一性只在单库 |
| 消息层 | 消费去重表 + 至少一次投递 | msgId |
去重表未落库就消费 |
判据:幂等键必须来自业务,不能来自请求。requestId 只能防"同一次请求重发",防不住"用户点了两次"。真正可靠的键是业务单号 + 状态前置条件。
支付回调的正确写法(伪代码,标注推断——未在真实支付网关验证):
// 1) 先按业务单号加行锁/乐观锁,2) 判断状态前置,3) 再改状态
int n = jdbc.update("update orders set status='PAID' where order_no=? and status='CREATED'", no);
if (n == 0) { /* 已处理过或状态不符:直接返回成功,不报错 */ return "SUCCESS"; }
第二行 if (n == 0) return "SUCCESS" 是重点:重复回调要返回成功,否则支付方会一直重试。
5. 长任务异步化与工作流(E-S18-026)
A-652 的项目(AI 求职信息聚合)给了三个真实技术问题,都是"需求→架构"的典型:
| 问题 | 方案 | 判据 |
|---|---|---|
| 耗时较长的 AI 任务(采集+抽取几十秒) | 提交任务 → 返回 taskId → 异步执行 → 轮询/推送结果 | 同步接口 P99 > 3s 就该异步 |
| 复杂的 AI 工作流(多步、可分支) | 状态机/工作流引擎编排,每步可重试、可观测 | 步骤 ≥4 或需要人工介入 |
| 动态业务配置(运营改规则) | 配置中心 + 版本化 + 灰度 | 改配置不该发版 |
异步化的四个必答项(少一个就会在生产出事):
- 任务状态机:
PENDING → RUNNING → SUCCESS/FAILED,且FAILED可重试; - 超时与看门狗:
RUNNING超过 N 分钟 → 标TIMEOUT并告警(否则任务永远"运行中"); - 幂等:同一 taskId 重复执行结果一致;
- 可观测:每步的耗时、输入摘要、失败原因都能查(对应 J05 的"失败留现场")。
A-2412 的状态机编排与这里的"工作流编排"是同一个模式在两个尺度的应用:把"谁调谁"换成"谁向编排者报告",代价是多一个组件,收益是流量控制、重试、可观测都集中到一处。
6. ADR 与五步评审清单(E-S18-023 / E-S18-024)
6.1 ADR 模板(Architecture Decision Record)
格式参考 adr.github.io (Michael Nygard 的原始提案),最小可用版本:
# ADR-0007 订单履约采用状态机编排而非串行调用
## 状态
已接受(2026-11-06)| 取代:无 | 被取代:—
## 背景(Context)
日均订单 1kw,峰值 200 单/s。履约链路由 4 个服务串行推进,
任一服务故障会导致整条链路停摆(MTTR 实测 18 分钟,影响 100% 订单)。
## 决策(Decision)
引入状态机作为编排者:各服务向状态机上报状态,状态机负责流量分配、
重试与背压。服务间不再直接串行调用。
## 备选方案(Alternatives)
A. 保持串行 + 增加重试 —— 拒绝:故障仍会阻塞链路
B. 纯 MQ 事件驱动 —— 拒绝:缺全局状态视图,补偿逻辑分散
C. Saga 编排 —— 保留:与状态机方案接近,待复杂跨域事务出现时再评估
## 后果(Consequences)
+ 故障隔离;可限流/背压/重试;可观测集中
− 多一个组件(状态机)成为新的单点 → 必须集群化 + MQ 异步交互
− 状态机成为"上帝对象"的风险 → 只做路由与状态流转,不含业务规则
## 度量(如何知道它对了)
履约链路 MTTR ≤ 5 分钟;单服务故障影响 ≤ 25% 订单;
状态机自身可用性 ≥ 99.95%。
关键在最后两节:"备选方案"证明你比较过;"度量"证明它可以被验收。没有这两节的 ADR 只是"技术决定通知"。
A-173 的三份 Markdown 可以看作 ADR 的"运行时版本":AGENTS.md=宪法(约束与红线,等价于 ADR 的 Decision)、STATE.md=记忆(当前进度与待决项,等价于 ADR 的 Status + 后续)、SKILL.md=能力单元(可复用的操作契约,等价于 Consequences 里的执行细则)。三者合起来,就是"决策 + 状态 + 执行"的闭环。这个类比的价值:ADR 不是写完就归档的文档,它应该像 STATE.md 一样被持续更新。
6.2 五步评审清单(E-S18-024)
A-652 把"从需求到架构"拆成五步,本文把它变成可勾选的评审表:
| 步 | 动作 | 产出物 | 评审问法(答不出就退回) |
|---|---|---|---|
| 1 | 业务需求 → 业务流程 | 2–3 条核心流程(用户流、数据流) | "这条流程里哪些步骤必须同步?" |
| 2 | 按职责拆模块 | 模块清单 + 职责一句话 | "这个模块的边界用一句话说得清吗?"(对应 SRP) |
| 3 | 设计分层 | 分层图 + 依赖方向 | "依赖箭头有没有朝外的?" |
| 4 | 解决核心技术问题 | 每问题一个方案 + 取舍 | "如果这个方案失效,退路是什么?" |
| 5 | 说明架构取舍 | 3 组"选 A 不选 B + 理由 + 代价" | "代价你打算怎么付?" |
为什么"取舍"必须放最后且必须有 3 组:因为前四步都是"加法"(加模块、加层、加组件),只有第五步是"减法"。评审时最容易被追问的也正是这一步——A-652 原文的一句话是:"核心原则:先明确业务,再划分模块,最后选择技术方案",顺序反了就会变成"用技术找需求"。
7. 常见误区
| 误区 | 真相 |
|---|---|
| "高可用 = 堆机器/堆组件" | 外部依赖越少越稳;每加一个组件多一个故障域(原文 2.6) |
| "容量靠估算就行" | 单实例能力必须压测;没有压测就写"待压测"并列为风险 |
| "重试能解决超时" | 非幂等重试会造重复数据;重试必须有预算与退避 |
| "分布式锁能解决一切竞态" | 锁的失效、续期、脑裂都要处理;能用唯一约束/状态前置就别上锁 |
| "同机房部署更可靠" | 它提升了延迟与分区容忍,但牺牲了机房级容灾 |
| "架构做完再写文档" | ADR 要在决策当时写;事后补的 ADR 只剩结论没有推理 |
8. 自测
- 日均 300 万单,峰均比 8,单实例压测 40 单/s,安全水位 70%,需要几个实例?
- 你的同步调用链有几跳?超过 3 跳时,最先该改哪一段?
- 支付回调重复到达,你的代码返回什么?为什么不是报错?
- 状态机编排引入的新单点怎么消除?
- 写一条你自己项目的 ADR,必须含"备选方案"和"度量"两节。
- 爆炸半径怎么算?给出你系统的当前数值。
下一篇
S18-X01 · 23 种设计模式速查表
正文 10 篇结束。附录两篇负责"查":X01 把 23 种模式压成"意图 / 场景 / 反例 / 易混淆"四列,X02 给 DDD 术语与建模模板。
本篇属于系列《S18 · 架构方法与领域建模》。旧文来源:A-2412(含 2 份重复导出)、A-652、A-173;扩展点 E-S18-021/022/023/024/025/026。容量数字为 Python 复核(116 单/s,误差 0.22%);熔断/超时参数为经验值(推断,需压测校准);ADR 格式参考 adr.github.io;CAP 与分区取舍为原文表述。