超值乾貨:微服務架構下如何解耦,對於已經緊耦合下如何重構?

今天準備談下微服務架構下各個微服務間如何解耦,以及對於已經緊耦合的微服務如何進行重構。要明白實際上微服務後續出現的諸多問題往往都是一開始微服務模塊劃分就不合理導致,對於具體的模塊劃分方法和原則,我總結出了以下幾點。

  • 原則1:劃分爲<10個微服務模塊
  • 原則2:強數據關聯模塊不要拆分
  • 原則3:以數據聚合驅動業務功能聚合
  • 原則4:從縱向功能劃分思路到橫向分層思路轉變
  • 原則5:高內聚、松耦合的基礎原則

對於具體的內容在這篇文章不再重複給出。可以看到對於微服務模塊拆分更多的是屬於業務建模和系統分析方面的內容,而今天談的微服務解耦重點是想從可用的技術手段入手來談下可用的以下解耦方法和策略。

問題綜述

最近幾年對於微服務架構,企業中臺構建,組件化和服務化,平臺+應用構建模式,包括Docker容器化,DevOps等都越來越受到傳統企業的關注,也可以看到很多企業傳統架構也在朝這個目標進行演進和轉型。對於微服務架構本身的優點和缺點,包括傳統企業實施微服務架構的演進路線等,在我前面很多微服務架構相關的文章中都有所介紹,今天主要談下在微服務架構下的解耦問題。

要明白,企業實施微服務架構後,原來所有內部的接口調用,內部的完整事務都會變成微服務模塊間的跨域的接口服務調用,傳統事務也變成了分佈式事務,這些本身是增加了系統複雜度。

原來一個系統就能夠完成的事情,現在要依賴底層技術組件服務,其它業務微服務模塊多個Http Rest API接口調用往往才能夠完成。只要其中任何一個API接口出現問題,都會直接影響到前端的業務功能使用。

微服務間的雪崩效應:在採用微服務架構後,各個微服務間存在大量的API接口服務調用,相互之間還形成了服務調用鏈,比如A-》B-》C,那麼如果C服務出現故障就將直接影響到B服務無法正常訪問和服務阻塞,同時B的故障又將進一步傳導到A服務的消費和使用。

對於互聯網企業實施微服務架構,其中有幾個關鍵點。

  • 其一就是微服務架構可以更好的進行平臺的性能擴展和高伸縮要求。
  • 其二就是互聯網應用本身業務規則相對簡單,模塊間容易解耦。
  • 其三就是大的互聯網企業IT技術積累更強,有更好的技術能夠搭建高可用的技術平臺,也有更好的技術能夠實現微服務架構實施後的自動化運維和監控。

而這些往往都是傳統企業在實施微服務架構所欠缺的,比如有些企業一開始實施微服務架構沒發現問題,結果最終上線後卻發現後續的系統運維和性能監控,故障問題分析和排查等能力跟不上,無法及時響應客戶需求並快速的定位和解決問題。

即前面經常說到的,傳統企業的IT治理和團隊技術能力跟不上,直接影響到微服務架構實施成敗。

那麼回到正題,今天希望討論和分析下,企業實施微服務架構後,如何儘量減少微服務模塊的耦合導致的單個微服務模塊功能實現和運行故障,簡單來說就是一個微服務模塊中業務功能的運行,如何做到最小化的依賴外部微服務模塊Http API服務接口的可用性。即使外部模塊掛點,當前模塊也能夠正常使用,或者說能夠不影響到當前模塊核心功能的使用。

對於該問題,我們可以分開從幾個方面進行討論。

同步調用轉爲異步調用

一說到解耦,我們一定會首先想到消息中間件來實現異步,即將同步轉爲異步,通過異步來實現解耦。我們可以先想消息發送給消息中間件,只要消息中間件是高可用性的沒有宕機,整個接口集成過程就是OK的,而消息中間件再異步方式分發消息給目標系統,同時支持重試。

消息中間件的採用

