Appearance
API设计与契约治理
定位:架构方法论深化系列之五。API 是团队之间最持久的资产,代码可以重写、接口不能轻动。这篇整理 5 个主题:
- 三种通信形态的契约差异。
- API 演进规则。
- 契约测试与 mock 漂移治理。
- SLA 分级与限流配额。
- 版本化策略对比。
RESTful / gRPC / 消息:契约差异
同样是"接口",三种通信形态的契约载体、演进规则、治理方法完全不同:
| RESTful | gRPC(Protobuf) | 消息(事件) | |
|---|---|---|---|
| 契约载体 | OpenAPI/Swagger(可后补) | .proto 文件(先行,强约束) | 事件 Schema + 版本字段 |
| 演进规则 | 加字段天然兼容(JSON) | 靠 field id:不复用已删编号,只用 optional | 双向都要认新旧格式 |
| 主要陷阱 | 字段改语义、数字精度(long 溢出 2^53) | required 一旦加上永远拆不掉 | 消息积压期间消费者升级顺序 |
| 耦合方向 | 消费方依赖提供方(拉) | 消费方依赖提供方(拉) | 双方只依赖事件契约(推,松耦合) |
| 治理方法 | 网关层 schema 校验、文档即契约 | IDL 仓库统一托管、CI 编译校验 | schema registry、topic 权限治理 |
共同点只有一条:契约是承诺,不是文档。序列化层面的细节坑包括 tag 复用、required 陷阱、字符集。这些坑在《系统设计如何全方位考虑兼容性问题》协议层有展开。
API 演进规则
核心原则两条:
- 新增优于修改:要扩展就加新字段/新接口/新枚举值,永远不动老的。老的哪怕难看也先留着——接口的难看是提供方的事,接口的突变是所有消费方的事。
- 废弃走两阶段(实务上三步):
第一步:标记废弃
字段/接口注 @Deprecated,文档标红,返回头带 Sunset 提示
第二步:监控归零
按调用方维度监控老接口流量,逐一确认迁移完成
(归零之前删除 = 白屏等您;归零之后保留 = 白付兼容成本)
第三步:真删 + 拒绝得体
下一版本删除;期间老路径返回明确的 410/错误码与迁移指引,
而不是 500允许与禁止的变更速查:
允许(向下兼容) 禁止(破坏性,需升大版本)
------------------------ ---------------------------
新增可选字段/参数(带默认值) 删除/重命名字段
新增错误码(确认过消费方兜底) 收窄类型(long->int)
接口加新的可选能力 改字段语义(枚举含义变化)
性能提升(语义不变) 幂等/一致性等隐式语义变化
required 化(原来可不传)演进规则只管"变得安全",接口本身的几个基本盘要在设计时就定好。否则每个消费方都会长出自己的方言:
命名与模型
[ ] 资源命名用业务名词而非实现细节(/orders 而不是 /orderTable/query)
[ ] 字段语义全局一致:同一业务概念在不同接口里同名同义
[ ] 时间统一毫秒时间戳+时区约定、金额统一最小单位(分)+字符串传输
分页与列表
[ ] 大列表强制分页;游标分页优先于 offset(深翻页与数据漂移问题)
[ ] 返回结构带游标与是否结尾(hasMore),别让消费方猜
幂等与错误
[ ] 写接口的幂等键(token/requestId)从第一天就有,后补极痛
[ ] 错误模型统一:code(机器读) + message(人读) + traceId(排查用)
[ ] 错误码区分"可重试/不可重试",避免消费方盲目重试放大问题
安全默认
[ ] 默认最小返回:不返回消费方用不到的敏感字段
[ ] 越权校验在每个接口上显式存在,而不是"网关应该挡了"基本盘是契约的"语法",演进规则是"词汇的变化规则",两者合起来才是完整的契约治理。
契约测试与 mock 漂移
契约测试(Pact 思想,Consumer-Driven Contract)解决的是:提供方改了接口,怎么知道炸的是哪些消费方。
消费方A ──写"期望"──> 契约A ─┐
消费方B ──写"期望"──> 契约B ─┼──> 契约仓库(broker)
消费方C ──写"期望"──> 契约C ─┘ │
│ 变更验证
提供方 CI <──────拉取全部消费方契约────┘
跑不通 = 有消费方会挂 = 拦截合并
对比:只有提供方单方面的集成测试
-> 提供方测自己的理解,消费方的真实用法没被覆盖mock 漂移是它的反面问题:消费方联调用的 mock 是手写的。时间一长和真实提供方对不上——mock 通过、集成爆炸。治理规则:
- mock 从契约生成,不手写:契约仓库是唯一事实源,mock 是它的派生物。
- 契约变更时消费方 CI 自动重放校验,漂移在合并前暴露,而不是上线后的凌晨。
- 契约测试不替代端到端测试,它是中间层:比单测真实、比集成测试便宜且稳定。
对外 API 的 SLA 分级与限流配额
对外(对其他业务方)开放的 API,必须分级承诺、按级收费(配额):
| 级别 | 定位 | 可用性承诺 | 典型时延要求 | 变更窗口 |
|---|---|---|---|---|
| L1 资金/核心交易 | 出错赔钱 | 99.99% | P99 < 100ms | 只能兼容演进,双跑切流 |
| L2 主链路读写 | 核心依赖 | 99.95% | P99 < 300ms | 常规灰度 |
| L3 查询/展示 | 可降级 | 99.9% | P99 < 1s | 自由 |
| L4 报表/批量 | 可延迟 | 按窗口 | 分钟级 | 自由 |
配套三件事:
- 限流配额是默认动作:每消费方有默认配额,超额走申请审批(写明场景与峰值),而不是出事再限。限流的算法选型见库内《限流算法与工程实践》。
- 分级联动降级:L3/L4 先被牺牲保 L1——容量预案按级别从低到高砍。
- 承诺写进契约:SLA 是契约的一部分,违约要复盘,承诺过的可用性没兑现和接口挂掉是同级事故。
版本化策略对比
兼容期终究要收敛,收敛靠版本机制:
| 策略 | 做法 | 优点 | 缺点 | 适合 |
|---|---|---|---|---|
| URL 路径版本 /v2/orders | 版本进路径 | 直观、路由/监控/缓存天然分流 | 多版本共存期长、路径膨胀 | 对外 REST、开放平台 |
| Header 版本 | 版本放协议头(或 content-type) | URL 干净 | 不直观、排查要抓包才能看到版本 | RPC、内部服务 |
| 双跑(新旧同实现并行) | 新旧版本同时计算、比对后切 | 切换最稳、可对账 | 成本×2、周期要克制 | 资金类 L1、高危变更 |
无论哪种,铁律是:老版本要有明确下线时间表(如两个迭代),并持续监控老版本流量归零。双跑期必须有自动对账与差异告警,否则双跑只是双倍的风险。
契约治理追问链
设计或评审一个 API 时连问:
1. 这个接口的契约载体是什么(OpenAPI/proto/schema)?在哪托管?
2. 三年后这个接口还在,最可能的调用方式长什么样?现在留了什么扩展位?
3. 破坏性变更有几个消费方会受影响?我怎么"知道"而不是"猜"?(契约测试)
4. 消费方的 mock 是手写还是契约生成?漂移谁来发现?
5. 这个接口是 L 几?SLA 承诺和限流配额匹配吗?
6. 老版本下线时间表在哪?流量归零谁在监控?与库内其他篇的关系
- 契约变更的四层兼容检查(接口/数据/协议/行为)见《系统设计如何全方位考虑兼容性问题》。
- 新旧版本切换的灰度阶梯与回滚见《发布工程与灰度体系》《系统切流通用方案》。
- 接口的归属应与团队边界对齐(谁拥有接口谁拥有服务),见《康威定律与团队拓扑》。重大破坏性升级的决策留痕见《技术决策与ADR》。
- 契约测试可以看作一组自动化的适应度函数,见《演进式架构与适应度函数》。
一句话总结
契约是承诺:新增优于修改、废弃走"标记→归零→删除"、破坏性变更升版本。mock 由契约生成防漂移,消费方契约进 CI 做自动拦截。对外 API 分级承诺 SLA、默认限流。版本策略按风险选(URL 直观/Header 内敛/双跑最稳),老版本带时间表地退场。
2026-10 新增(架构方法论深化系列)。