编者按: API版本控制是每个团队都知道应该做但大多数都推迟到为时已晚的事情之一。Michael Sun阐述了不同版本控制策略之间的真实权衡,并令人信服地论证:从一开始就实施版本控制的成本远低于后期改造的成本。
六个月前你本应该进行的API版本控制讨论
这是我至少目睹过十几次的场景。一个团队构建了一个API。它最初作为内部工具,然后移动应用开始使用它,接着是合作伙伴集成,再后来是公开的开发者计划。大约在第三个消费者出现时,有人更改了响应字段名称,导致整个移动版本在生产环境中崩溃。
事后分析总是得出相同的结论:我们应该从一开始就为API进行版本控制。然后有人问”如何做”,真正的争论就开始了。
API版本控制不是一个已解决的问题。没有普遍正确的做法。但是决定进行版本控制以及你选择的策略会产生巨大的后续影响。第一天就做对是便宜的。后期改造则是昂贵且痛苦的。
为什么从一开始就进行版本控制很重要
反对早期版本控制的论点通常是这样的:”我们只有一个客户端。我们可以更改任何东西。版本控制增加了不必要的复杂性。”这种推理有两个失败之处。
首先,你的API契约是一种承诺,即使唯一的消费者是你自己的前端。一旦你发布了一个依赖于特定响应形状的客户端,更改该形状就需要协调API变更与客户端部署。这就是版本控制——你只是隐式地、危险地做着这件事。
其次,为已有活跃消费者的现有API添加版本控制比从一开始就构建它要困难得多。你必须审核每个端点,识别每个消费者,构建兼容性层,迁移客户端,并同时维护多个版本——所有这些都无需停机。从一开始就使用版本控制意味着所有这些痛苦永远不会出现。
三种主要策略
URL路径版本控制
这是最明显和最常见的做法。你的API端点直接在URL中包含版本:
GET /api/v1/users/123
GET /api/v2/users/123
优势: 客户端使用哪个版本一目了然。缓存自然有效,因为不同版本有不同的URL。路由很简单——如果需要,你的Web服务器或API网关可以将v1和v2的流量引导到不同的后端服务。文档清晰,因为每个版本都有自己的一组端点。
缺点: URL 路径版本化违反了 REST 原则,即 URL 应该标识资源,而不是表示形式。/v1/users/123 和 /v2/users/123 是同一个用户 — 版本描述的是响应的格式,而不是正在访问的资源。这是一个理论上的问题,在实践中很少重要,但它冒犯了纯粹主义者。
更实际的问题是,URL 版本化鼓励粗粒度的版本跳跃。当版本在 URL 中时,团队倾向于为整个 API 创建新版本,而不是为单个端点进行版本控制。这导致了”大爆炸”式的版本转换,其中 v2 是 v1 的完全替代,构建成本高,客户端迁移成本也高。
何时使用: 在清晰度和简洁性最为重要的公共 API 中。由第三方开发者使用的 API,他们需要轻松理解和控制他们使用的版本。这是我建议大多数团队起步时采用的策略。
基于请求头的版本控制
这种方法中,版本在请求头中指定,通常是自定义头或带有供应商媒体类型的标准 Accept 头:
GET /api/users/123
Accept: application/vnd.myapi.v2+json
# 或使用自定义头:
GET /api/users/123
X-API-Version: 2
优点: URL 保持干净且符合 REST 规范 — 它们标识资源,而版本描述表示形式。这种方法支持细粒度版本控制,因为单个端点可以独立响应版本头。它还允许内容协商,服务器可以根据客户端功能返回最佳可用版本。
缺点: 版本在 URL 中不可见,这使得调试更加困难。当有人将 API URL 粘贴到浏览器或 Slack 消息中时,你无法判断他们使用的是哪个版本。缓存变得更加复杂,因为 CDN 和代理缓存需要配置以根据版本头进行变化。使用 curl 等简单工具进行 API 测试需要记得每次都包含请求头。
最大的实际问题是,请求头版本控制需要更复杂的客户端库。每个 HTTP 请求都需要附加版本头,这意味着你的 SDK 或客户端代码需要一种可靠的方式来设置默认头。在结构良好的代码中这很简单,但在快速脚本和一次性集成中会成为错误的来源。
何时使用: REST 纯粹性很重要、需要细粒度的每个端点版本控制,或者 API 消费者主要使用维护良好的客户端库而不是临时 HTTP 请求的 API。
查询参数版本控制
版本作为查询参数传递:
GET /api/users/123?version=2
优点:版本号在 URL 中可见,但不改变路径结构。可以轻松添加到现有 API 中,无需重构路由。可以在服务器端设置默认行为,因此省略该参数的客户端会获取当前稳定版本。
缺点:查询参数通常与过滤和分页相关,而不是 API 元数据。这会造成概念上的混淆,可能让开发者感到困惑。缓存行为各不相同——一些 CDN 会去除查询参数,一些会将不同的参数组合缓存为独立条目。而且可选的查询参数意味着如果客户端忘记该参数,你永远无法确定他们想要使用哪个版本。
使用时机:作为次要的版本控制机制,或需要向后兼容版本测试的 API。我很少推荐将其作为主要策略,因为可选参数的模糊性产生的问题比它解决的问题更多。
我实际推荐的策略
对于大多数团队,特别是那些构建首个生产级 API 的团队,我推荐使用带有特定约束的 URL 路径版本控制:
- 从第一天就开始使用 /api/v1/。即使你永远不会创建 v2,成本为零,而可选性很有价值。
- 将版本视为主要里程碑,而不是小改动。不破坏性更改——添加新字段、新端点、新可选参数——不需要新版本。只有破坏性更改——删除字段、更改类型、重构响应——才需要版本升级。
- 书面定义”破坏性”的含义。创建一个 API 兼容性策略,明确规定哪些更改被视为破坏性更改。发布该策略。让团队遵守。这份文件可以防止日后的争论。
- 最多同时支持两个版本。当 v3 发布时,v1 会被设定一个弃用时间表。支持超过两个活跃版本在运营上成本高昂,通常也不必要。
- 对整个 API 进行版本控制,而不是单个端点。混合版本控制——/v1/users 和 /v2/orders 同时存在——会造成混淆。如果一个端点需要破坏性更改,该更改将进入下一个完整的 API 版本。
关于 API 弃用,没人告诉你的事
版本控制只是问题的一半。另一半是弃用——实际上移除旧版本。这是大多数团队失败的地方。
我见过最有效的模式是激进弃配配以慷慨的时间表。在新版本发布时宣布弃用日期。给客户端 6-12 的时间。在 90 天、30 天和 7 天时发送提醒邮件。然后关闭它。没有真正的业务原因不要延长截止日期。
诱惑在于无限期地保留旧版本,因为某些客户端”可能仍然需要它”。这条路会导致同时维护三、四、五个 API 版本,每个版本都有自己的错误、安全补丁和运营开销。这是不可持续的。设定时间表并严格执行。
超越 REST 的版本控制
如果您使用的是 GraphQL,版本控制的故事就不同了。GraphQL 被明确设计为无需版本控制即可演进——您可以添加新字段并弃用旧字段。这在实践中效果良好,尽管它需要围绕弃用过程保持纪律。风险在于,弃用的字段可能会永远存在,因为移除它们会破坏未知的客户端。
对于 gRPC,Protocol Buffers 具有内置的字段级兼容性规则。您可以添加字段、弃用字段,并在架构级别保持向后兼容性。gRPC 中的版本号通常出现在包级别,并遵循与 REST 中 URL 路径版本化类似的模式。
关键要点
- 从首次发布就添加 API 版本控制。成本几乎为零,而后期改造则痛苦且风险高。即使您认为永远不会需要 v2,也以 /api/v1/ 开始。
- URL 路径版本控制是大多数团队的最佳默认选择。它简单、明确、对缓存友好,并且所有消费者都易于理解。
- 基于标头的版本控制适合需要细粒度控制的 API,这些 API 主要通过客户端库而非临时 HTTP 请求使用。
- 在需要之前书面定义您的破坏性变更政策。这可以防止争论,并为 API 消费者设定明确的期望。
- 弃用与版本控制同样重要。设定激进的时间表,清晰沟通,并强制执行截止日期,以避免维护越来越多的 API 版本。
