微服務架構
作者:DevOps亮哥
來自:DevOps探路者
一、關鍵點:
對于面向對象的設計,我們遵循SOLID原則。對于微服務設計,我們建議開發人員遵循IDEALS原則:接口分離(Interface segregation),可部署性(deployability),事件驅動(event-driven),可用性勝于一致性(Availability over Consistency),松耦合(Loose coupling)和單一責任(single responsibility)。
- 接口分離:指的是不同類型的客戶端(移動應用程序,web應用程序,CLI程序)應該能夠通過適合其需求的協議與服務端交互。
- 可部署性:指的是在微服務時代,也就是DevOps時代,開發人員需要在打包、部署和運行微服務方面做出關鍵的設計決策和技術選擇。
- 事件驅動:指的是在任何時候,都應該對服務進行建模,通過異步消息或事件而不是同步調用。
- 可用性勝于一致性:指的是最終用戶更看重系統的可用性而不是強一致性,它們對最終一致性也很滿意。
- 松耦合:仍然是一個重要的設計問題,涉及傳入和傳出的耦合。
- 單一責任:是一種思想,它支持對不太大或太細的微服務進行建模,因為他們包含了適當數量的內聚功能。
在2000年,Robert C. Martin編寫了面向對象設計的五個原則。Michael Feathers后來將這五個原則的首字母組成了縮略詞,也就是SOLID。從那時起,用于OO設計的SOLID原則就在業界被廣為人知。這五個原則是:
- 單一責任原則(Single responsibility principle)
- 開閉原則(Open/closed principle)
- 里氏代換原則(Liskov substitution principle)
- 界面分離原則(Interface segregation principle)
- 依賴倒置原則(Dependency inversion principle)
幾年前,我在給其他人講授微服務設計時,一個學生問:“SOLID原則適用于微服務嗎”,經過一番思考,我的回答是“部分”。后面的幾個月,我發現我一直在尋找微服務的基本設計原則(包含一些縮略詞)。為什么這樣的問題會如此重要呢?
作為一個行業,我們已經設計和實施基于微服務的解決方案已經超過六年了。在此期間,越來越多的工具、框架、平臺和支撐產品圍繞微服務建立了一個極其豐富的技術環境。在這種情況下,一個新手微服務開發人員在面對一個微服務項目中許多設計決策和技術選擇時會感到迷惑。在這個領域,一組核心原則可以幫助開發人員更好的理解基于微服務的解決方案。
雖然有些SOLID原則適用于微服務,但面向對象是一種設計范式,它處理的元素有類、接口和繼承等,與一般分布式系統中的元素(特別是微服務)有著根本的區別。
因此,我們提出以下一套微服務設計的核心原則:
- 接口分離(Interface segregation)
- 可部署性(Deployability (is on you))
- 事件驅動(Event-driven)
- 可用性勝于一致性(Availability over consistency)
- 松耦合(Loose coupling)
- 單一職責(Single responsibility)
這些原則并沒有覆蓋基于微服務的解決方案的所有設計決策,但他們涉及到創建現代基于服務的系統的關鍵關注點和成功因素。下面對微服務的“IDEALS”的原則進行詳細的解釋。
二、接口分離
最初的接口分離原則是指防止面向對象中類使用“胖”接口。換句話說,就是每種類型的客戶端應該有單獨的接口,而不是提供一個滿足所有客戶端需要的所有可能方法的接口。
微服務體系結構風格是面向服務體系結構的一種特殊化,其中接口(即服務契約)的設計一直是最重要的。從21世紀初開始,SOA文檔就規定了所有客戶端都應該遵守的規范模型和規范模式。然而,自從SOA的舊時代以來,我們處理服務契約設計的方式發生了改變。在微服務時代,同一個服務邏輯通常有許多客戶端程序。這就是將接口分離應用于微服務的主要目的。
實現微服務的接口分離
微服務接口分離的目標是每種類型的前端都能看到最適合其需求的服務契約。例如,一個移動應用程序系統調用端點,這些端點返回簡短的JSON格式數據作為響應。當另一個web應用程序需要返回完整的JSON格式數據作為響應。與此同時,還有一個桌面應用程序調用同一個服務,但需要返回完整的XML格式數據。不同的客戶端也可能使用不同的協議。例如,外部的客戶端希望使用HTTP來調用gRPC服務。
我們沒有試圖在所有類型的服務客戶機上強加相同的服務契約(使用規范模型),而是通過“分離接口”,以便每種類型的客戶機都能看到它需要的服務接口。我們怎么做?一個突出的選擇是使用API網關,它可以進行消息格式轉換(message format transformation),消息結構轉換(message structure transformation),協議橋接(protocol bridging),消息路由(message routing)等。一個流行的替代方案是BFF(Backend for Frontends)模式。在這種情況下,我們為每種不同類型的客戶機提供了一個API網關,也就是通常說的,為每種客戶機提供不同的BFF,如下圖所示:
三、可部署性
幾乎在整個軟件歷史上,設計工作都集中在與實現單元(模塊)如何組織以及運行時元素如何交互相關的設計決策上。架構策略、設計模式和其他設計策略為在層中組織軟件元素、避免過度依賴、為特定類型的組件分配特定的角色或關注點以及軟件空間中的其他設計決策提供了指導。對于微服務開發人員來說,有一些關鍵的設計決策超出了軟件元素。
作為開發人員,我們早就意識到將軟件正確打包并部署到適當的運行時環境中的重要性。然而,我們從沒有像今天的微服務那樣關注部署和運行時監控。在這里稱為“可部署性”的技術和設計決策領域已經成為微服務成功的關鍵。主要原因是一個簡單的事實,即微服務顯著增加了部署單元的數量。
因此,IDEALS中的D代表微服務開發者有責任確保軟件及其新版本隨時可以部署到環境中供用戶使用。總之,可部署性包括:
- 配置運行時基礎設施,包括容器、pods、集群、持久性、安全性和網絡。
- 微服務的擴縮容,或者將他們從一個運行時環境遷移到另一個運行時環境。
- 加速提交+構建+測試+部署過程
- 減少版本升級時的停機時間
- 同步相關軟件的版本更改
- 監控微服務運行狀況,以快速識別和修復故障。
實現良好的可部署性
自動化是實現高效部署的關鍵。自動化包括明智的使用工具和技術,這是自微服務出現以來不斷看到最大變化的領域。因此,微服務開發人員應該在工具和平臺方面尋找新的方向。但總是質疑每一個新選擇的好處和挑戰。(這里可參考Thoughtworks技術雷達和軟件架構與設計趨勢報告)
以下是開發人員在任何基于微服務的解決方案中為提高可部署性而應該考慮的策略和技術列表:
- 容器化和容器編排:容器化的微服務更容易實現跨平臺和云提供商進行復制和部署,而編排平臺為路由、擴展、復制、負載均衡等提供了共享資源和機制。Docker和Kubernetes是當今容器和容器編排的事實標準。
- 服務網格:這種工具可以用于流量監控,策略執行,身份驗證,RBAC,路由,斷路器、消息轉換等,以幫助容器編排平臺中的服務進行通信。流行的服務網格包括Istio、Linkerd和Consul Connect。
- API網關:通過攔截對微服務的調用,API網關產品提供了豐富的功能集,包括消息轉換和協議橋接、流量監控、安全控制、路由、緩存、請求限制和API配額以及熔斷。這一領域的主要參與者是Ambassador、Kong、Apiman、WSO2 API Manager、Apigee和Amazon API Gateway。
- 無服務器架構:通過將服務部署到遵循FaaS范式的無服務器平臺,可以避免容器編排的復雜性和操作成本。AWS Lambda、Azure函數和google云函數都是無服務器平臺的示例.
- 日志整合工具:微服務可以輕松的將部署單元的數量增加一個數量級。我們需要工具來整合這些組件的日志輸出,以及搜索、分析和生成告警的能力。這個領域流行的工具有Fluentd、Graylog、Splunk和ELK(Elasticsearch、Logstash、Kibana)。
- 鏈路跟蹤工具:這些工具可用于檢測您的微服務,然后生成、收集和可視化運行時跟蹤數據,以顯示跨服務的調用。它可以幫助您發現性能問題。跟蹤工具的例子有Zipkin, Jaeger, and AWS X-Ray,OpenTraceing。
- DevOps:當開發人員和運維人員可以進行更緊密的溝通和協作時,微服務的工作會更加容易,從基礎設施配置到事件處理。
- 藍綠部署和金絲雀發布:這些部署策略允許在發布新版本的微服務時實現零或接近零的停機時間,并在出現問題時進行快速切換。
- 基礎設施即代碼:這種做法使得構建-部署周期中的交互更少,從而變得更快,更不容易出錯,更易于審計。
- 持續交付:這是縮短從提交到部署間隔并保持代碼質量的必要實踐。傳統的CICD工具有Jenkins、Gitlab CI/CD、Bamboo、GoCD、CircleCI和Spinnaker。最近,Weaveworks和Flux等GitOps工具被添加到這個領域,將CD和IaC結合起來。
- 配置管理:將配置屬性存儲在微服務部署單元之外,并且易于管理。
四、事件驅動
微服務架構風格用于創建后端服務,這些服務通常使用以下三種類型的方式進行調用:
- HTTP調用(REST服務)
- 使用特定于平臺的組件技術進行類RPC調用,如gRPC或GraphQL
- 通過消息中間件處理異步消息
前兩個通常是同步的,HTTP調用也是最常見的方式。通常,服務需要調用其他服務進行組合,太多時候,組合中的服務調用是同步的。如果異步,需要連接和接收Queue/Topic里的消息,那么我們將創建一個事件驅動的體系結構。(我們可以討論消息驅動和事件驅動的區別,但都可以表示網絡上的異步通信,使用消息中間件產品(Apache Kafka、RabbitMQ和Amazon SNS)提供的Queue和Topic)
事件驅動體系結構的一個重要好處是提高了可伸縮性和吞吐量。這是因為:消息發送者在等待響應時不會被阻塞,并且同一個消息/事件可以由多個接收者以發布-訂閱的方式并行使用。
事件驅動的微服務
IDEALS中的E就表示使用事件驅動對微服務進行建模。因為他們更能滿足當今軟件解決方案的可伸縮性和性能要求,這種設計還促進了松耦合,因為消息發送方和接收方——微服務——是對立的,彼此不了解。可靠性也得到了提升,因為這個設計可以處理微服務的臨時中斷,當微服務恢復后可以處理排隊中的消息。
但事件驅動的微服務,也稱為反應式微服務,也會帶來挑戰,比如異步處理和并行執行,可能需要同步點和相關標識符。設計需要考慮錯誤和丟失的消息——校正事件和撤銷數據更改的機制(如Saga模式)通常是必須的。對于事件驅動體系結構帶來的面向用戶的事務,應仔細考慮用戶體驗,以使最終用戶了解進度和事故。
五、可用性勝于一致性
CAP理論本質上給了我們兩個選擇:可用性或者一致性。我們看到業界為了讓我們選擇可用性而付出了巨大努力,從而最終實現一致性。原因很簡單:今天的最終用戶不會容忍服務不可用。假如一個網絡商店,如果我們在瀏覽產品時顯示的庫存量和購買時更新的實際庫存量之間強制執行強一致,那么數據變更將會帶來巨大的開銷,如何任何更新庫存的服務暫時無法訪問,那么頁面無法顯示庫存信息,結賬將停止服務。相反,如果選擇可用性,用戶瀏覽產品時顯示的庫存量和購買時更新的實際庫存量之間會有偶爾的不一致。當用戶在下單買時,然后再去查詢真實的庫存量,如果沒有庫存,再提示用戶沒有庫存。從用戶的角度來看,這個場景比由于系統要實現強一致而讓整個系統不可用或超級慢對所有用戶來說要好很多。
有些業務操作確實需要很強的一致性。然而,正如Pat Helland指出,當你面對你是想要正確?還是想要現在?的問題時,人們通常想要的是現在而不是正確的答案時,就需要考慮強一致。
最終一致性的可用性
對于微服務來說,保證可用性選擇的主要策略是數據復制。可以采用不同的設計模式,有時可以組合使用。
- 服務數據復制模式:當微服務需要訪問屬于其他應用程序的數據(而API調用不適合獲取數據)時,使用此基本模式。我們創建該數據的副本,并使其隨時可供微服務使用。該解決方案還需要一種數據同步機制(如ETL工具/程序、發布-訂閱消息傳遞、物化視圖),該機制將定期或基于觸發器使副本與主數據庫保持一致。
- 命令查詢責任分離(CQRS)模式:這里我們將更改數據(Command)的操作設計與實現和只讀數據(Query)的操作分開。CQRS通常建立在服務數據復制的基礎上,用于提高查詢的效率。
- 事件源(Event Source)模式:我們不在數據庫中存儲對象的當前狀態,而是存儲影響該對象的僅附加的、不可變的事件序列。當前狀態是通過回放事件獲得,這樣做是為了提供數據的“查詢視圖”。因此,事件源通常建立在CQRS設計的基礎上。
我們經常使用的CQRS模式通常如下圖所示:一個可以更改數據的HTTP請求由后臺一個REST服務處理,該服務可以操作一個集中式的Oracle數據庫。其他只讀的HTTP請求轉到另一個后臺服務,該服務可以從基于文本的Elasticsearch數據存儲中獲取數據。一個Spring Batch Kubernetes cron任務定期將在Oracle數據庫中的變更同步到ES中,這個設計使用兩個數據存儲之間的最終一致性。即使Oracle DB和cron任務不起作用,查詢服務也是可用的。
六、松耦合
在軟件工程中,耦合是指兩個軟件元素之間相互依賴的程度。對于基于服務的系統來說,傳入耦合是指服務用戶如何與服務交互。我們知道這種交互應該通過服務契約來實現,并且該服務契約不應該與實現細節和特定技術緊密結合。服務是可以由不同程序調用的分布式組件。有時候,服務提供方不知道所有服務用戶在哪里(公共API服務通常就是這樣)。因此,服務契約應該避免變更。如何服務契約與服務邏輯或技術緊密耦合,那么當邏輯或技術需要演化時,它也需要同時發生變化。
服務通常需要與其他服務或其他類型的組件交互,從而產生傳出耦合。這種交互建立了直接影響服務自治的運行時依賴關系。如果一個服務的自治性較低,它的行為就不那么可預測:最好的情況就是,該服務將與他需要調用的最慢的,最不可靠的和最不可用的組件一樣快速、可靠和可用。
微服務的松耦合策略
IDEALS中的L表示要關注服務及微服務的耦合。可以使用并組合多種策略來改進傳入和傳出的松散耦合。這些策略包括:
- 點對點和發布-訂閱(Point-to-point and Publish-subscribe):這種通過消息傳遞的模式改進了耦合性,因為發送方和接收方彼此不知道對方。響應式微服務(如Kafka消費方)的契約將成為消息隊列的名稱和消息的結構體。
- API網關和BFF:這些解決方案規定了一個中間組件,該組件處理服務契約與客戶端需要的消息格式和協議之間的差異,從而有助于分離他們。
- 契約優先設計(Contract-first design):通過設計與任何現有代碼相關的契約,從而避免創建與技術和實現緊密耦合的api。
- 超媒體(Hypermedia):對于REST服務,超媒體幫助前端更加獨立于服務端點。
- Facade和Adapter/WrApper模式:這些GoF模式的變體在微服務架構中可以規定內部的組件和服務,可以防止在微服務實現中傳播不良的耦合性。
- 每個微服務一個數據庫模式:該模式使得微服務不僅獲得了自治性,而且避免了與數據庫共享帶來的直接耦合。
七、單一職責
最初的單一職責原則( Single Responsibility Principle,SRP)是關于在OO類中具有內聚功能。在一個類中擁有多個職責自然會導致緊耦合,并導致脆弱的設計,在變更時會發生意想不到的結果。SRP這個想法很簡單,也很容易理解,但要用好并不容易。
單一職責的概念可以擴展到微服務中服務的內聚性。微服務體系結構風格部署單元應該包含一個服務或幾個內聚服務。如果一個微服務包含太多的職責,也就是說,有太多不太具有內聚力的服務,那么它就可能會承受一個巨大的痛苦。膨脹的微服務在功能和技術棧方面變得更難發展。而且,持續交付將會變得繁重,因為他們將會使許多開發人員在同一個部署單元開發不同的內容。
另一方面,如果微服務過于細粒度,則其中的幾個服務可能需要交互來滿足用戶請求。在最壞的情況下,數據變更可能會跨多個微服務,可能會產生分布式事務的場景。
粒度適中的微服務
微服務設計成熟度的一個重要方面是創建粒度適中的微服務的能力。這里的解決方案不是任何工具或技術中,而是在適當的領域建模上。為后端服務建模并為其定義微服務邊界可以通過多種方式完成。業界流行的一種驅動微服務范圍的方法是遵循領域驅動設計(Domain-Driven Design,DDD)原則。簡而言之:
一個服務(例如:REST服務)可以具體DDD聚合的作用域。一個微服務的作用域可以是DDD限定的上下文。該微服務中的服務將對應于該限定上下文的聚合。
對于微服務間的通信,我們可以使用:當異步消息滿足需求時使用領域事件(Domain Event);當請求-響應更適合時,使用某種形式的中間層進行API調用;當一個微服務需要另一個可用區的大量數據時,可以使用數據復制保證最終一致性。
八、結論
IDEALS是在大多數典型的微服務設計中要遵循的核心設計原則。然而,遵循IDEALS并不是使我們的微服務設計成功的良藥。通常,我們還需要對質量需求有一個很好的理解,并使設計決策意識到他們的權衡。此外,我們還應該學習可以用來幫助實現設計原則的設計模式和架構策略。還應該掌握可用的技術選型。
多年來,我一直使用IDEALS設計、實現和部署微服務,在設計研討會和講座中,我與來自不同組織的數百名軟件開發人員討論這些核心原則以及每一個原則背后的許多策略。有時候,會讓人覺得微服務的工具、框架、平臺和模式層出不窮,我相信,對微服務IDEALS更好的理解,能夠幫助我們更清晰的了解技術領域。
翻譯自:
https://www.infoq.com/articles/microservices-design-ideals/?itm_source=infoq&itm_campaign=user_page&itm_medium=link