BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 Brandur Leach 关注 0 他的粉丝 ,译者 谢丽 关注 11 他的粉丝 发布于 2017年8月28日. 估计阅读时间: 15 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

谈到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)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT