關於 API 版本管理的爭論,幾乎總是從錯誤的一端開始:版本號到底要放在哪裡。事實上,那是整個主題裡後果最輕微的一個決定。真正要緊的是究竟哪些變更才需要一個新版本,而多數團隊恰好在這裡判斷失準,而且是往掉以輕心的方向失準。他們發布了自認為純屬新增的東西,然後某個用戶端就壞了。
好用的思考模型是這樣:你的 API 是一份承諾,界定了呼叫端可以倚賴什麼。如果一次變更讓一個講理的呼叫端原本倚賴的東西失效,那它就是破壞性的。而呼叫端倚賴的東西,遠遠多過你的文件明確允許他們倚賴的範圍。
讓所有人都踩到的那種變更: 在回應裡多加一個欄位。它只是新增,照理說弄不壞一個寫得規矩的用戶端,可它偏偏經常弄壞真實世界的用戶端,因為其中有些會嚴格驗證回應,並拒絕任何未知欄位。等到對方的串接已經停擺、電話已經打過來,這究竟算你的錯還是他們的錯,早就沒有任何意義了。
究竟什麼才算破壞性變更
毫無疑問的破壞性變更: 刪除或改名一個欄位,改變欄位的型別,替請求加上必填參數,把驗證收緊,改變某個既有值的意義,或是更動呼叫端拿來做分支判斷的狀態碼。
看起來安全、實際上會出事: 在用戶端嚴格驗證的情況下往回應裡加欄位。改變陣列的順序,雖然你從未承諾過順序,呼叫端卻早就當成理所當然。更動某一則錯誤訊息,而有人正把它當字串比對。把一個同步操作改成非同步。
真正安全的變更: 新增一個選用的請求參數,新增一個端點,把驗證放寬,以及在列舉裡加入一個新值。最後這一項有前提:必須一開始就告訴過用戶端會出現未知的值,而且你能驗證他們確實處理得了。
這裡的規律是:安不安全取決於呼叫端實際上做了什麼,而不是規格允許做什麼。如果每個用戶端都在你手上,你可以逐一驗證。如果不在,那就假定有人正倚賴著你以為無關緊要的那一處,因為事實就是如此。
API 版本管理的做法與各自的代價
版本放在 URL 裡。 最常見的做法,優點是一目了然:/v1/orders 和 /v2/orders 一眼就是兩個不同的資源。代價是它會助長整個 API 一起跳版,於是只影響一個端點的變更把其餘全部拖下水,用戶端也只能一次全部遷移。
版本放在標頭裡。 讓 URL 保持穩定,也能做到更細的粒度,代價是它看不見。不特地去找,沒有人會在瀏覽器或某一行紀錄檔裡看到版本;而漏掉這個標頭的呼叫端,拿到的就是你設定的預設值,這個預設值必須由你有意識地決定。
依日期劃分版本。 呼叫端釘住一個日期,就拿到 API 在那一天的行為。Stripe 就是這樣記載這套做法的:每個帳戶有一個預設版本,個別請求還可以覆寫它。它給出的遷移步伐盡可能小,代價是把相容性的負擔挪進你自己的程式碼庫,而這套程式碼庫從此得長期維護版本之間的轉換。
不做版本,只做新增。 如果你真的能承諾永遠不刪除任何東西,這條路完全走得通,而且被嚴重低估。它的代價是不斷累積你刪不掉的欄位,以及你再也修不回來的行為,那是一筆慢慢滲出的稅,而不是一張突然到期的帳單。
沒有標準答案,只有你的遷移負擔和客戶的遷移負擔之間的一次交換。依日期劃分版本對呼叫端最友善,維運起來也最貴;把版本放進 URL 則正好相反。
讓破壞性變更變得可以承受
先擴張,再收斂。 把新欄位放在舊欄位旁邊,兩個都寫入資料,給用戶端足夠的時間搬過去,再在之後的版本裡把舊的拿掉。這樣一次破壞性變更就變成兩次安全的變更,多走這一步幾乎每次都值得。
把誰在用什麼量出來。 不知道還有誰留在舊版本上,就沒辦法安全地把它退役。請在每一次請求都記下版本與用戶端身分,這樣宣告淘汰才會是一場有憑有據的對話,而不是朝著黑暗喊一聲。
用機制通知,不要只寄一封信。 Deprecation 與 Sunset 這兩個回應標頭,可以讓用戶端用程式找出停用日期,這比寄到一個再也沒人看的信箱地址的訊息更容易被注意到。
給一個務實的緩衝期。 維護串接的是另有優先事項的人,比他們發布週期還短的期限,只會直接被略過。公開 API 六個月是常見做法;如果只是少數幾個你直接談過的已知合作夥伴,短一點也說得過去。
讓舊版本退役
正是上面那套量測,才讓這一步成為可能。公布日期,看著流量往下掉,然後點名聯絡那些還沒搬走的呼叫端。
要有長尾的心理準備。一定會有一些串接,客戶公司裡已經沒有人記得那是自己的東西,而它們只會在你把版本關掉的那一刻現身。這裡短暫停用很有用:在最終日期之前,挑幾個事先公布的時段把舊版本短暫關掉,讓故障發生在有人正等著它的時候,而不是發生在業務尖峰的正中間。
誠實的說法是:有些呼叫端要等到舊版本徹底不能用了才會動。與其被這件事嚇一跳,不如提前把它寫進計畫,並且確保失敗時回傳的是一則講清楚發生什麼事的明確錯誤,而不是一次逾時。
你並不需要的那個版本
多數內部 API 根本不需要版本管理,因為每個呼叫端都在你手上,兩邊可以一起改。替兩個自家服務之間的介面加上版本協商,是一套要花成本、卻什麼也保護不了的機器。
真正需要它的分界點,是你再也沒辦法把所有消費端同時部署上線的時候,不論對方屬於另一個團隊、另一個發布週期,還是另一家公司。這才是真正的觸發條件,跟這個 API 有多公開毫無關係。我們那篇無伺服器 API 指南裡的設計取捨,在邊緣上同樣適用。
Mecanik 在軟體開發業務裡會設計並長期維護這一類 API。版本方案本身很少是那個有意思的決定;而知道你的哪一個呼叫端還留在舊版本上,永遠都是。
常見問題
什麼樣的改動算是破壞性的 API 變更? 刪除或改名欄位、改變欄位型別、加上必填參數、收緊驗證、改變某個值的意義,或是更動呼叫端拿來做分支判斷的狀態碼。以下幾種在實務上同樣是破壞性的:用戶端嚴格驗證時往回應裡加欄位、改變呼叫端早已當成理所當然的陣列順序,以及修改有人正在比對的錯誤訊息文字。
API 版本該放在 URL 還是標頭? 放在 URL 看得見又單純,但會助長整個 API 一起跳版,逼著用戶端一次遷移全部內容。放在標頭能讓 URL 保持穩定、支援更細的粒度,卻在紀錄檔與瀏覽器裡都看不到,還需要你為漏帶標頭的呼叫端有意識地設定預設值。兩者都不算錯,它們只是在你的遷移負擔與客戶的遷移負擔之間做交換。
什麼是依日期劃分的 API 版本? 呼叫端釘住一個日期,就拿到 API 在那一天的行為,Stripe 的做法就是每個帳戶有一個預設版本,個別請求可以覆寫它。這種方式給用戶端的遷移步伐盡可能小,同時把相容性負擔挪進你的程式碼庫,由它長期維護版本之間的轉換。
API 淘汰的緩衝期該留多久? 公開 API 六個月是常見做法,如果只是少數幾個你直接談過的已知合作夥伴,短一點也說得過去。維護串接的是另有優先事項的人,所以比他們發布週期還短的緩衝期,不管公告得多清楚都會被錯過。
內部 API 需要做版本管理嗎? 通常不需要,因為每個呼叫端都在你手上,兩邊可以一起改。版本管理開始變得必要的時刻,是你再也沒辦法把所有消費端同時部署上線,不論對方屬於另一個團隊、另一個發布週期還是另一家公司。觸發條件是這個,而不是 API 公不公開。
評論