微服務架構基本原理學習筆記(二)

上一篇:微服務架構基本原理學習筆記(一

三、微服務架構

  從一個已有的單體架構的應用程序開始進行微服務架構的重構往往是一個不錯的選擇。隨着業務量和功能的增加,我們可以考慮使用微服務架構來擴充應用程序中原有的功能,或者每次添加新功能時,都爲其創建一個新的微服務。這比從一開始就選擇使用微服務架構進行設計要相對容易一些,因爲微服務架構的好處通常不會體現在小型項目中。所以,考慮讓項目持續迭代一段時間,直到我們能夠非常清晰地確定服務的邊界,通過微服務架構來進行功能的劃分。

  因此,對於每一個微服務,我們都需要明確它們各自的職責,並定義公共接口。

每個微服務管理各自的數據

  前面我們已經介紹過,微服務是自治的並且可以單獨運行和部署,而實現這一特性的關鍵就是確保每個微服務都擁有自己的數據存儲。也就是說,微服務架構中不允許出現多個微服務共享同一個數據的情況。任何想要訪問其它微服務中數據的情況都應該通過公共接口來完成。

  你可能在想,如果沒有一箇中心的共享數據庫,如果保證數據的一致性呢?尤其是對於事務而言,數據的一致性至關重要。在微服務架構中,我們不能在單個數據庫事務中去更新來自不同的微服務中的數據,我們要麼通過較爲複雜的分佈式事務來實現這一功能,要麼通過微服務自己來保證數據的一致性。而後者是通常較爲推薦的做法。這就意味着,我們無法保證在一個相對較短的時間內數據的一致性,而是需要等待一段時間之後才能獲得整體狀態一致的數據。具體的操作是,當其中一個微服務的數據更改時,在一個短暫的時間內其它微服務中的數據無法與其保持一致,這個時候,你需要有一種機制能夠應對這種暫時性的數據非一致性,直到所有微服務中的數據最終獲得一致性。在微服務中使用緩存機制是一個不錯的選擇,這可以大大減少從多個微服務聚合數據以執行單個操作的成本。

  查看Github上的eShopOnContainers的微服務架構圖,我們可以看到其中不同的微服務都有各自的數據存儲。例如Identity微服務使用了SQL Server數據,Ordering微服務也使用了SQL Server數據庫,但它和Identity微服務使用的不是同一個數據庫,它們之間也不能相互訪問。Basket微服務使用的則是完全不同的Redis緩存數據庫。

  現在考慮一個實際場景,Ordering微服務中有一個OrderItem實例,它代表用戶的一個訂單,其中包括ProductId、ProductName和UnitPrice。Catalog微服務中有一個CatalogItem實例,它代表商品信息,其中包括ID、Name和Price。當用戶購買商品時,Ordering微服務中不僅會記錄商品的ID,同時還會將商品的名稱和價格複製到自己的數據庫中。那麼,如果將來Catalog微服務中的商品信息發生了變化,這個數據並不一定會及時地同步到Ordering微服務中。那麼,如果我們只在Ordering微服務中存儲商品ID,而當需要的時候通過API去請求Catalog微服務以獲取商品的詳細信息豈不是更好?不一定!

  Ordering微服務中商品數據的冗餘反映的是用戶購買商品時的情形,它與存儲在Catalog微服務中商品現在的信息並非一致。Ordering微服務並不關心商品當前的價格和名稱,它關心的是用戶下單時商品的價格和名稱。所以事實上,這不是真正的數據冗餘,這些數據在不同的微服務上下文中具備不同的含義。微服務之間的這種數據冗餘以及不一致性不僅不會導致系統出現問題,相反,它是由不同的業務需求所決定的。

微服務的組成部分

  微服務不一定是在單個服務器或虛擬機上運行的單個進程,它通常至少有兩部分組成:一個由代碼構成的WebAPI,而另一個則是數據庫。所以這至少是兩個不同的進程,而且它們通常並不在同一臺服務器或虛擬機中運行。進一步地,如果我們對服務進行橫向擴展,並同時對數據庫進行分片配置,那麼一個微服務甚至會在好幾個不同的服務器或虛擬機中運行,而且它可能還需要某種定時任務來執行數據維護,並監聽和觸發各種不同的消息。因此,實際上一個微服務可能會涉及到多個不同的服務器和進程,所有的這些部分一起構成了一個微服務。

  我們對比看一下eShopOnContainers的微服務架構圖,其中的Ordering微服務包含一個名爲Ordering.API的WebAPI,另外還有一個名爲Ordering.BackgroundTasks的後臺任務,這是兩個獨立運行的進程。我們不必讓微服務的所有代碼都在一個進程中運行。

  從概念上講,每個微服務及其公共接口都有明確的邊界定義,數據只能通過這些公共接口訪問。

每個微服務都是可獨立部署的

  微服務可獨立部署和存在,這就意味着依賴於該微服務的客戶端程序不需要同時升級。要做到這一點,你必須確保微服務的公共接口始終保持對舊客戶端程序的兼容。實際上,我們通常把這些公共接口稱之爲微服務與客戶端之間的協議,協議是不能單方面更改的。你可能會問,隨着業務和需求的增長,這些公共接口怎麼能永遠保持不變呢?最簡單的解決辦法是永遠只對公共接口進行增量修改,例如增加新的接口,或在已有的接口上對數據增加新的屬性。

  如果不可避免地要對公共接口進行重大調整,可以考慮下面兩種實踐方式:

  一是讓開發客戶端的團隊等待新的微服務上線之後再更新客戶端程序,以確保對微服務的調整不會影響到舊客戶端程序的運行。

  另外一個推薦的做法是針對不同版本的客戶端程序創建自動化測試,並將其加入到持續集成構建中,這樣每次部署之前,如果有自動化測試未完成,構建就會失敗,我們就可以查找原因並分析是否存在兼容性問題。

  需要注意的一點是,有一種模式可以引入客戶端和微服務都使用的共享代碼,從而可以非常方便地生成一個客戶端包來簡化微服務的調用。我們應該儘量避免使用這種方式,不要將微服務的開發和測試與客戶端緊密耦合在一起,因爲這會導致客戶端對微服務的強依賴而迫使它們同時升級。

如何確定微服務的邊界

  如何確定微服務的邊界是一項比較困難的事情,因爲錯誤的微服務邊界定義會導致後期系統性能的下降,並且一旦這些微服務部署到了生產環境,後期再做調整就比較困難了。因此,在項目開始之前,值得我們花一些時間來認真考慮如何確定微服務的邊界。

  我們前面也介紹過,從已有的單體架構的應用程序開始進行微服務架構設計會使任務變得相對容易一些。你會發現其中有一些模塊本身就與應用程序的其它部分鬆散耦合,它們之間通過比較清晰的接口訪問數據,這些模塊比較容易轉換成微服務。

  另外一種方式是從數據庫層面着手,看看從某些概念上是否可以將部分表組合在一起,形成一個相對獨立的部分。因爲每個微服務都擁有自己獨立的數據,所以從數據庫層面抽取相對獨立的部分也是一個好的想法,我們應該儘量避免從多個不同的微服務之間獲取數據。

  微服務始終應該圍繞着業務來進行組織,這方面可以參考領域驅動設計(Domain Driven Design)的概念,它推薦從應用程序的上下文中來確定邊界,併爲其定義模型,這意味着不同的微服務將使用不同的模型,即使它們使用的數據看起來沒什麼區別,但對應的業務場景卻不同。

  在eShopOnContainers微服務架構中,Ordering微服務和Catalog微服務儘管都與商品信息有關,但它們處在不同的上下文中,因此它們實際上具有不同的屬性,可以自由地爲同一條信息使用不同的名稱。

  在確定微服務的邊界時,你可能會遇到一些陷阱,例如對所有數據庫中的業務表進行簡單的包裝並生成對應的CRUD服務,這些服務充其量只能稱之爲數據庫表的實體類,而不能稱之爲微服務,因爲它們只具有添加和更新實體的方法,而並沒有將與實體相關的業務邏輯包含進來。另一個導致微服務邊界模糊的情況是,當多個微服務相互之間存在循環依賴關係時,會導致頻繁的相互通信,我們應該儘量避免這種情況的出現。

  讓我們詳細瞭解一下eShopOnContainers微服務架構中的具體實現,來看看以上這些原則在實際應用中是如何體現的。

Catalog Microservice 存儲商品的詳細信息。
Basket Microservice 跟蹤和存儲客戶的購物籃信息。
Ordering Microservice 處理客戶的訂單信息。
Identity Microservice 用於處理用戶身份認證。
  1. 職責分離,有助於提高彈性。即使Ordering微服務不可用,依然不影響客戶瀏覽商品信息並將其添加到購物車。
  2. 不同的微服務都被設計爲處理不同的數據量和訪問模式。Catalog微服務需要支持靈活的查詢,以滿足客戶根據各種不同的查詢方式來搜索到想要購買的商品。所以,Catalog微服務需要將數據存儲在支持大量豐富查詢的數據庫中以滿足業務的需要,例如這裏選擇了SQL Server。而Basket微服務只需要存儲比較短暫的數據,這些數據甚至都不需要寫入數據庫,所以這裏使用了Redis內存緩存來保存客戶購物車中的數據。Ordering微服務用來處理用戶的訂單信息,因此對數據的可靠性有非常嚴格的要求。另外它還需要處理一些敏感數據,例如客戶的收穫地址和付款信息等,所以對安全性也有非常高的要求。Identity微服務用來處理身份驗證,我們將在後面的微服務安全部分對其進行詳細說明。
  3. 這不是唯一的確定微服務的方式,你可以根據不同的業務需求對其中的功能進行重新組合。就目前來看,這種設計還不錯。

四、構建微服務

  當我們開發微服務應用程序時,我們希望它能運行在不同的環境中,例如開發人員希望在自己工作的電腦上運行和調試代碼;測試人員希望在臨時搭建的測試環境中運行,當然也可能是在雲上運行;當微服務正式發佈之後,它會在我們的生產環境中運行。那麼我們如何託管微服務,使其非常方便地運行在不同的環境中呢?

  下面列出了幾種不同的方式:

  1. 傳統的方式是使用虛擬機。你可以爲每一個微服務選擇一臺虛擬機,當然如果你的微服務數量特別多的話,這麼做成本可能會比較高。你也可以選擇將多個微服務打包到一臺虛擬機上,但是你需要爲其中的每一個微服務安裝不同的框架和依賴包,這會讓初始化工作變得較爲繁瑣。
  2. 第二種方式是選擇PASS(Platform as a Service)平臺。有許多雲提供商都提供了微服務的託管服務,你只需要專注於微服務的具體實現, PASS平臺負責微服務的管理和基礎設施,並可以爲每個內置負載均衡的微服務提供自動擴展和DNS條目,另外還有標準的安全和監控功能等。
  3. 第三種方式是選擇使用容器。這是當下最流行的方式之一。容器可以將應用程序及其所有的依賴項都打包在一起,然後非常方便地在任何容器主機上輕鬆移植並運行。容器主機可以是本地工作的電腦,也可以在雲上,這大大簡化了開發和部署的任務。示例應用程序eShopOnContainers就使用了容器來構建微服務,具體步驟可以查看Github上的文檔。容器可以使構建和運行微服務的過程變得簡單,如果我們單獨爲每一個微服務安裝所有依賴的軟件並配置所有內容,將會是一個非常痛苦的過程,可能需要好幾天的時間,期間也可能會遇到各種各樣的問題,而容器會使這一切變得非常簡單。而且,許多開發工具也允許將調試器附加到容器內運行的代碼,這對開發人員debug微服務也帶來了很多的便利。

如何開始創建一個微服務

  首先,你需要一個用來保存源代碼的倉庫,例如Github。雖然從技術上來說你可以將所有微服務的代碼都保存在同一個源代碼倉庫中,但是如果微服務的數量很多,這會大大增加微服務間的耦合程度,這顯然違背了微服務設計的初衷。

  其次,我們還需要一個能夠自動化持續集成構建微服務的系統,每當我們提交代碼到源代碼管理器的時候,它都會自動構建一個新的微服務版本,同時還會執行自動化測試,如果測試未通過,則新的構建就會失敗,並自動發消息給相應的開發人員。測試是構建微服務的一個非常重要的部分。

測試微服務

  測試是構建微服務中一個非常關鍵的部分。在構建微服務的過程中,有幾種不同類型的測試:

  1. 單元測試。單元測試是針對代碼級別的,通常運行比較快。在單元測試中,我們要儘可能保證高的代碼覆蓋率,尤其是那些針對特定業務的邏輯和模塊。
  2. 集成測試。也叫做服務級別的測試,是針對於單個微服務的測試。通常我們需要將單個微服務部署到服務器中,並進行相應的配置,然後通過調用暴露出來的公共接口來測試微服務的功能是否正常。相對於單元測試而言,集成測試更難編寫,但它們對於微服務的質量和穩定性來說非常有價值,所以值得我們花時間去創建一個測試框架,然後爲每個微服務創建集成測試。這些測試也應該作爲自動化構建微服務的一部分。
  3. 端到端測試。這些測試是模擬生產環境中所有運行的微服務,我們可以通過UI界面來執行一些關鍵的業務操作流程,以保證儘可能多的微服務之間的協同工作是否能否達到預期的目標。這部分的測試在編寫和維護方面更加困難,而且往往很容易出錯,因爲任何一個底層邏輯的改變都有可能導致測試失敗。你也可以嘗試其它類型的測試來驗證儘可能多的功能,但往往端到端的測試在驗證某些關鍵功能方面仍然具有價值,例如在冒煙測試中快速檢測系統關鍵點的功能是否正常。

微服務模板

  在創建微服務時,並不是每次都從零開始。從一個標準模板開始創建微服務可以省去很多工作。你可以根據需要在自己的代碼倉庫中維護微服務模板,也可以在某個特定的微服務的基礎上進行改寫。有許多通用的功能都可以標準化,例如:

  • 日誌(Logging),它將系統中所有微服務的日誌集中管理。
  • 健康檢查(Health Checking),如果每個微服務都可以報告自身的運行狀態,告知是否正在運行,是否可以與依賴的其它微服務進行通信,這將是一個不錯的設計。
  • 配置(Configuration),讓所有微服務都採用統一的方式進行配置不失爲一個好的想法。
  • 身份認證(Authentication),我們可以使用一些標準的身份認證機制,這可以降低開發過程中一些錯誤配置而導致我們的微服務存在安全漏洞。
  • 構建腳本(Build Scripts),使用標準的構建方法可以避免我們少走彎路。例如eShopOnContainers中使用容器,每個微服務都使用一個Docker文件來生成容器鏡像。

  當然,你可以根據需要將某些特定的功能添加到模板中。模板的好處是提供了開箱即用功能(Out of Box),這大大減少了啓動和運行微服務所需的時間,並且還可以確保系統中所有微服務的一致性。不過這種所謂的一致性不應該限制微服務所使用的技術,例如我們不應該限制所有的微服務都使用同一種編程語言,儘管這可以給開發人員在不同的微服務之間工作帶來便利,但是我們不應該限制這種技術自由,微服務的開發團隊可以自由選擇最佳的開發工具。我們的目標是使微服務開發團隊儘可能高效地工作,將時間花在實現微服務的業務需求上,而不是圍繞微服務技術本身。

  每個開發人員都應該具備獨立處理和運行一個微服務的能力,例如在集成測試環境中檢查各個功能點是否正常。而且,每個開發人員也應該能夠在一個完整的系統中測試他們開發的某個功能。開發人員可以在本地環境中運行所有的內容,也可以在雲上直接訪問整個系統。但是無論採用哪種方式,工作流程都應該儘量簡單並可以自動完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章