消息中間件(message oriented middleware)是指支持與保障分佈式應用程序之間同步/異步收發消息的中間件。消息是分佈式應用之間進行數據交換的基本信息單位,分佈式應用程序之間的通信接口由消息中間件提供。其中,異步方式指消息發送方在發送消息時不必知道接收方的狀態,更無需等待接收方的回覆,而接收方在收到消息時也不必知道發送方的目前狀態,更無需進行同步的消息處理,它們之間的連接完全是松耦合的,通信是非阻塞的,這種異步通信方式是由消息中間件中的消息隊列及其服務機制保障的。

消息中間件實現了發佈者和訂閱者在時間、空間和流程三個方面的解耦:

  • 時間解耦—-發佈方和訂閱方無需同時在線就能夠進行消息傳輸,消息中間件通過存儲轉發提供了這種異步傳輸的能力;
  • 空間解耦——發佈方和訂閱方都無需知道對方的物理地址、端口,甚至無需知道對方的邏輯名字和個數;
  • 流程解耦——發佈方和訂閱方在發送和接收數據時並不阻塞各自的控制流程。

從消息中間件的基本功能來看,無論是點對點消息中間件還是消息代理,其體系結構都是非常清晰簡單的。但由於分佈式應用及其環境的多樣性和複雜性,導致了消息中間件的複雜性。

當前的消息中間件仍然分爲兩類,一類是基於AMQP高級消息協議的,一類是基於JMS消息協議的。對於互聯網使用較多的RabbitMQ,Kafka等基本都基於AMQP高級消息協議。而對於Weblogic JMS,IBM MQ則是基於JMS消息協議的消息中間件產品。

對於Weblogic而言是一個企業級的應用服務器中間件,同時Weblogic JMS也是企業級的消息中間件產品,該產品是一個企業級消息中間件產品,具備了高可靠,高可用,高擴展,高性能基礎特性。支持主流的各種消息模型,消息發佈訂閱,消息持久化,事務處理,集羣等核心特性。

消息中間件的使用場景,具體包括瞭如下幾個方面:

  • 消息通知:單據狀態變化後的事件通知,數據傳輸完成後的事件通知
  • 異步集成:服務消費方只需要將數據送到OSB即實時返回,通過異步集成實現徹底解耦
  • 目標系統削峯:大併發數據導入而目標系統處理性能受限的場景
  • 消息發佈訂閱:基礎主數據通過JMS實現1對多的實時數據分發
  • 高可靠性場景:確保在數據集成中不出現任何丟失的情況

對於採用Weblogic JMS來實現消息集成,具體過程如下圖:

基於事件驅動的業務分析

而要做到同步轉異步,我們必須從業務需求分析開始就轉變思維,即從傳統的業務流程需求分析方法轉到事件驅動分析方法,這個在我很早的EDA事件驅動架構內容整理的時候專門談到過,今天摘錄部分內容供大家參考。

事件驅動框架(EDA)裏事件可傳輸於鬆散耦合的組件和服務之間。一個事件驅動系統典型地由事件消費者和事件產生者組成。事件消費者向事件管理器訂閱事件,事件產生者向事件管理器發佈事件。當事件管理器從事件產生者那接收到一個事件時,事件管理把這個事件轉送給相應的事件消費者。如果這個事件消費者是不可用的,事件管理者將保留這個事件,一段間隔之後再次轉送該事件消費者。

EDA架構往往具備如下特徵:

  • 廣播通信:參與通信的系統將事件廣播給任何對該事件感興趣的參與者。
  • 實時性:在業務事件發生時候,EDA架構下可以實時的發送事件給消費方,而無需等待
  • 異步:事件發佈系統不用等待事件接收系統來處理事件,發送到EDA模塊即可返回。
  • 細粒度:只要具備獨立的業務價值,即可以發佈爲細粒度的事件,而不是傳統服務下的粗粒度。
  • 複雜事件處理:根據業務流程需求,事件間可以聚合和組裝,形成事件鏈滿足複雜事件處理。
  • 並行運行:多個事件可以同時運行,單個事件可以同時分發給多個訂閱方。
  • 非阻塞:EDA本身提供MQ等消息持久化機制,不會在事件大併發下出現事件阻塞情況。

