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

責(zé)任編輯:editor004

作者: Brandur Leach

2017-08-28 11:20:09

摘自:INFOQ

然后,我們會按照時間向前追溯,請求這個過程中找到的每一個版本變更模塊,直到找到目標(biāo)版本:Event的request 字段之前是一個字符串,現(xiàn)在是一個還包含冪等鍵的子對象(在上述版本變更中產(chǎn)生的):

談到API,它的變更并不受人歡迎。對于軟件開發(fā)人員來說,他們早已習(xí)慣了快速頻繁的功能迭代;而API開發(fā)者卻不一樣,哪怕只有一位用戶調(diào)用了API,那么這個API想要改動就很麻煩了,它牽一發(fā)而動全身。我們中許多人都了解Unix操作系統(tǒng)的演化。1994年,Unix-Haters手冊發(fā)布,其中包含很多有關(guān)該軟件的郵件——內(nèi)容無所不包,從專門針對Teletype機(jī)器而優(yōu)化的、過度隱晦的命令名稱,到不可逆的文件刪除,再到選項過多的、不直觀的程序本身。20多年后,甚至是在眾多的現(xiàn)代衍生系統(tǒng)中,這些吐槽中的絕大多數(shù)仍然適用。這是因為,Unix的應(yīng)用已經(jīng)如此廣泛,改變其行為影響巨大。但是,無論如何,它與客戶訂立的契約,定義了Unix接口的行為方式。

類似地,一個API代表了一份通信契約,沒有通力的配合和大量的工作是無法修改的。由于許許多多的企業(yè)都將Stripe作為基礎(chǔ)設(shè)施,所以我們從Stripe成立開始就一直在考慮這些契約。截至目前,我們要維護(hù)自2011年公司成立以來每個API的每個版本的兼容性。在這篇文章中,我們將分享在Stripe我們是如何管理API版本的。

編寫代碼集成API的過程中會加入某些固定的預(yù)期。如果一個端點返回一個名為verified的布爾型字段用于說明一個銀行賬戶的狀態(tài),那么用戶可能會編寫如下代碼:

if bank_account[:verified] ...else ...end

如果我們后來使用一個status字段代替了銀行賬戶的布爾型字段verified,由它包含verified的值(我們在2014年這樣做過),那么上述代碼就會被破壞,因為它依賴于一個此時已經(jīng)不存在的字段。這種類型的變更不具備向后兼容性,是我們應(yīng)該避免的。以前有的字段應(yīng)該一直保留,而且類型和名稱應(yīng)該保持不變。不過,不是所有的變更都是向后不兼容的;例如,新增一個API端點,或者向一個已經(jīng)存在但曾未用過的API端點添加一個新字段,這些都是安全的。

以通力合作為基礎(chǔ),我們也許能讓我們的用戶了解我們將要做出的變更,并讓他們更新自己的集成代碼,但即使可以這樣做,也不是很友好的方式。就像電網(wǎng)連接或供水,在連接好之后,API應(yīng)該盡可能地保持運(yùn)行不中斷。

Stripe的使命是提供互聯(lián)網(wǎng)經(jīng)濟(jì)基礎(chǔ)設(shè)施。就像電力公司不應(yīng)該每隔兩年就改變電壓一樣,我們認(rèn)為,我們應(yīng)該讓用戶相信,我們提供的Web API會盡可能地保持穩(wěn)定。

API版本控制方案

Web API演進(jìn)的一種常見方法是使用版本控制。用戶在發(fā)出請求時指定版本,API提供商可以根據(jù)需要修改下一個版本而又保持和當(dāng)前版本兼容。當(dāng)新版本發(fā)布后,用戶可以在方便的時候升級。

