写点什么

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:002320
用户头像

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

关注

评论

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

《数据安全与流通:技术、架构与实践》新书发布

星环科技

BOE(京东方)发布2023年三季度报告 营收利润双增长 盈利能力持续提升

科技热闻

星环科技分布式向量数据库Transwarp Hippo正式发布,拓展大语言模型时间和空间维度

星环科技

史上最短苹果发布会;三星、LG、高通联手进军 XR 市场丨 RTE 开发者日报 Vol.74

声网

golang结构体内存对齐

fm

华为云开源校园行 | 线下meetup • 电子科技大学站

华为云开源

开源 云原生 华为云 Meetup

软件开发全文档归档,开发、管理、实施、运维、服务巡检、信息安全、安全运维

金陵老街

概要设计 详细架构设计文档 软件文档

TDengine 受邀参加 CNCC 2023,大会现场展位前“人山人海”!

TDengine

tdengine 时序数据库

如何优雅的开发?低代码搭建应用如此轻松

互联网工科生

软件开发 低代码平台 JNPF

API商品数据接口调用实战

Noah

NFTScan 发展成为 PlatON 网络最大验证者节点之一

NFT Research

NFT NFT\ NFTScan

一文吃透低代码开发与传统IT开发的区别

树上有只程序猿

软件开发 低代码开发 IT开发

KaiwuDB 获山东省工信厅“信息化应用创新优秀解决方案”奖

KaiwuDB

微服务之负载均衡使用场景

互联网工科生

负载均衡 微服务

KaiwuDB 联合信通院数据库应用创新实验室召开数据库技术研讨沙龙

KaiwuDB

第19期 | GPTSecurity周报

云起无垠

大模型时代的人工智能+大数据平台,加速创新涌现

阿里云大数据AI技术

人工智能

“敏捷教练进阶课程”12月2-3日 · A-CSM认证在线周末班【分时段模块化教学】CST导师亲授

ShineScrum

如何构建适合自己的DevOps软件测试改进方案

DevOps和数字孪生

DevOps 软件测试 仿真建模

文心一言 VS 讯飞星火 VS chatgpt (125)-- 算法导论10.6 6题

福大大架构师每日一题

福大大架构师每日一题

打造企业级门户,WorkPlus助您打造个性化与高效的企业通讯平台

BeeWorks

掌握Spring事件监听器的内部逻辑与实现

华为云开发者联盟

spring 开发 华为云 华为云开发者联盟

CAEE2023 | 造物云×华为云共建电子电路智慧云工厂,引领产业创新发展浪潮

极客天地

证券开源领航者!国金证券通过中国信通院可信开源治理“先进级”评估

新消费日报

用 Milvus 和 NVIDIA Merlin 搭建高效推荐系统

Zilliz

nvidia Milvus Zilliz 向量数据库

短视频不仅带火了直播/PK,也带着RTC走进了军备竞赛

X2Rtc

音视频 短视频 RTC

全网最详细4W字Flink全面解析与实践(下)

Java随想录

Java 大数据 flink

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