簡單來講,消息集成,異步,徹底解耦,消息發佈訂閱,事件鏈是EDA整個架構的核心。但是在EDA包括CEP複雜事件處理,在使用的時候首先還是應該瞭解清楚其和傳統流程驅動在業務分析方法上的區別。簡單來說,流程驅動和事件驅動的一個簡單比較可以用下圖描述:

基於EDA的核心業務分析思路說明

在事件驅動架構下,業務分析的核心就是事件的識別。而對於傳統方法往往則是關鍵流程和活動即可。在總體分析思路上的變化來說,傳統分析方法只分析到第2-3級業務流程,識別業務活動和交互點,而EDA需要業務分析時候分析到L4級的最底層EPC事件流程圖,並識別關鍵業務事件和事件分解,聚合關係。

在具體分析內容上的變化來說,傳統方法只關心業務活動,而不關係業務活動具體的啓動機制,業務活動完成後產生的業務事件。基於EDA業務分析方法,需要打開業務活動,識別業務活動的前者觸發條件和業務活動引起的業務對象狀態的變化,往往狀態變化點都是關鍵的事件識別點。

具體可以用下圖進行描述:

簡單事件-基於業務需求用例分析和識別

業務事件的識別可以從業務需求用例入手,分析業務用例中的業務前置觸發條件,分析業務對象的狀態流轉過程和後續操作,以找尋業務活動的事件輸入和事件產生。

從下圖裏面也可以看到,對於事件的識別往往比用例的識別更加細化,需要詳細的分析業務用例中的基本流,擴展流,業務規則,特別是關注其中核心的業務對象和單據狀態的變化。同時對於用例分析中的觸發條件也需要重點分析,這些觸發條件往往是事件鍊形成,或者說觸發消息事件訂閱的來源。

複雜事件-基於事件識別形成事件鏈

傳統的基於流程的業務分析方法往往只會分析到業務流程,具體的業務活動,而不關心具體業務活動執行前或執行後產生的業務事件,這和接口平臺前期重點關注數據集成有關係。而爲了保證業務實時響應需求,必須準確的識別業務事件,才能進一步設計基於業務事件的處理和響應機制。基於EPC事件流程鏈分析思路,需要對傳統分析流程進行細化,增加紅色事件識別點和事件分解聚合關係。在事件鏈的形成過程中往往存在一些複雜場景需要分析,包括了事件的一對多分發和訂閱,也包括了多個事件聚合,在滿足某個特定的業務規則後才觸發下一個新的業務活動和新事件。這些都是在複雜事件分析中必須考慮的內容之一。

從EDA事件驅動到CQRS

顧名思義,CQRS即命令查詢職責分離,將CUD操作和R查詢操作分離,對於CUD操作仍然參考傳統的領域模型建模思路來實現,但是在命令中增加了消息事件機制,實現CUD操作變更通過消息事件異步寫入到數據庫。

在CQRS中,查詢方面,直接通過方法查詢數據庫,然後通過DTO將數據返回,這個方面的操作相對比較簡單。而命令方面,是通過發送具體Command,接着由CommandBus來分發到具體的CommandHandle來進行處理,CommandHandle在進行處理時,並沒有直接將對象的狀態保存到外部持久化結構中,而僅僅是從領域對象中獲得產生的一系列領域事件,並將這些事件保存到Event Store中,同時將事件發佈到事件總線Event Bus進行下一步處理;接着Event Bus同樣進行協調,將具體的事件交給具體的Event Handle進行處理,最後Event Handler把對象的狀態保存到對應Query數據庫中。

對於CQRS,最容易想到的還是在數據庫層面做的讀寫分離模式,可以看到CQRS本身和數據庫的讀寫分離模式可以更好的匹配,由於採用事件驅動和消息訂閱模式,對於R讀庫我們可以更加容易對數據變更信息進行更新,達到讀庫數據的及時同步更新。同時讀庫既可以採用讀寫分離數據庫,也可以採用類似Solr,Nosql等分佈式,非結構化數據來實現彈性水平擴展能力。

