本參考指南是四部曲系列的第二部,說明如何設計、建構及部署微服務。本系列文章將說明微服務架構的各種元素。本系列文章將說明微服務架構模式的優缺點,以及如何套用這類模式。
- 微服務簡介
- 將單體式應用程式重構為微服務 (本文件)
- 微服務設定中的服務間通訊
- 微服務應用程式中的分散式追蹤
本系列文章適用於設計及實作遷移作業的應用程式開發人員和架構師,以便將單體應用程式重構為微服務應用程式。
將單體式應用程式轉換為微服務的過程,就是一種應用程式現代化。為了完成應用程式現代化,建議您不要同時重構所有程式碼。建議您改為逐步重構單體應用程式。當您逐步重構應用程式時,您會逐漸建構由微服務組成的新應用程式,並與單體式應用程式一同執行應用程式。這種做法也稱為「Strangler Fig」模式。隨著時間推移,單體應用程式實作的功能數量會逐漸減少,直到完全消失或成為其他微服務為止。
如要將功能從單體中分離,您必須小心地擷取功能的資料、邏輯和面向使用者的元件,並將這些項目重新導向至新服務。請務必先充分瞭解問題空間,再進入解決方案空間。
瞭解問題空間後,您就能瞭解該領域的自然邊界,並提供適當的隔離層級。建議您在充分瞭解該網域之前,先建立較大的服務,而非較小的服務。
定義服務範圍是一項疊代程序。由於這個程序需要花費大量心力,您必須持續評估解耦的成本與所獲得的效益。以下因素可協助您評估如何分離單體:
- 請勿一次重構所有項目。如要優先考量服務分離作業,請評估成本與效益。
- 微服務架構中的服務會依業務需求而非技術需求進行安排。
- 逐步遷移服務時,請設定服務和單體之間的通訊,以便透過明確定義的 API 合約進行。
- 微服務需要更多自動化功能:請事先考慮持續整合 (CI)、持續部署 (CD)、集中式記錄和監控。
以下各節將討論各種策略,以便分離服務,並逐步遷移單體應用程式。
透過領域導向設計分離
微服務應以業務功能為設計重點,而非資料存取或訊息傳遞等橫向層。微服務也應具有鬆散的耦合和高功能內聚性。如果您可以變更一項服務,而不需要同時更新其他服務,則表示微服務是鬆散耦合的。如果微服務具有單一且明確的用途 (例如管理使用者帳戶或處理付款),就會具有內聚性。
領域驅動設計 (DDD) 需要瞭解應用程式所屬領域。建立應用程式所需的領域知識,都掌握在瞭解該領域的專家手中。
您可以將 DDD 方法回溯套用至現有應用程式,步驟如下:
- 找出通用語言,也就是所有利害關係人共用的詞彙。開發人員在程式碼中使用非技術人員可理解的術語非常重要。程式碼應反映貴公司的程序。
- 找出單體應用程式中的相關模組,然後將常用字彙套用至這些模組。
- 定義受限的內容,將明確的界線套用至已識別的模組,並明確定義職責。您找出的受限內容是可重構為較小微服務的候選項目。
下圖顯示如何將受限的內容區塊套用至現有的電子商務應用程式:
圖 1. 應用程式功能會分成可遷移至服務的受限內容。
在圖 1 中,電子商務應用程式的功能會分割為受限的結構定義,並且會按照下列方式遷移至服務:
- 訂單管理和履行功能會綁定至下列類別:
- 訂單管理功能會遷移至訂單服務。
- 物流配送管理功能會遷移至配送服務。
- 商品目錄功能會遷移至商品目錄服務。
- 會計功能會綁定至單一類別:
- 消費者、賣家和第三方功能會綁定在一起,並遷移至帳戶服務。
排定遷移服務的優先順序
要開始分離服務,理想的做法是找出單體應用程式中鬆散耦合的模組。您可以選擇鬆散耦合的模組,做為要轉換為微服務的首要候選項目。如要完成各模組的相依性分析,請參考下列資訊:
- 依附元件的類型:與資料或其他模組相依。
- 依附元件的規模:所識別模組的變更可能對其他模組造成的影響。
遷移一個與資料高度相依的模組通常不是件容易的事。如果您先遷移功能,稍後再遷移相關資料,可能會暫時從多個資料庫讀取資料,並將資料寫入這些資料庫。因此,您必須考量資料完整性和同步處理的挑戰。
建議您擷取與其他單體不同的資源需求模組。舉例來說,如果模組有記憶體內資料庫,您可以將其轉換為服務,然後在記憶體較大的主機上部署。將具有特定資源需求的模組轉換為服務,即可讓應用程式更容易進行調整。
從作業角度來看,將模組重構為專屬服務,也意味著要調整現有的團隊結構。如要明確劃分責任,最好的做法就是讓負責整個服務的小型團隊擁有自主權。
影響服務遷移優先順序的其他因素包括:業務重要性、全面測試涵蓋率、應用程式的安全性態勢,以及組織的支持程度。根據評估結果,您可以按照重構帶來的好處,按照本系列第一份文件所述對服務進行排名。
從單體式應用程式中擷取服務
找出理想的候選服務後,您必須找出微服務和單體模組共存的方式。管理這種共存情形的其中一種方法,是引入處理序間通訊 (IPC) 轉接器,這有助於模組協同運作。隨著時間的推移,微服務會承接負載並淘汰單體元件。這個漸進式程序可讓您逐步偵測錯誤或效能問題,進而降低從單體式應用程式遷移至新微服務的風險。
下圖顯示如何實作 IPC 方法:
圖 2:IPC 轉接器會協調單體應用程式與微服務模組之間的通訊。
在圖 2 中,模組 Z 是您要從單體式應用程式中擷取的候選服務。模組 X 和 Y 依附於模組 Z。微服務模組 X 和 Y 會使用單體應用程式中的 IPC 轉接程式,透過 REST API 與模組 Z 通訊。
本系列的下一篇文件「在微服務設定中進行跨服務通訊」將說明 Strangler Fig 模式,以及如何從單體中解構服務。
管理單體資料庫
一般來說,單體式應用程式都有各自的單體式資料庫。微服務架構的原則之一,就是為每個微服務建立一個資料庫。因此,當您將單體式應用程式翻新為微服務時,必須根據您識別的服務邊界來分割單體式資料庫。
如要決定要將單一資料庫分割至何處,請先分析資料庫對應項目。在服務擷取分析中,您收集了一些關於需要建立的微服務的洞察資料。您可以使用相同的方法分析資料庫用量,並將資料表或其他資料庫物件對應至新的微服務。SchemaCrawler、SchemaSpy 和 ERBuilder 等工具可協助您執行這類分析。對應資料表和其他物件有助於您瞭解資料庫物件之間的耦合關係,這些物件橫跨潛在的微服務界線。
不過,分割單一資料庫的作業相當複雜,因為資料庫物件之間可能沒有明確的區隔。您也需要考量其他問題,例如資料同步、交易完整性、彙整和延遲。下一節將說明在分割單體資料庫時,可協助您回應這些問題的模式。
參考表
在單體應用程式中,模組通常會透過 SQL 與其他模組的資料表彙整,存取其他模組中所需的資料。下圖使用先前的電子商務應用程式範例,說明 SQL 彙整存取權的程序:
圖 3. 模組會將資料彙整至其他模組的資料表。
在圖 3 中,訂單模組會使用 product_id
外部鍵,將訂單與產品表格彙整。
不過,如果您將模組解構為個別服務,建議您不要讓訂單服務直接呼叫產品服務的資料庫,以便執行彙整作業。以下各節將說明可用來區隔資料庫物件的選項。
透過 API 共用資料
將核心功能或模組分割為微服務時,通常會使用 API 分享及提供資料。參照的服務會將資料公開為呼叫服務所需的 API,如下圖所示:
圖 4:服務會使用 API 呼叫,從其他服務取得資料。
在圖 4 中,訂單模組使用 API 呼叫,從產品模組取得資料。由於額外的網路和資料庫呼叫,這個實作方式有明顯的效能問題。不過,如果資料大小有限,透過 API 分享資料會是個不錯的選擇。此外,如果所呼叫的服務傳回的資料變更率已知,您可以在呼叫端實作本機 TTL 快取,以減少對所呼叫服務的網路要求。
複製資料
在兩個獨立的微服務之間共用資料的另一種方式,是複製依附服務資料庫中的資料。資料複製作業為唯讀,且可隨時重建。這個模式可讓服務更具凝聚力。下圖顯示兩個微服務之間的資料複製方式:
圖 5. 服務中的資料會複製到依附服務資料庫。
在圖 5 中,產品服務資料庫會複製到訂單服務資料庫。這項實作可讓訂單服務取得產品資料,而無須重複呼叫產品服務。
如要建構資料複製作業,您可以使用物化檢視表、變更資料擷取 (CDC) 和事件通知等技術。複製的資料最終會一致,但複製資料可能會延遲,因此可能會提供過時的資料。
將靜態資料做為設定
靜態資料 (例如國家/地區代碼和支援的貨幣) 變動緩慢。您可以在微服務中,將這類靜態資料做為設定資料插入。現代的微服務和雲端架構提供功能,可使用設定伺服器、鍵/值儲存空間和金庫管理這類設定資料。您可以以宣告方式加入這些功能。
共用的可變動資料
單體應用程式有一個常見的模式,稱為共用可變動狀態。在共用可變狀態設定中,多個模組會使用單一資料表,如下圖所示:
圖 6:多個模組使用單一資料表。
在圖 6 中,電子商務應用程式的訂單、付款和運送功能使用相同的 ShoppingStatus
資料表,以便在購物歷程期間維護客戶的訂單狀態。
如要遷移共用的可變動狀態單體,您可以開發單獨的 ShoppingStatus 微服務來管理 ShoppingStatus
資料庫資料表。這個微服務會公開 API,用於管理消費者的購物狀態,如下圖所示:
圖 7. 微服務會將 API 公開給多個其他服務。
在圖 7 中,付款、訂單和運送微服務使用 ShoppingStatus 微服務 API。如果資料庫資料表與其中一個服務密切相關,建議您將資料移至該服務。接著,您可以透過 API 公開資料,供其他服務使用。這項實作可確保您不會有太多彼此頻繁呼叫的精細服務。如果您將服務分割錯誤,就必須重新定義服務範圍。
分散式交易
將服務與單體式系統分離後,原始單體式系統中的本機交易可能會分散至多個服務。跨多項服務的交易會視為分散式交易。在單體應用程式中,資料庫系統會確保交易是原子性的。如要處理以微服務為基礎的系統中各項服務之間的交易,您必須建立全域交易協調器。交易協調器會處理回溯、補償動作和其他交易,詳情請參閱本系列的下一篇文件「微服務設定中的服務間通訊」。
資料一致性
分散式交易會帶來挑戰,因為需要在各項服務之間維持資料一致性。所有更新都必須以原子方式完成。在單體應用程式中,交易的屬性可確保查詢會根據隔離層級傳回資料庫的一致性檢視畫面。
相反地,請考慮在以微服務為基礎的架構中進行多步驟交易。如果任何一項服務交易失敗,則必須透過回溯其他服務中成功的步驟,來協調資料。否則,應用程式資料的全球檢視畫面會因服務而異。
要判斷實作最終一致性的步驟何時失敗,可能相當困難。舉例來說,步驟可能不會立即失敗,但可能會遭到封鎖或逾時。因此,您可能需要實作某種逾時機制。如果在被叫用的服務存取時,重複的資料已過時,則在服務之間快取或複製資料以減少網路延遲,也可能導致資料不一致。
本系列的下一篇文件「微服務設定中的跨服務通訊」將提供處理跨微服務分散式交易的模式範例。
設計服務間通訊
在單體式應用程式中,元件 (或應用程式模組) 會直接透過函式呼叫來叫用彼此。相較之下,以微服務為基礎的應用程式則由多項服務組成,這些服務會透過網路彼此互動。
設計服務間通訊時,請先思考服務應如何互動。服務互動可以是下列任一項目:
- 一對一互動:每個用戶端要求都會由一項服務處理。
- 一對多互動:每項要求都由多項服務處理。
另外,請考慮互動是否為同步或非同步:
- 同步:用戶端會預期服務即時回應,並可能在等待期間阻斷。
- 非同步:在等待回應時,用戶端不會阻斷。回應 (如果有) 不一定會立即傳送。
下表列出互動樣式的組合:
一對一 | 一對多 | |
---|---|---|
同步 | 要求和回應:向服務發出要求,並等待回應。 | — |
非同步 | 通知:向服務傳送要求,但不會傳送或預期收到回覆。 | 發布和訂閱:用戶端發布通知訊息,而零或多個感興趣的服務會使用該訊息。 |
要求和非同步回應:向服務傳送要求,服務會以非同步方式回應。客戶不會封鎖。 | 發布和非同步回應:用戶端會發布要求,並等待感興趣的服務提供回應。 |
每項服務通常會同時採用這兩種互動類型。
實作服務間通訊
如要實作服務間通訊,您可以選擇不同的 IPC 技術。舉例來說,服務可以使用同步的以要求/回應為基礎的通訊機制,例如以 HTTP 為基礎的 REST、gRPC 或 Thrift。或者,服務可以使用非同步的訊息式通訊機制,例如 AMQP 或 STOMP。您也可以選擇各種訊息格式。舉例來說,服務可以使用人類可讀的文字型格式,例如 JSON 或 XML。或者,服務可以使用 Avro 或 Protocol Buffers 等二進位格式。
設定服務直接呼叫其他服務,會導致服務之間的耦合度過高。建議您改用訊息或事件式通訊:
- 訊息傳遞:實作訊息傳遞時,服務不必直接互相呼叫。相反地,所有服務都會知道訊息媒介,並將訊息推送至該媒介。訊息媒介程式會將這些訊息儲存在訊息佇列中。其他服務可以訂閱自己有興趣的訊息。
- 事件為基礎的通訊:實作事件驅動處理作業時,服務之間的通訊會透過個別服務產生的事件進行。個別服務會將事件寫入訊息仲介。服務可以監聽感興趣的事件。由於事件不包含酬載,因此這個模式可讓服務保持鬆散連結。
在微服務應用程式中,我們建議使用非同步服務間通訊,而非同步通訊。要求-回應是眾所皆知的架構模式,因此設計同步 API 可能比設計非同步系統更自然。您可以使用訊息傳遞或事件驅動通訊,實作服務之間的非同步通訊。使用非同步通訊有下列優點:
- 鬆散連結:非同步模型會將要求/回應互動分成兩個個別訊息,一個用於要求,另一個用於回應。服務的使用者會啟動要求訊息並等待回應,而服務供應器會等待要求訊息,並以回應訊息回覆。這項設定表示呼叫端不必等待回應訊息。
- 失敗隔離:即使下游消費者失敗,傳送者仍可繼續傳送訊息。取用端會在復原時接收待處理工作。這項功能在微服務架構中特別實用,因為每項服務都有其生命週期。不過,非同步 API 需要下游服務可供使用,否則作業會失敗。
- 回應速度:如果上游服務不需要等待下游服務,就能更快回覆。如果有一系列的服務依附元件 (服務 A 呼叫 B,而 B 呼叫 C 等等),等待同步呼叫可能會增加無法接受的延遲時間。
- 流程控制:訊息佇列會充當緩衝區,讓接收端以自己的速度處理訊息。
不過,以下是使用非同步訊息的幾項挑戰:
- 延遲:如果訊息中介服務成為瓶頸,端對端延遲時間可能會變長。
- 開發和測試的額外負擔:根據所選的訊息或事件基礎架構,可能會出現重複的訊息,導致難以讓作業具備冪等性。使用非同步訊息,也可能難以實作及測試要求-回應語意。您需要一種方法來連結要求和回應訊息。
- 傳輸量:使用中央佇列或其他機制的非同步訊息處理作業,可能會成為系統的瓶頸。後端系統 (例如佇列和下游消費者) 應配合系統的吞吐量需求進行調整。
- 錯誤處理變得複雜:在非同步系統中,呼叫端不知道要求是否成功或失敗,因此需要在頻外處理錯誤。這類系統可能會導致難以實作重試或指數輪詢等邏輯。如果有多個鏈結的非同步呼叫,且必須全部成功或失敗,則錯誤處理會變得更加複雜。
本系列的下一篇文件「微服務設定中的服務間通訊」會提供參考實作項目,以解決上述清單中提到的部分挑戰。
後續步驟
- 請參閱本系列的第一份文件,瞭解微服務、其優點、挑戰和用途。
- 請參閱本系列的下一篇文件「微服務設定中的服務間通訊」。
- 請參閱本系列的第四份也是最後一份文件,進一步瞭解微服務之間的請求分散追蹤。