Skip to content

API设计与契约治理 ​

定位:架构方法论深化系列之五。API 是团队之间最持久的资产,代码可以重写、接口不能轻动。这篇整理 5 个主题:

  • 三种通信形态的契约差异。
  • API 演进规则。
  • 契约测试与 mock 漂移治理。
  • SLA 分级与限流配额。
  • 版本化策略对比。

RESTful / gRPC / 消息:契约差异 ​

同样是"接口",三种通信形态的契约载体、演进规则、治理方法完全不同:

RESTfulgRPC(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 新增(架构方法论深化系列)。