微服務的合約、呼叫方式和 API

App Engine 中的微服務通常會使用 HTTP 為基礎的 RESTful API 呼叫彼此。您也可以使用工作佇列在背景叫用微服務,在這種情況下,您必須遵守下述 API 設計原則。請務必遵循特定模式,以確保微服務型應用程式能夠穩定運作、安全無虞且效能良好。

使用完善合約

微服務型應用程式的一大要點,就是能夠部署彼此完全獨立的微服務。為達到這種獨立性,每項微服務都必須提供定義完善的版本化合約給用戶端,而這些用戶端是其他微服務。如果不確定是否有其他微服務需依賴特定版本化合約,則每項服務都必須遵守這些版本化合約。請注意,其他微服務可能需要復原至先前的程式碼版本,而該版本可能需要先前的合約,因此在制定淘汰及終止政策時請務必考量這一點。

如何培養出使用完善版本化合約的風氣,或許是機構在打造穩定微服務型應用程式的過程中最大的挑戰。開發團隊必須充分瞭解破壞性和非破壞性變更之間的差異、知道何時需要推出新的主要版本,以及從服務中移除舊合約的方式和時機。團隊必須採用適當的溝通技巧 (包含淘汰和終止通知),確保成員隨時掌握微服務合約的異動情形。這聽起來似乎是項大工程,但如果開發團隊培養出使用這類做法的風氣,就能隨著時間大幅提高作業速度和品質。

呼叫微服務

服務和程式碼版本可直接呼叫。因此,您可以在使用現有程式碼版本的同時部署新的程式碼版本,而且可先對新的程式碼進行測試,再將它設為預設提供的版本。

每項 App Engine 專案都有預設服務,且每項服務都有預設程式碼版本。如要針對含有應用程式 ID my-app 的專案預設版本呼叫其預設服務,請使用以下網址:

http://my-app.appspot.com

如果您部署了名為 user-service 的服務,可以使用以下網址存取該服務的預設提供版本:

http://user-service.my-app.appspot.com

如果您另外部署了名為 banana 的非預設程式碼版本到 user-service 服務,可以使用以下網址直接存取該程式碼版本:

http://banana.user-service.my-app.appspot.com

此外,如果您另外部署了名為 cherry 的非預設程式碼版本到 default 服務,可以使用以下網址存取該程式碼版本:

http://cherry.my-app.appspot.com

App Engine 規定預設服務中的程式碼版本名稱不得與服務名稱相牴觸。

只有在進行基本測試,或是為了方便進行 A/B 版本測試、更新和復原作業時,才可直接呼叫特定程式碼版本。用戶端程式碼應該只呼叫預設服務或特定服務的預設提供版本:

http://my-app.appspot.com
http://user-service.my-app.appspot.com

這種呼叫方式可讓微服務部署新版服務 (包括錯誤修正內容),而不需變更用戶端。

上述所有的主機名稱都有對應的 HTTPS。App Engine 用於 appspot.com 的內建憑證不支援服務和版本的深層主機名稱,因此名稱必須使用 -dot- 整併。舉例來說,下列主機名稱是前述範例改為 HTTPS 後的對應主機名稱:

https://my-app.appspot.com
https://user-service-dot-my-app.appspot.com
https://banana-dot-user-service-dot-my-app.appspot.com  # whew, that's a mouthful
https://cherry-dot-my-app.appspot.com

使用 API 版本

每個微服務 API 的網址中都應有主要 API 版本,例如:

/user-service/v1/

這個主要 API 版本會在記錄中清楚標明呼叫的是微服務的哪個 API 版本。更重要的是,主要 API 版本會產生不同的網址,讓新的主要 API 版本可與舊的主要 API 版本並行提供:

/user-service/v1/
/user-service/v2/

您不需要在網址中加入次要 API 版本,因為次要 API 版本就定義上來說不會造成任何破壞性變更。事實上,在網址中加入次要 API 版本會使得網址數量增加,導致用戶端不一定能移至新的次要 API 版本。

請注意,本文假設有一個持續整合和持續推送軟體更新的環境,其中主要分支版本一律會部署到 App Engine。本文會提到兩種不同的版本概念:

  • 程式碼版本:直接對應到 App Engine 服務版本,並代表主要分支版本的特定修訂版本標記。

  • API 版本:直接對應到 API 網址,並代表要求引數的型態、回應文件的型態,以及 API 行為。

本文同時假設進行單一程式碼部署作業後,會在常用程式碼版本中同時實作舊版和新版 API。舉例來說,您部署的主要分支版本可能會同時實作 /user-service/v1//user-service/v2/。在發佈新的次要和修補版本時,這種做法可讓您將兩個程式碼版本的流量分開,不受程式碼實際實作的 API 版本影響。

您的機構可選擇在不同的程式碼分支版本中開發 /user-service/v1//user-service/v2/;也就是說,沒有任何程式碼部署作業會同時實作這兩者。App Engine 也支援這種模型,但如要將流量分開,您必須將主要 API 版本移到服務名稱中。舉例來說,用戶端必須使用下列網址:

http://user-service-v1.my-app.appspot.com/user-service/v1/
http://user-service-v2.my-app.appspot.com/user-service/v2/

