19.2 OpenAPI、gRPC、AsyncAPI 与契约演进
契约把路径、消息、字段、错误和兼容承诺变成可评审、可生成、可测试的制品。它不是实现自动导出的漂亮文档,而是生产者和消费者共同依赖的边界。
选择契约形式
| 场景 | 常见契约 | 关注点 |
|---|---|---|
| HTTP 请求/响应 | OpenAPI | 路径、方法、参数、schema、安全和响应 |
| 高效服务 RPC/流 | Protocol Buffers + gRPC | 方法、消息字段号、deadline、状态与流语义 |
| 消息与事件 API | AsyncAPI | channel、operation、message、协议 binding 与 schema |
协议选择取决于客户端生态、浏览器需求、流式交互、性能、治理和长期兼容,不存在对所有边界最优的一种协议。
OpenAPI 描述 HTTP 契约
以下是公开赛事查询的最小契约:security: [] 明确允许匿名读取,ID 沿用上一课的数字字符串。openapi 是规范版本,info.version 是该 API 的版本;使用 3.2.0 前要确认生成器、lint 与网关支持它。示例只定义成功响应体,生产错误体和其他操作还需补齐。
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,包含语法声明、包名和请求/响应类型。能生成客户端不代表服务端已经实现结算、授权或幂等。
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 原编号和名称,不能把编号分配给新语义:
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 名中。选择哪种不如以下问题重要:
- 两个版本要并存多久;
- 客户端如何发现弃用和迁移指南;
- 生产者如何知道还有谁使用旧版;
- 何时停止支持,谁批准;
- 回滚时新旧数据是否兼容。
契约测试的三层门禁
- 静态兼容检查:OpenAPI/Proto/AsyncAPI diff 和 lint;
- 提供者验证:实现是否满足契约和示例;
- 消费者场景:关键消费者依赖的字段与交互是否仍成立。
消费者驱动契约适合捕捉真实依赖,但不能让每个消费者冻结提供者的内部设计。契约应围绕公开行为,避免断言无关字段和调用次数。
治理不是中央审批队列
有效治理提供团队能直接复用的自动化流程:模板、lint、兼容 diff、生成、测试、目录和弃用仪表盘。平台团队制定共通安全与可操作性规则,领域团队仍拥有业务语义。
每个契约都需要:所有者、稳定级别、认证方式、SLO、变更记录、弃用政策和支持渠道。
参考资料
- OpenAPI Initiative, OpenAPI Specification 3.2.0
- Protocol Buffers, Proto3 language guide
- gRPC, Core concepts
- gRPC, Status codes
- AsyncAPI Initiative, AsyncAPI Specification