如何停用一个 API,又不至于失去它的开发者
阅读约 1 分钟
停用一个 API,就是宣布某个东西今天仍然可以正常工作,并将在一个确定的日期停止工作,然后把这个承诺的两个方面都真正兑现。大多数停用都在第二个方面失败:日期悄悄地推迟了,或者日期到了,但那些从未看到通知的调用方,是通过一个错误才发现的。一次停用真正结束,是当每一个受影响的调用方要么已经完成迁移,要么已经被单独告知他们还没有完成的时候。
什么是 API 停用
停用,是宣布一个端点、字段或版本即将消失、到真正把它移除之间的这段时间。在这段时间里,旧的行为依然能正常工作,文档说明它即将消失,每一个响应都带有机器可读的警告。移除是一个独立的、更晚发生的事件,通常被称为”服务下线”。这两者经常被混为一谈,而正是这种混淆造成了伤害:“已停用”开始意味着”可能已经消失了”,调用方对这两个词都不再信任。
| 术语 | 含义 | 调用方可以依赖的东西 |
|---|---|---|
| 已停用 | 已宣布即将消失,但仍然工作 | 在服务下线日期之前,行为完全不变 |
| 服务下线 | 停止工作的那个日期 | 这个日期之后什么都没有了 |
| 已退役 / 已移除 | 已经消失;请求会失败 | 一个错误,理想情况下会指明替代方案 |
| 遗留 | 未定义。避免使用这个词 | 什么都没有,而这正是问题所在 |
停用期应该持续多久
要足够长,长到让调用方能够发现并完成迁移工作,而且要从通知真正送达他们的那一刻算起,而不是从你写下这条通知的那一刻算起。九十天是公开 web API 的一个常见下限。对于嵌入在终端用户自行安装的软件中的东西,十二个月才是正常的,因为修复也必须通过用户自己的发布流程才能到达。Google 的版本控制指南 AIP-185 要求提供合理的过渡期,甚至在移除 beta 功能之前也建议留出 180 天,而 Kubernetes 把它的停用政策以发布次数而不是月份来记录,这在调用方按版本升级时才是正确的单位。
选定一个期限,把它写成一份政策,然后不要再逐个变更去重新决定它。一份已经发布的政策,能把每一次停用都从一场谈判,变成对一条规则的应用。
把停用政策写下来,覆盖的是窗口期的开始;下线一个 API 版本 讲的是在结尾处需要的那条独立通知,也就是停用期真正结束、版本真正停止工作的那一刻。
停用的时间线
四个日期,在第一天就一起宣布出来。它们到来时各自会成为一条独立的体验日志条目,因此对于只读体验日志的人来说,这个故事会被讲述四遍。
- 宣布。 这条条目说明什么被停用了、为什么、什么将取代它,以及服务下线的日期。旧功能的文档会加上一条链接到迁移方法的横幅。响应会加上下文描述的那些标头。
- 在中间点提醒一次。 第二条条目,以及给每一个仍在使用旧行为的调用方发送的直接消息。这一步需要使用数据:如果你无法列出还有谁在调用那个已停用的端点,你就做不到这一步,而这一点值得在下一次停用之前先修好。
- 在日期临近之前短暂中断一次。 在一个短暂的窗口——一个小时或一天——里对旧行为返回错误,然后恢复它。所有错过了每一条通知的调用方,现在能趁着还有时间发现这件事。GitHub 在停用 API 的密码认证之前就使用了这种预定的短暂中断,这是这份清单里最有效的一步。
- 服务下线。 移除它。取代它的那个错误会指明替代方案,并链接到迁移指南。让这个错误长期保留下去;一个 404 什么都不会告诉调用方。
一条停用通知应该说些什么
一条停用通知,应该说明什么即将消失、什么时候停止、应该改用什么,以及谁会受到影响。以下是这个形式的具体示例:
GET /v1/reports/daily已停用,将于 2027 年 3 月 1 日停止工作。 它将由GET /v2/reports?granularity=day取代,后者以稳定的 schema 和分页返回相同的数据。影响过去 30 天内调用过 v1 端点的 214 个集成;如果你的集成是其中之一,你也会通过邮件收到这条通知。迁移指南:[链接]。在 2027 年 3 月 1 日之前,一切都不会改变。从那个日期起,v1 端点将返回带有指向本条目链接的410 Gone。
每一句话都承载着读者需要的信息。受影响集成的数量,告诉每一位读者是否需要继续往下读。“在……之前,一切都不会改变”这句话,是让不受影响的人可以直接关掉标签页的那一句。体验日志范例页面收集了那些始终坚持写出这种形式的团队的条目,在写自己的第一条之前,值得先读上三条。
一个已停用的端点应该发送哪些响应头
从宣布之日起,就在这个已停用端点的每一个响应中发送 Deprecation、Sunset,以及一个指向后继版本的 Link。Deprecation 标头携带停用生效的日期;Sunset 标头携带该端点停止响应的日期;Link: <url>; rel="successor-version" 指明应该改用什么。
HTTP/1.1 200 OK
Deprecation: @1756425600
Sunset: Mon, 01 Mar 2027 00:00:00 GMT
Link: <https://api.example.com/v2/reports>; rel="successor-version"
Link: <https://example.com/changelog/daily-reports>; rel="deprecation"
大多数调用方永远不会自己去读这些标头。它们真正的价值在于,调用方的 HTTP 客户端、网关或监控系统能够读到它们,这会把你的停用变成对方那一侧的一个警报,而不是你这一侧的一个页面。你发布的 SDK,在看到这些标头时应该记录一条警告日志。
谁被告知了,你又是怎么知道的
这一步决定了服务下线到底会悄无声息,还是会变成一场支持事故,而这也是仅靠体验日志最难做到的一步。一条体验日志条目会告诉所有读体验日志的人。而一次停用必须触达那些代码即将出问题的具体的人,找到他们的常规方式,正是中间点提醒所需要的那份使用数据:最近调用过那个已停用行为的 API key、应用或账户。
我们运行的这套循环是这样的:这条条目从添加停用逻辑的那个 pull request 起草而来,由人来审核措辞和日期,一旦它被发布,这条条目本身就是通知。任何人,只要其关于那个问题的小组件反馈、或对替代方案的请求,变成了一个由该 pull request 关闭的 GitHub issue,都会在那个 issue 上收到一条评论,说明它已经上线,并附上这条条目的链接。体验日志信息流和小组件会把同一条条目提供给其他所有人,连同API 体验日志里的每一条其他记录一起。我们不会做的一件事,是在人真正发布它之前,就让这次停用变成”已上线”;一条日期错误的通知,比完全没有通知更糟糕。
无论你用什么工具,在服务下线那天你必须能够回答的问题是:上周谁还在使用这个功能,我们又直接告诉了其中的哪些人?如果答案是”我们发过一篇公告”,那么这次服务下线还没准备好。
停用和版本控制有什么区别
版本控制是在新的行为存在的同时,让旧的行为继续可用的方式;停用是让旧的行为退役的方式。一个没有为上一个版本制定停用政策的新 API 版本,只是一份要把两个版本都永远运行下去的承诺。没有版本控制的停用,则是一个带延迟的破坏性变更。两者都需要,而版本控制是其中更容易的那一半。GraphQL 是个值得点名的例外:通常那里根本没有版本号可以升,GraphQL 模式停用讲的正是一份共享的模式如何改用一个指令来让某个字段退役。
FAQ
一个已停用的端点应该继续和以前完全一样地工作吗? 应该,直到服务下线日期为止。唯一被允许的变化,是新增的标头,以及在临近末期时那个已经提前公告过的、有计划的短暂中断。
一个已退役的端点应该返回什么状态码?
410 Gone,正文和一个 Link 标头指向替代方案和体验日志条目。404 说的是这个 URL 从未存在过,这既是假的,也没有任何帮助。
停用期可以缩短吗? 只有出于安全原因才可以。如果旧行为存在可被利用的漏洞,就明确说明这一点,缩短这段期限,并直接告知每一个受影响的调用方,而不是仅仅依赖体验日志。
我需要停用一个字段,还是只需要停用整个端点? 字段、参数、枚举值、默认值和标头都需要同样的处理,因为它们每一个都可能破坏一个正确的调用方。被移除的字段是最常见的停用类型,也是最常被忽略的一种。
本文的技术内容未经独立审核。如有错误,请告诉我们,我们会更正。