主要 API 版本移入服務名稱中,例如 user-service-v1user-service-v2 (在這個模型中,路徑的 /v1//v2/ 部分是多餘的,您可以將其移除,不過在進行記錄分析時,這些部分或許仍可派上用場)。使用這個模型時,您可能需要更新部署指令碼,才能在主要 API 版本有所異動時部署新服務,因此需要多費點功夫。另外也請留意每個 App Engine 應用程式允許的服務數量上限

破壞性和非破壞性變更的比較

請務必瞭解破壞性和非破壞性變更之間的差異。破壞性變更往往具有削減性質,意思是會移除要求或回應文件的某些部分。變更文件的型態或金鑰名稱可能會導致破壞性變更。新的必要引數一律是破壞性變更。如果微服務的行為有所改變,也會發生破壞性變更。

非破壞性變更往往具有添加性質。新的選用要求引數或回應文件中新的額外部分都是非破壞性變更。如果想達成非破壞性變更,傳輸過程中的序列化選擇將是關鍵。許多序列化格式都支援非破壞性變更,例如 JSON、Protocol Buffers 或 Thrift。在去序列化時,這些序列化格式會默默忽略非預期的額外資訊。在動態程式語言中,額外資訊會直接顯示在去序列化的物件中。

請參考下列適用於 /user-service/v1/ 服務的 JSON 定義:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

以下破壞性變更需要將服務的版本重設為 /user-service/v2/

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

不過,以下非破壞性變更不需要新版本:

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

部署新的非破壞性次要 API 版本

部署新的次要 API 版本時,App Engine 可讓您同時發佈新的程式碼版本和舊的程式碼版本。在 App Engine 中,雖然您可以直接呼叫任何已部署的版本,但只有一個版本是預設提供的版本;請回想一下,每項服務都有一個預設提供的版本。在這個範例中,有一個舊的程式碼版本叫做 apple,其剛好是預設提供的版本,而我們同時要部署叫做 banana 的新程式碼版本。請注意,因為我們部署的是非破壞性次要 API 變更,因此這兩個版本的微服務網址都是 /user-service/v1/

App Engine 提供相關機制,可將新的程式碼版本 banana 標示為預設提供的版本,進而自動將流量從 apple 遷移到 banana。設定新的預設提供版本後,所有的新要求都會轉送至 banana,而非 apple。如要更新至實作新的次要或修補 API 版本的新程式碼版本,但不想對用戶端微服務造成影響,就可以採用這種做法。

如果發生錯誤,只要反向進行上述流程即可復原至舊的版本,也就是將預設提供的版本改回我們範例中的舊版本 apple。這樣一來,所有的新要求都會轉送至舊的程式碼版本,而非 banana。不過請注意,處理中的要求仍可繼續完成。

App Engine 也可讓您只將特定百分比的流量導向新的程式碼版本;這項程序通常稱為「初期測試版」程序,而這項機制在 App Engine 中稱為流量拆分。您可以將 1%、10%、50% 或任何所需百分比的流量導向新的程式碼版本,並隨著時間調整這個數值。舉例來說,您可以發佈新的程式碼版本 15 分鐘,並在這段時間內慢慢提高流量,看看是否有任何問題發生,導致您可能要復原至舊的版本。這項機制還可讓您對兩種程式碼版本進行 A/B 版本測試:將流量拆分設定為 50%,然後比較這兩種程式碼版本的成效和錯誤率,以確認是否達成預期中的改善效果。

下圖顯示的是 GCP 主控台中的流量拆分設定:

Google Cloud Platform 主控台中的流量拆分設定

部署新的破壞性主要 API 版本

部署破壞性主要 API 版本時,更新和復原的流程與非破壞性次要 API 版本相同。不過,由於破壞性 API 版本是新發佈的網址 (例如 /user-service/v2/),因此您通常不會進行任何流量拆分或 A/B 版本測試。當然,如果您變更了舊的主要 API 版本的基礎實作設定,您可能仍會想要使用流量拆分功能來進行測試,確認舊的主要 API 版本可繼續正常運作。

部署新的主要 API 版本時,請務必記得系統仍可能會繼續提供舊的主要 API 版本。舉例來說,發佈 /user-service/v2/ 後,系統仍可能會提供 /user-service/v1/。這是獨立程式碼版本的一大要點。如要終止舊的主要 API 版本,您必須確認沒有任何其他微服務需要這些版本,包括可能需要復原至舊程式碼版本的其他微服務。

以具體實例來說明,假設您有一個名為 web-app 的微服務需依賴另一個名為 user-service 的微服務。user-service 需要變更一些基礎實作設定,像是將 firstNamelastName 合併成叫做 name 的單一欄位,而這類變更會造成無法支援 web-app 目前使用的舊版主要 API。也就是說,user-service 必須終止舊的主要 API 版本。

為了進行這項變更,有三項不同的部署作業必須完成:

  • 首先,user-service 必須部署 /user-service/v2/,但同時仍支援 /user-service/v1/。如要進行這項部署作業,您可能需要編寫暫時性的程式碼來支援回溯相容性,微服務型應用程式常常會出現這類結果

  • 接著,web-app 必須部署更新過的程式碼,將依附元件從 /user-service/v1/ 變成 /user-service/v2/

  • 最後,user-service 團隊確認 web-app 不再需要 /user-service/v1/web-app 不需要復原後,即可部署程式碼,移除舊的 /user-service/v1/ 端點和支援舊版本所需的任何暫時性程式碼。

雖然看起來是項大工程,但對於微服務型應用程式和獨立的開發版本週期而言,這是必經的流程。明確來說,這項流程看似環環相扣,但重點在於上述每個步驟都可依獨立的時程進行,且更新和復原只會發生在單一微服務範圍中。這些步驟只有順序是固定的,實際執行時間可以是好幾個小時、好幾天,甚至是好幾個星期。

後續步驟

本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Python 2 適用的 App Engine 標準環境