快手、孩子王、华为等专家分享大模型在电商运营、母婴消费、翻译等行业场景的实际应用 了解详情
写点什么

Stripe 面向未来的 APIs 版本控制方案

  • 2017-08-27
  • 本文字数:4125 字

    阅读完需:约 14 分钟

谈到 API,它的变更并不受人欢迎。对于软件开发人员来说,他们早已习惯了快速频繁的功能迭代;而 API 开发者却不一样,哪怕只有一位用户调用了 API,那么这个 API 想要改动就很麻烦了,它牵一发而动全身。我们中许多人都了解 Unix 操作系统的演化。1994 年, Unix-Haters 手册发布,其中包含很多有关该软件的邮件——内容无所不包,从专门针对 Teletype 机器而优化的、过度隐晦的命令名称,到不可逆的文件删除,再到选项过多的、不直观的程序本身。20 多年后,甚至是在众多的现代衍生系统中,这些吐槽中的绝大多数仍然适用。这是因为,Unix 的应用已经如此广泛,改变其行为影响巨大。但是,无论如何,它与客户订立的契约,定义了 Unix 接口的行为方式。

类似地,一个 API 代表了一份通信契约,没有通力的配合和大量的工作是无法修改的。由于许许多多的企业都将 Stripe 作为基础设施,所以我们从 Stripe 成立开始就一直在考虑这些契约。截至目前,我们要维护自 2011 年公司成立以来每个 API 的每个版本的兼容性。在这篇文章中,我们将分享在 Stripe 我们是如何管理 API 版本的。

编写代码集成 API 的过程中会加入某些固定的预期。如果一个端点返回一个名为verified的布尔型字段用于说明一个银行账户的状态,那么用户可能会编写如下代码:

复制代码
if bank_account[:verified]
...
else
...
end

如果我们后来使用一个status字段代替了银行账户的布尔型字段verified,由它包含verified的值(我们在 2014 年这样做过),那么上述代码就会被破坏,因为它依赖于一个此时已经不存在的字段。这种类型的变更不具备向后兼容性,是我们应该避免的。以前有的字段应该一直保留,而且类型和名称应该保持不变。不过,不是所有的变更都是向后不兼容的;例如,新增一个 API 端点,或者向一个已经存在但曾未用过的 API 端点添加一个新字段,这些都是安全的。

以通力合作为基础,我们也许能让我们的用户了解我们将要做出的变更,并让他们更新自己的集成代码,但即使可以这样做,也不是很友好的方式。就像电网连接或供水,在连接好之后,API 应该尽可能地保持运行不中断。

Stripe 的使命是提供互联网经济基础设施。就像电力公司不应该每隔两年就改变电压一样,我们认为,我们应该让用户相信,我们提供的 Web API 会尽可能地保持稳定。

API 版本控制方案

Web API 演进的一种常见方法是使用版本控制。用户在发出请求时指定版本,API 提供商可以根据需要修改下一个版本而又保持和当前版本兼容。当新版本发布后,用户可以在方便的时候升级。

这经常被视为一种主要的版本控制方案,将类似v1v2v3这样的名称作为 URL 前缀(如/v1/widgets)或者通过 HTTP 头(如Accept)传递。这是一种有效的方法,但是,其主要缺点是,版本之间的变化太大,对用户的影响也太大,其痛苦程度都快赶上重新集成了。这种方法也没有明显的优势,因为不愿意或无法升级的用户就被困在了旧版本上。这时,提供商就必须做出艰难的选择,是退役 API 版本,还是舍弃那些用户,或者付出相当大的代价没完没了地维护旧版本。虽然让提供商维护旧版本可能乍看之下对用户是有好处的,但是,他们也间接地付出了获得更新的速度下降的代价,因为工程时间花在了维护旧代码而不是开发新特性上。

在 Stripe,我们通过滚动版本实现版本控制,版本命名使用了 API 发布的日期(如2017-05-24)。虽然向后不兼容,但每个版本包含一小部分变化,这让增量升级变得相对容易,这样一来,集成就可以跟上版本更新的步伐。

用户第一次发起 API 请求时,他们的账户会自动钉选到最新的可用版本,之后,他们发起的每次 API 调用都会被隐式地分配到那个版本。这种方法可以确保用户不会突然接收到破坏性修改,并通过减少必要的配置让最初的集成少了些痛苦。用户可以手动设置Stripe-Version头,或者从 Stripe 控制板更新其账户钉选的版本,覆写任意单个请求的调用版本。

