跳到内容

第 6 卷 / 第 19 章 / 第 2 课

19.2 OpenAPI、gRPC、AsyncAPI 与契约演进 ​

契约把路径、消息、字段、错误和兼容承诺变成可评审、可生成、可测试的制品。它不是实现自动导出的漂亮文档,而是生产者和消费者共同依赖的边界。

选择契约形式 ​

场景常见契约关注点
HTTP 请求/响应OpenAPI路径、方法、参数、schema、安全和响应
高效服务 RPC/流Protocol Buffers + gRPC方法、消息字段号、deadline、状态与流语义
消息与事件 APIAsyncAPIchannel、operation、message、协议 binding 与 schema

协议选择取决于客户端生态、浏览器需求、流式交互、性能、治理和长期兼容,不存在对所有边界最优的一种协议。

OpenAPI 描述 HTTP 契约 ​

以下是公开赛事查询的最小契约:security: [] 明确允许匿名读取,ID 沿用上一课的数字字符串。openapi 是规范版本,info.version 是该 API 的版本;使用 3.2.0 前要确认生成器、lint 与网关支持它。示例只定义成功响应体,生产错误体和其他操作还需补齐。

yaml
openapi: 3.2.0
info:
  title: Tournament API
  version: 1.4.0
paths:
  /tournaments/{id}:
    get:
      operationId: getTournament
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, pattern: '^[0-9]+$' }
      responses:
        '200':
          description: Public tournament found
          content:
            application/json:
              schema:
                type: object
                required: [id, name]
                properties:
                  id: { type: string, pattern: '^[0-9]+$' }
                  name: { type: string, minLength: 1 }
              example: { id: '8472', name: 'Autumn Cup' }
        '400':
          description: Invalid identifier
        '404':
          description: Public tournament not found

契约应在评审和 CI 中检查:唯一 operationId、认证声明、所有状态响应、分页与幂等约定、示例是否通过 schema 校验。

生成客户端能减少手写错误,但生成代码不能替代语义设计。SDK 还要处理重试、deadline、认证、可观测性和版本支持政策。

gRPC 契约保护字段号 ​

下面可保存为 scoring.proto,包含语法声明、包名和请求/响应类型。能生成客户端不代表服务端已经实现结算、授权或幂等。

proto
syntax = "proto3";
package tournament.scoring.v1;

service ScoringService {
  rpc SettleMatch(SettleMatchRequest) returns (SettleMatchResponse);
}

message SettleMatchRequest {
  string match_id = 1;
  string winner_id = 2;
  string idempotency_key = 3;
}

message SettleMatchResponse {
  string settlement_id = 1;
}

Protocol Buffers 的字段号是二进制线格式中的身份。下面是删除 winner_id 后用来替换原请求定义的片段,不能和旧定义同时放进同一个文件。应 reserved 原编号和名称,不能把编号分配给新语义:

proto
message SettleMatchRequest {
  reserved 2;
  reserved "winner_id";
  string match_id = 1;
  string idempotency_key = 3;
}

新服务必须已有其他可信的胜者来源,并经过兼容迁移,才能真的删除这个字段;保留字段号只防止误复用,不会补出丢失的业务含义。Proto3 普通标量缺省时读取默认值,空字符串不等于已提供有效 ID;需要区分未提供与默认值时,应选用明确的 presence 机制,例如 optional,并做业务验证。

客户端应显式设置 deadline。DEADLINE_EXCEEDED 对写请求仍可能表示服务端已完成,必须结合幂等键或状态查询处理。

gRPC status code 应区分参数无效、前置条件失败、并发冲突、未认证、无权限和暂时不可用;不要把所有异常映射为 UNKNOWN 或 INTERNAL。

AsyncAPI 描述消息交互 ​

消息契约不只包含 payload,还应说明:

  • channel/topic 与发布、订阅操作;
  • 事件 ID、聚合 ID、版本和时间字段;
  • 分区键与顺序范围;
  • schema 兼容策略;
  • 安全、协议 binding 和重试/死信约定;
  • 生产者和消费者所有者。

AsyncAPI 3.x 的 operation 用 action: send 或 receive 表示所描述应用的方向,不能直接照搬 2.x 文档里的 publish/subscribe 结构。

AsyncAPI 能描述结构,端到端投递、幂等和补偿仍需运行设计与测试。

“新增字段”也可能破坏消费者 ​

结构上向后兼容的变更可能语义不兼容:

  • 新增枚举值会击穿客户端的穷举分支;
  • 原可选字段变成业务必填;
  • 数字单位从秒变毫秒;
  • 列表排序规则改变;
  • 以前不会为空的数组开始返回空;
  • 错误码含义或重试建议改变。

兼容性要同时检查 wire、source、behavior 和 data 四个层面。

版本策略 ​

优先做兼容演进:新增可选能力、维持旧语义、给出弃用窗口。只有无法兼容时才引入新主版本或新资源。

版本可以放在路径、header、media type 或 schema/topic 名中。选择哪种不如以下问题重要:

  • 两个版本要并存多久;
  • 客户端如何发现弃用和迁移指南;
  • 生产者如何知道还有谁使用旧版;
  • 何时停止支持,谁批准;
  • 回滚时新旧数据是否兼容。

契约测试的三层门禁 ​

  1. 静态兼容检查:OpenAPI/Proto/AsyncAPI diff 和 lint;
  2. 提供者验证:实现是否满足契约和示例;
  3. 消费者场景:关键消费者依赖的字段与交互是否仍成立。

消费者驱动契约适合捕捉真实依赖,但不能让每个消费者冻结提供者的内部设计。契约应围绕公开行为,避免断言无关字段和调用次数。

治理不是中央审批队列 ​

有效治理提供团队能直接复用的自动化流程:模板、lint、兼容 diff、生成、测试、目录和弃用仪表盘。平台团队制定共通安全与可操作性规则,领域团队仍拥有业务语义。

每个契约都需要:所有者、稳定级别、认证方式、SLO、变更记录、弃用政策和支持渠道。

参考资料 ​

Built with VitePress | Software Systems Atlas