這經(jīng)常被視為一種主要的版本控制方案,將類似v1、v2、v3這樣的名稱作為URL前綴(如/v1/widgets)或者通過HTTP頭(如Accept)傳遞。這是一種有效的方法,但是,其主要缺點是,版本之間的變化太大,對用戶的影響也太大,其痛苦程度都快趕上重新集成了。這種方法也沒有明顯的優(yōu)勢,因為不愿意或無法升級的用戶就被困在了舊版本上。這時,提供商就必須做出艱難的選擇,是退役API版本,還是舍棄那些用戶,或者付出相當(dāng)大的代價沒完沒了地維護(hù)舊版本。雖然讓提供商維護(hù)舊版本可能乍看之下對用戶是有好處的,但是,他們也間接地付出了獲得更新的速度下降的代價,因為工程時間花在了維護(hù)舊代碼而不是開發(fā)新特性上。

在Stripe,我們通過滾動版本實現(xiàn)版本控制,版本命名使用了API發(fā)布的日期(如2017-05-24)。雖然向后不兼容,但每個版本包含一小部分變化,這讓增量升級變得相對容易,這樣一來,集成就可以跟上版本更新的步伐。

用戶第一次發(fā)起API請求時,他們的賬戶會自動釘選到最新的可用版本,之后,他們發(fā)起的每次API調(diào)用都會被隱式地分配到那個版本。這種方法可以確保用戶不會突然接收到破壞性修改,并通過減少必要的配置讓最初的集成少了些痛苦。用戶可以手動設(shè)置Stripe-Version頭,或者從Stripe控制板更新其賬戶釘選的版本,覆寫任意單個請求的調(diào)用版本。

可能有讀者已經(jīng)注意到,Stripe API也有使用前綴路徑定義主版本(如/v1/charges)的情況。雖然我們確實會在某些時候使用這種方式,但是目前使用的方式在一段時間內(nèi)將不會改變。如上所述,主版本變化往往會讓升級很痛苦,而且,我們很難想象,一個API的重新設(shè)計重要到要讓用戶受到這種程度的影響。我們目前采用的方法已經(jīng)支撐我們在過去六年中完成了將近100次向后不兼容的升級。

底層版本控制

版本控制總是要兼顧改善開發(fā)體驗和維護(hù)舊版本的成本。我們努力實現(xiàn)前者,同時又最小化后者,并實現(xiàn)了一個版本控制系統(tǒng)幫助我們實現(xiàn)這一目標(biāo)。讓我們快速瀏覽一下它的工作原理。Stripe API每一種可能的響應(yīng)都被編寫成類,我們稱之為API資源。API資源使用DSL定義可用的字段:

class ChargeAPIResource required :id, String required :amount, Integerend

API資源被記錄下來,其所描述的結(jié)構(gòu)就是我們希望API的當(dāng)前版本返回的內(nèi)容。當(dāng)我們需要做出向后不兼容的變更時,我們將其封裝在一個版本變更模塊中,其中定義了變更相關(guān)的注釋、一個轉(zhuǎn)換以及符合條件需要修改的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 endend

在主列表中為版本變更分配一個相應(yīng)的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

版本變更被記錄下來,因此可以期望它們從當(dāng)前的API版本按照順序自動向后適用。但每次版本變更都會假設(shè),“即使后續(xù)可能有新的變更,但它們收到的數(shù)據(jù)應(yīng)該和該API最初編寫出來時一樣”。

在生成響應(yīng)時,API首先會通過描述當(dāng)前版本的API資源來格式化數(shù)據(jù),然后根據(jù)下面三項內(nèi)容中的一項確定目標(biāo)API的版本:

如果提供了的話,則根據(jù)Stripe-Version頭;如果請求是以用戶的名義發(fā)送,則根據(jù)經(jīng)過OAuth授權(quán)的應(yīng)用程序的版本;根據(jù)用戶釘選的版本,這是在用戶首次向Stripe發(fā)送請求時設(shè)定的。

然后,我們會按照時間向前追溯,請求這個過程中找到的每一個版本變更模塊,直到找到目標(biāo)版本:

  在返回響應(yīng)之前,請求由版本變更模塊處理

版本變更模塊會將更舊的版本從核心代碼路徑中剔除出去。在構(gòu)建新產(chǎn)品時,開發(fā)人員多半可以不考慮它們。

具有副作用的變更