在命令查詢職責沒有分離的時候,可以看到一方面是模型本身的擴展性受到影響,另外一方面是原有的領域模型本身偏重,而且Entity實體本身也通過完整的DTO對象進行傳輸,這樣在一些特殊的只需要更新或查詢個別字段的時候,整個模型仍然偏重。

通過命令查詢職責的解耦,不僅僅是提升整個框架模型的擴展性,更加重要是將兩類業務規則和實現徹底的解耦開,方便後續的功能開發和運維,特別是在整個業務場景和邏輯實現複雜的情況下,這種解耦會使整個開發架構更加清晰簡單。

同時也可以看到有一Command命令都是採用異步事件的方式進行寫入,因此不存在同步和長連接佔用的問題,有利於提升整個平臺在大併發下的整體響應性能。

當然,採用CQRS模式最大的一個問題點就是無法實現命令和查詢兩部分內容的強一致性保障,即很可能你界面上查詢到的數據不是最新的持久化數據庫裏面的數據,這個本身和消息管道異步寫入的實時性有關係。

其次在使用CQRS模式的時候,有一個重要假設就是,在事件和命令發出後,無特殊情況在事件接收方都必須要能夠接收事件成功處理,否則就存在大量的異常錯誤消息的異步回寫,反而增加系統的複雜度。舉個簡單例子來說:

當我們在電商平臺購買一個商品的時候,只要訂單提交成功,那麼這個訂單就一定能夠生效,也一定有庫存能夠發運和配送,而不是在後續到了配送環節的時才發現沒有庫存而導致訂單取消。如果這樣的話就極大的降低了系統本身的易用性。

即在異步命令和事件發送場景,當命令發送成功時候,雖然我們沒有及時接收到處理方的事件處理結果信息,但是我們默認是接收方能夠成功處理事件。但是我們也看到在CQRS場景框架下,只要命令事件發出,我們並不需要等待任何反饋信息。

另外還有一種CQRS實現場景,即雖然在內部對Command命令處理的時候是基於事件機制,異步響應,但是客戶在前端的操作是同步等待返回。在這種情況下我們就可以保持前端連接,但是是否後端的類似DB連接等。

在CQRS模型下,由於職責分離,可以看到我們通過事件和消息的訂閱,可以實現多個讀庫的訂閱,這些讀庫既可以是結構化數據庫,也可以是非結構化數據庫;既可以用來實現業務功能本身的查詢讀,也可以用來做海量數據本身的分佈式全文檢索。

對於CQRS框架的實施,不是簡單的設計模式使用問題,更加重要的仍然是是否能夠接受最終一致性要求,同時在該要求下將傳統的同步請求下業務功能和邏輯處理機制轉變爲異步事件價值下的事件鏈驅動模式。要實現這種轉變就必須能夠拆分出獨立,自治的命令和事件,同時確保這些事件在朝後端業務功能和邏輯模塊發送的時候能夠處理成功(即該做的校驗必須提前做完)。

將同步接口調用轉爲本地消息緩存

這個類似消息中間件的功能,舉例來說我們設計了一個同步發送訂單到ERP系統的接口,如果在同步實時調用這個接口服務的時候出現異常,那麼我們可以首先將消息存儲到本地,然後設置定時任務和重試機制,通過重試方式將消息發送到目標系統。

即對於業務功能來說不用關心實時是否發送成功,而由業務系統自身機制來完成消息發送的重試。而要做到這點,在接口功能設計時候,最好要做到單據業務完整性校驗接口和實際的數據發送接口分離,即先調用接口進行完整性校驗,在校驗沒有問題後再進行消息發送。以確保最終發送的消息不會因爲數據完整性的原因導致無法發送成功。

查詢數據的本地化緩存或落地

memcached是一套分佈式的高速緩存系統,由LiveJournal的Brad Fitzpatrick開發,但被許多網站使用。這是一套開放源代碼軟件,以BSD license授權發佈。