可能有读者已经注意到,Stripe API 也有使用前缀路径定义主版本(如/v1/charges)的情况。虽然我们确实会在某些时候使用这种方式,但是目前使用的方式在一段时间内将不会改变。如上所述,主版本变化往往会让升级很痛苦,而且,我们很难想象,一个 API 的重新设计重要到要让用户受到这种程度的影响。我们目前采用的方法已经支撑我们在过去六年中完成了将近 100 次向后不兼容的升级。

底层版本控制

版本控制总是要兼顾改善开发体验和维护旧版本的成本。我们努力实现前者,同时又最小化后者,并实现了一个版本控制系统帮助我们实现这一目标。让我们快速浏览一下它的工作原理。Stripe API 每一种可能的响应都被编写成类,我们称之为 API 资源。API 资源使用 DSL 定义可用的字段:

复制代码
class ChargeAPIResource
required :id, String
required :amount, Integer
end

API 资源被记录下来,其所描述的结构就是我们希望 API 的当前版本返回的内容。当我们需要做出向后不兼容的变更时,我们将其封装在一个版本变更模块中,其中定义了变更相关的注释、一个转换以及符合条件需要修改的 API 资源类型集:

复制代码
class CollapseEventRequest < AbstractVersionChange
description \
"Event objects (and webhooks) will now render " \
"`request` subobject that contains a request ID " \
"and idempotency key instead of just a string " \
"request ID."
response EventAPIResource do
change :request, type_old: String, type_new: Hash
run do |data|
data.merge(:request => data[:request][:id])
end
end
end

在主列表中为版本变更分配一个相应的 API 版本:

复制代码
class VersionChanges
VERSIONS = {
'2017-05-25' => [
Change::AccountTypes,
Change::CollapseEventRequest,
Change::EventAccountToUserID
],
'2017-04-06' => [Change::LegacyTransfers],
'2017-02-14' => [
Change::AutoexpandChargeDispute,
Change::AutoexpandChargeRule
],
'2017-01-27' => [Change::SourcedTransfersOnBts],
...
}
end

版本变更被记录下来,因此可以期望它们从当前的 API 版本按照顺序自动向后适用。但每次版本变更都会假设,“即使后续可能有新的变更,但它们收到的数据应该和该 API 最初编写出来时一样”。

在生成响应时,API 首先会通过描述当前版本的 API 资源来格式化数据,然后根据下面三项内容中的一项确定目标 API 的版本:

  • 如果提供了的话,则根据Stripe-Version头;
  • 如果请求是以用户的名义发送,则根据经过 OAuth 授权的应用程序的版本;
  • 根据用户钉选的版本,这是在用户首次向 Stripe 发送请求时设定的。

然后,我们会按照时间向前追溯,请求这个过程中找到的每一个版本变更模块,直到找到目标版本:

在返回响应之前,请求由版本变更模块处理

版本变更模块会将更旧的版本从核心代码路径中剔除出去。在构建新产品时,开发人员多半可以不考虑它们。

具有副作用的变更

大多数向后不兼容的 API 变更都会修改响应,但情况并非总是如此。有时候,可能需要进行比较复杂的变更,其范围超出了定义它的模块。我们为这些模块添加has_side_effects注解,它们定义的转换变成了空操作:

复制代码
class LegacyTransfers < AbstractVersionChange
description "..."
has_side_effects
end

在代码的其他地方需要对它们进行检查,看看是否还有效:

复制代码
VersionChanges.active?(LegacyTransfers)

这种弱化的封装让具有副作用的变更更加难以维护,因此,我们会极力避免。

声明式变更

自包含版本变更模块的其中一个好处是,它可以定义注释,说明它们影响的字段和资源。我们可以再次利用该注释快速向用户提供更多有用的信息。例如,我们的 API 变更日志是程序生成的,新版本的服务一部署,变更日志就会收到更新。

我们还针对特定的用户裁剪 API 参考文档。它知道谁登录了,并根据账户的 API 版本注释字段。这里,我们会警告开发人员,他们使用的 API 自钉选版本之后有向后不兼容的变更。Event 的request 字段之前是一个字符串,现在是一个还包含幂等键的子对象(在上述版本变更中产生的):

我们的文档会检测用户的 API 版本并发出相关警告

最小化变更

提供广泛的向后兼容性并不是免费的;每个新版本都意味着更多需要理解和维护的代码。我们尽力让我们编写的代码清晰,但是,如果整个项目里到处都是无法清晰封装、需要足够时间和大量检查的版本变更,则会延缓项目、降低可读性,让 API 变得更加脆弱。我们采用了一些度量指标,尽力避免招致这种昂贵的技术债务。