大多數(shù)向后不兼容的API變更都會修改響應(yīng),但情況并非總是如此。有時候,可能需要進(jìn)行比較復(fù)雜的變更,其范圍超出了定義它的模塊。我們?yōu)檫@些模塊添加has_side_effects注解,它們定義的轉(zhuǎn)換變成了空操作:

class LegacyTransfers < AbstractVersionChange description "..." has_side_effectsend

在代碼的其他地方需要對它們進(jìn)行檢查,看看是否還有效:

VersionChanges.active?(LegacyTransfers)

這種弱化的封裝讓具有副作用的變更更加難以維護(hù),因此,我們會極力避免。

聲明式變更

自包含版本變更模塊的其中一個好處是,它可以定義注釋,說明它們影響的字段和資源。我們可以再次利用該注釋快速向用戶提供更多有用的信息。例如,我們的API變更日志是程序生成的,新版本的服務(wù)一部署,變更日志就會收到更新。

我們還針對特定的用戶裁剪API參考文檔。它知道誰登錄了,并根據(jù)賬戶的API版本注釋字段。這里,我們會警告開發(fā)人員,他們使用的API自釘選版本之后有向后不兼容的變更。Event的request 字段之前是一個字符串,現(xiàn)在是一個還包含冪等鍵的子對象(在上述版本變更中產(chǎn)生的):

  我們的文檔會檢測用戶的API版本并發(fā)出相關(guān)警告

最小化變更

提供廣泛的向后兼容性并不是免費(fèi)的;每個新版本都意味著更多需要理解和維護(hù)的代碼。我們盡力讓我們編寫的代碼清晰,但是,如果整個項目里到處都是無法清晰封裝、需要足夠時間和大量檢查的版本變更,則會延緩項目、降低可讀性,讓API變得更加脆弱。我們采用了一些度量指標(biāo),盡力避免招致這種昂貴的技術(shù)債務(wù)。

即使我們有可用的版本控制系統(tǒng),我們還是盡可能地避免使用它,并設(shè)法在最初設(shè)計時保證API的正確性。輸出變更是通過一個輕量級的API審核流程收集的,這些變更會被寫入一個簡單的支持文檔中,并提交到郵件列表。這讓每一個變更提案都可以被公司里更多的人看到,讓我們可以在它們發(fā)布之前發(fā)現(xiàn)錯誤和不一致的地方。

我們一直都注意停用和使用的取舍。保持兼容性很重要,但即便如此,我們最終還是會希望開始退役舊的API版本。幫助用戶遷移到API的新版本讓他們可以利用新特性,同時也簡化了我們構(gòu)建新特性的基礎(chǔ)。

變更的原則

滾動版本和支持這一機(jī)制的內(nèi)部框架,這兩者的結(jié)合讓我們吸引了大量的用戶,我們對API做了大量的變更,同時又將對現(xiàn)有集成的影響降至了最低。這種方式依賴于我們過去幾年來總結(jié)出的一些規(guī)則。我們認(rèn)為重要的——API升級應(yīng)該是:

“輕量級的(Lightweight)”。盡量降低升級成本(不管是對用戶而言,還是對我們自己而言)。“一等的(First-class)”。讓版本控制成為API的一等概念,這樣,可以用它保持文檔和工具的準(zhǔn)確和及時更新,并自動生成變更日志。“成本固定的(Fixed-cost)”。通過將舊版本密封進(jìn)版本變更模塊來最小化維護(hù)成本。換句話說,在編寫新代碼時需要考慮的舊版本行為越少越好。

圍繞REST、GraphQL、gRPC的爭論及這些技術(shù)的發(fā)展讓我們興奮,更廣泛地說,是Web API未來會發(fā)展成什么樣子讓我們興奮,我們希望在接下來的很長一段時間內(nèi)可以繼續(xù)支持版本控制方案。

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

鏈接已復(fù)制,快去分享吧

企業(yè)網(wǎng)版權(quán)所有?2010-2024 京ICP備09108050號-6京公網(wǎng)安備 11010502049343號