memcached的API使用三十二比特的循環冗餘校驗(CRC-32)計算鍵值後,將數據分散在不同的機器上。當表格滿了以後,接下來新增的數據會以LRU機制替換掉。由於memcached通常只是當作緩存系統使用,所以使用memcached的應用程序在寫回較慢的系統時(像是後端的數據庫)需要額外的代碼更新memcached內的數據。

對於實時查詢類接口,將查詢的基礎數據進行本地化緩存,即如果在實時查詢出現異常的時候,我們可以直接查詢本地緩存的數據,減少對業務功能使用的影響。

比如查詢供應商接口服務,如果主數據系統提供的接口出現異常,我們可以直接查詢本地緩存的供應商數據。這種模式對於變更不頻繁的數據基本都適應,同時本身也減少實時調用接口帶來的性能損耗。

如果是接口服務註冊在API網關或ESB服務總線上面,我們還可以考慮在ESB服務總線上啓用緩存能力,即對於調用過的接口,在同樣參數重複調用的時候能夠通過緩存數據獲取,這樣即使在源端業務系統不可用的情況下,也不好影響到當前接口服務的成功調用。

可以適度考慮數據落地

在微服務架構裏面,我們一直在強調一點,即數據實時需要實時訪問,不進行底層數據庫的數據集成和同步,這既滿足了數據的高一致性,也滿足了數據實時性的要求。

但是帶來的問題就是強耦合,如果數據提供方出現異常,那麼導致消費方業務功能也無法使用。後臺(朱小廝的博客)回覆1024,即可領取k8s資料。

因爲我們可以適量考慮數據落地方式的數據集成在整體微服務架構實施過程中,對於變化不頻繁的數據適度落地到微服務模塊本地。這樣本身可以減少實時的業務接口服務調用,增加單個微服務模塊的可用性和可靠性。

對於已經出現強耦合如何重構

如果微服務已經實施完成並出現了大量緊耦合的情況,那麼我們就需要在後期考慮對微服務架構進行重構,具體重構的方法可以從如下幾點考慮。

兩個微服務本身緊耦合

如果兩個微服務間出現大量接口相互調用,即可以認爲緊耦合。

或者我原來的判斷標準,即兩個微服務對應的後臺數據表,其中30%以上都需要兩個微服務交叉訪問,那麼就認爲兩個微服務本身耦合性極強。

在這種情況下處理措施就是原來微服務劃分的太細了,需要對兩個微服務進行合併。

交叉依賴變爲共性依賴

要知道在傳統軟件開發裏面往往是不允許兩個組件交叉依賴的。

但是在新的IOC和微服務開發裏面,大量都是反射調用,兩個組件相互依賴不會有問題。但是這本身也不是一種很好的設計方法。

如果兩個微服務或多個微服務相互依賴內容本身具備共性。那麼最好的做法就是將共性內容全部移出,下沉爲一個共性基礎微服務模塊再朝上提供服務。

即交叉依賴轉變爲對底層的共性依賴。

對某個微服務實現單元進行遷移

爲什麼出現這種場景?

簡單來說就是原來的微服務模塊劃分和業務功能劃分不合理。比如上圖中的微服務A中的A1部分。這個部分內容需要大量被微服務B調用,但是A1實際依賴微服務A中其它部分的內容卻很少。

這種就是典型的A1部分功能劃分位置不合理。

最好的做法就是將A1功能從微服務A遷移到微服務B,實現對原有業務劃分不合理的糾正。

將細粒度服務轉變爲粗粒度服務

服務本身應該具備粗粒度屬性,暴露僅僅需要暴露的內容。

比如微服務A實現客戶信用檢查和評級。微服務B需要客戶信用。有兩種做法

第一種是B調用A多個接口,把客戶基本信息,客戶交易信息,客戶違約信息全部查詢過來,然後自己計算客戶信用。

第二種即是隻需要輸入客戶編碼,微服務A返回最早的信用評級。

對於後者就是我們常說的粗粒度接口或領域服務,服務間的交互應該以領域服務和粗粒度服務爲主,避免掉完全的數據庫表的CRUD類服務接口。

寫在最後

歡迎大家關注我的公衆號【風平浪靜如碼】,海量Java相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。

覺得寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

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