即使我们有可用的版本控制系统,我们还是尽可能地避免使用它,并设法在最初设计时保证 API 的正确性。输出变更是通过一个轻量级的 API 审核流程收集的,这些变更会被写入一个简单的支持文档中,并提交到邮件列表。这让每一个变更提案都可以被公司里更多的人看到,让我们可以在它们发布之前发现错误和不一致的地方。

我们一直都注意停用和使用的取舍。保持兼容性很重要,但即便如此,我们最终还是会希望开始退役旧的 API 版本。帮助用户迁移到 API 的新版本让他们可以利用新特性,同时也简化了我们构建新特性的基础。

变更的原则

滚动版本和支持这一机制的内部框架,这两者的结合让我们吸引了大量的用户,我们对 API 做了大量的变更,同时又将对现有集成的影响降至了最低。这种方式依赖于我们过去几年来总结出的一些规则。我们认为重要的——API 升级应该是:

  • “轻量级的(Lightweight)”。尽量降低升级成本(不管是对用户而言,还是对我们自己而言)。
  • “一等的(First-class)”。让版本控制成为 API 的一等概念,这样,可以用它保持文档和工具的准确和及时更新,并自动生成变更日志。
  • “成本固定的(Fixed-cost)”。通过将旧版本密封进版本变更模块来最小化维护成本。换句话说,在编写新代码时需要考虑的旧版本行为越少越好。

围绕 REST、GraphQL、gRPC 的争论及这些技术的发展让我们兴奋,更广泛地说,是 Web API 未来会发展成什么样子让我们兴奋,我们希望在接下来的很长一段时间内可以继续支持版本控制方案。

查看英文原文 APIs as infrastructure: future-proofing Stripe with versioning


感谢雨多田光对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-08-27 19:001847
用户头像

发布了 1008 篇内容, 共 387.4 次阅读, 收获喜欢 344 次。

关注

评论

发布
暂无评论
发现更多内容

架构实战营 - 模块 6- 作业

泄矢的呼啦圈

架构实战营

网络攻防学习笔记 Day48

穿过生命散发芬芳

网络攻防 6月日更

深入浅出 Ajax 的核心原理

悟空聊架构

ajax 架构 大前端 6月日更 悟空聊架构

软件技术从1996年以后就停滞不前了!?难道软件技术难返“春天”了么?

三掌柜

试用期 签约计划 人气作者 TOP10

JAVA面向对象(六)--类和对象

加百利

Java 6月日更

新视界 | 透过鸿蒙OS看手机操作系统

架构精进之路

操作系统 HarmonyOS 6月日更

第六次作业

Geek_9cf7b5

☕【JVM性能调优】「CMS垃圾回收器」调优化方案

洛神灬殇

CMS GC jvm调优 6月日更

我曾与纪伯伦的对话

卢卡多多

6月日更

【21-11】PowerShell文件 IO 操作

耳东@Erdong

PowerShell 6月日更

ServeMux详解

Rayjun

HTTP Go 语言

项目经理如何才能不背锅

escray

学习 极客时间 朱赟的技术管理课 6月日更

Nike上货提醒:监控到商品尺码SKU

木头

Nike监控 上货监控 SKU监控

科技进步道阻且长,仍未来可期

架构精进之路

科技 6月日更

JavaScript 学习(五)

空城机

JavaScript 大前端 6月日更

上云赋智的企业们,也许是这届618的最大赢家

脑极体

Linux之cd命令

入门小站

Linux

Kubernetes手记(15)- dashboard

雪雷

k8s 6月日更

Java常用的几个Json库,性能强势对比!

xcbeyond

性能优化 6月日更

解读SpringBoot和SpringMVC中配置类的@Impot等导入是如何解析的

赵镇

spring

【Flutter 专题】100 何为 Flutter Widgets ?

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 6月日更

「SQL数据分析系列」7. 数据生成、转换以及操作

Databri_AI

数据库 sql

Kubernetes手记(16)- 网络通信

雪雷

6月日更

架构实战营模块6作业

Vic

架构实战营

Bzz矿机分币APP,深圳云矿机系统开发

自然语言处理技术

Qien Z.

nlp 6月日更

简单实现springmvc中的请求处理

赵镇

springmvc

微服务的由来

卢卡多多

微服务 6月日更

JSON转Excel

入门小站

工具分享

【Vue2.x 源码学习】第十八篇 - 根据 render 函数,生成 vnode

Brave

源码 vue2 6月日更

【架构实战营】第 6 模块作业

swordman

架构实战营

Stripe面向未来的APIs 版本控制方案_语言 & 开发_Brandur Leach_InfoQ精选文章