1. 程式人生 > >從既有系統到微服務架構

從既有系統到微服務架構

微服務近年來可謂炙手可熱,合理的使用微服務架構可以解耦系統、提供更好的軟體伸縮性以及提高組織的敏捷性。然而現實中較少有專案一開始就會選擇使用微服務架構,絕大多數新專案在最初都會務實地從更容易掌控的單體架構起步構建,如果最終發現單體架構複雜到影響了團隊的開發效率及軟體的伸縮性等方面時,才會開始考慮逐步將系統往微服務架構做演進。

現實中任何軟體架構都是諸多trade-off的結果,想要獲得微服務架構所帶來的好處也就意味著有能力承擔它所帶來的的副作用。Martin Fowler就曾在MicroservicePrerequisites一文中指出實施微服務所需要的先決條件,他用“個子是否夠高”形象地比喻了微服務所需的技能門檻。

而對於既有系統,還需要一種務實的演進方法和實施策略,使得能夠伴隨著恰當的程式碼重構,逐步積累能力和完善基礎設施,最終平穩的將其演進到微服務架構。

本文總結了一些從既有系統到微服務演進之路上會遇到的問題和解決策略。文中使用“既有系統”而非“遺留系統”,是因為遺留系統給人一種即將退出生命週期、行將就木的感覺,而我們則希望把精力投入到還有長遠商業價值的系統上,通過合理的微服務演進讓其具有持續的生命力。

演進策略

本文推薦的從既有系統到微服務的一種務實安全的演進策略是:自上向下分析,自下向上重構,逐步完善配套。

所謂“自上向下分析”,主要包含以下步驟:

1.整體演進路線規劃:

梳理既有系統的領域模型,設計合理的內部服務邊界,按照優先順序和依賴關係規劃演進路線;

2.服務治理方案設計:

按照優先順序,為新服務定義職責,介面,與既有系統的互動方式以及跨服務的整合測試方案;

定義新服務的打包、測試、釋出、部署、整合方式,目標是能夠為其構建獨立的程式碼庫和持續交付流水線;

3.程式碼解耦設計和重構:

分析屬於新服務的獨立程式碼以及和既有系統耦合的程式碼,從物理打包和邏輯程式碼重構兩層面解決耦合問題;

針對不同的解耦策略,制定不同的測試策略,完善自動化測試以支撐對應的程式碼重構工作。

所謂“自下向上重構”,指的是按照前面的分析設計結果,從程式碼重構開始,自下向上按照優先順序和一定節奏持續進行服務化改造。

而“逐步完善配套”,指的是隨著服務化的開展,逐步完善程式碼庫管理,多流水線整合,並逐步按需引入服務治理框架,積累微服務需要的技術和工具能力。

上述過程是一個迭代的過程:通過適度的分析和設計,規劃出具體的落地工作,然後通過小步增量的實踐迅速獲得成果和反饋,在過程中逐步培養人的能力、完善支撐微服務架構的工程實踐。

自上向下設計

明確目標和約束

對既有系統做微服務化解耦,需要對不同解耦方向能獲得的收益和存在的約束做到心中有數。見過一些組織在做微服務拆分時只強調可以獲得的片面好處,忽略了對組織更有益的其它潛在價值,或者低估了微服務化帶來的問題。這往往會導致不合理的服務邊界劃分或者錯誤的優先順序排序。

沿著不同的邊界劃分,目的是為了不同的價值目標:

沿著系統內不同的變化原因和變化頻率做服務劃分

通過隔離不同的變化方向,減少特性開發之間的干擾,使能小的獨立交付團隊。通過獨立程式碼庫、獨立流水線,獨立的開發、測試、交付和運維過程,提高交付效率和響應速度。

沿著不同的資源使用邊界做服務劃分

通過將不同資源佔用特徵的服務進行隔離,使能獨立的水平彈縮,優化資源使用效率和提升業務響應能力。

沿著不同效能路徑邊界做服務劃分

通過將效能核心路徑作為獨立服務進行隔離,可以為效能核心路徑使用不同的技術棧以及做各種極致的效能優化;另一方面避免各種改動影響到關鍵路徑的效能下降(例如被動引入更多的非同步互動等)。

由於服務劃分會為系統引入新內部邊界,所以必須考慮如下的約束:

資料一致性約束:服務劃分後可能帶來資料一致性變弱的問題,需要考慮是否可以接受;

效能約束:服務劃分後帶來的潛在效能下降,需要考慮如何度量以及承受程度;

容錯性約束:服務劃分為系統內部引入更多的分散式故障點,需要能夠為其找到可接受的容錯設計;

耦合關係約束:服務劃分會放大系統的耦合問題,所以需要考慮沿著系統的鬆耦合邊界進行服務劃分,避免服務間複雜的互動或者聯動修改。

在開始可以按照理想的價值目標去劃分微服務邊界,然後再接受每一項約束的挑戰,最終的服務劃分方案往往是一個在目標和約束之間逐漸平衡後的結果。

避免過度設計陷阱

對既有系統的微服務改造設計往往會陷入“架構設計陷阱”。過於詳盡的分析和設計反而常常會阻礙微服務的拆分,經常得到一個“成本很大,困難很多”的論證。

對於這種情況,建議採用 快速啟動、增量交付、大膽實驗、小心求證 的原則。即快速構建目標,通過敏捷和精益軟體開發的方式快速實踐,通過反饋進行快速學習,在行動中解決各種問題。

具體的實踐過程中:

有了基本的分析後,快速成立試點團隊作為探索者進行解耦驗證,儘早獲得反饋;

雖然快速啟動,但是短期目標要明確,通過迭代的增量交付來規避風險;

在實踐過程中逐步按需對修改影響較大的特性補充和完善自動化測試;

對有較大風險的程式碼修改,可以先拷貝一份在新的服務內做實驗,獲得足夠反饋後再擇機合入原始碼庫;

可以藉助工具分析程式碼的依賴關係。曾經在一個專案我們通過部署doxygen和graphviz來視覺化程式碼的依賴關係和解耦進展,取得了不錯的效果。

微服務設計

關於微服務設計的方方面面已經有很多優秀的書和文章了,例如《微服務設計》就是一本不錯的教材。即便如此到任何一個具體的領域,仍有很多困難和挑戰,需要領域專家和軟體工程師們密切配合去解決。

使用領域驅動設計(DDD)方法可以幫助所有參與者重新梳理業務並達成共識。通過識別業務的界定上下文和聚合根,可以為如何劃分服務提供參考。可以嘗試組織DDD Workshop,但是要清楚這只是一項工具,而且有時成本並不小。DDD是一個演進式的過程,更多的工作需要隨著深入業務和程式碼,通過實踐收集反饋迭代式的進行。

現實情況中,負責系統架構演進的人員都是對業務和設計現狀比較熟悉的專家,一種高效的做法是從當前的資料模型直接入手。分析每一張表和每項欄位所支撐的業務,將業務按照資料的內聚性進行分類,然後以此作為服務劃分的起點。可以假設已經將資料按照新的服務邊界重新分庫分表,然後嘗試基於此重新構建每條業務流程,並在過程中解決由於資料拆分而出現的各種問題。該做法適合對微服務架構有經驗的人和領域專家合作完成,這樣能夠對出現的各種問題找到不偏頗的解決方案。

天下沒有免費的午餐,有時為了得到微服務的好處,是需要做一些妥協的。例如資料模型中某一實體的不同屬性具有不同的業務內聚度,所以同一概念的不同屬性資料被分到了不同服務中,但是這些資料在某些場景下要保持同步(例如需要被整體刪除或修改等)。最常見的解決方案是選擇一個穩定的服務作為對該實體的權威擁有者,其它服務通過某種手段(例如訊息佇列)和該服務對齊各種例項操作。這意味著業務要能接受最終一致性,還得接受在某些異常場景下資料一直沒有同步成功而上報的告警。

在設計服務的整合方式時,需要站在業務角度去識別誰是更穩定的服務。依據“向穩定方向依賴”的原則,我們只會讓不穩定的服務去呼叫穩定服務的API,而反過來穩定的服務最好通過訊息佇列釋出事件。那些需要跨越多個服務去獲取資料的服務,一般可以通過監聽事件和快取與系統解耦,但這並非適合所有場景。在某些場景下由於業務的一致性和效能限制,我們確實需要往回退,把某些服務進行合併。這就是個不斷的頭腦風暴,然後再在各種選擇中做trade-off,最終獲得平衡的過程。

對於缺乏經驗的團隊可以從較容易拆分的服務做起。例如一個web服務端可以先把路由和基本鑑權拆分出來,交給API Gateway負責;然後再把各種報表和統計等一致性與效能要求相對低的拆分出來,最後再嘗試切分其它業務處理。

一旦服務拆分出來,就可以根據業務特徵重新優化資料模型並選擇更適合的資料庫。另外服務的API設計也是有技巧的:應用介面隔離原則,需要API能獨立完成功能,又要粒度相對小可以靈活組合。這方面亞馬遜各種AWS服務的API設計就是不錯的樣例。

自下向上重構

得到了可行的服務劃分方案,接下來就需要實際操作程式碼,將新服務的程式碼與既有系統進行解耦,為獨立的服務程式碼庫和流水線做好準備。

目錄/包結構調整

軟體的包結構一般和構建軟體的組織結構以及建模方式有關。一般複雜系統同時存在著兩個大的變化方向:技術維度和業務維度,而軟體的包結構往往只能反映其中的一個維度。當組織結構以軟體的技術維度進行劃分,那麼系統的包結構也基本上會以此劃分,這時業務維度的變化往往會對映到系統的每一個包上。反過來也是一樣!衡量哪種包結構合理,往往是看當前哪個是主要的變化方向。對主要的變化方向進行拆包隔離,可以降低程式碼變化之間的互相影響程度。

如果按照變化方向進行包的拆分,就會發現系統中應該存在很多小的包,最後每個服務是一堆原子的小包組合。這本質上是將系統重新進行合理模組化的過程。Adam Drake在文章Enough with the microservices中就直接指出微服務架構應該先從良好的模組化重構做起,大多數時候當模組化做好了甚至會發現很多問題已經得到解決了。

然而既有系統的模組合理化調整很難僅通過重新拆包達成!因為程式碼是有邏輯的,模組化的邏輯邊界不可能剛剛好落在程式碼檔案邊界。大多數情況下都需要先對某一個程式碼檔案進行拆分,對某一個類或者函式進行重構,對某一段邏輯進行重新設計,然後才能重新得到一個一致的邏輯和物理邊界,支撐繼續的拆包工作。

之前見過一個組織通過拆包進行系統解耦,他們把新服務和既有系統共享的所有程式碼拆分成很多小的共享包。這樣做後看似每個服務在構建和流水線是獨立性的,但是問題在於那些共享包的程式碼量並不小而且包含很多耦合的業務邏輯,新的修改經常導致新服務和既有系統一起升級更新。

可以先對新服務建立獨立的目錄,然後嘗試把屬於新服務的程式碼逐漸往獨立目錄中遷移,在這一過程中識別出新服務和既有系統耦合的程式碼,然後一邊重構,再一邊繼續調整目錄和包結構,最後使得新服務和既有系統在物理和邏輯上同時解耦。

程式碼重構

軟體重構目的是為了解耦新服務和既有系統之間的共享程式碼。共享程式碼一般分為如下幾種形式:

1.共同依賴的元件或者類,這又分為如下幾種情況:

穩定的基礎功能程式碼。例如編解碼庫,加解密等等。這些程式碼可以按照功能釋出成獨立元件,供每個服務自行決定使用。

服務間介面和訊息定義。這類程式碼可以劃分到獨立的庫中,儘量保持向前相容,由介面的消費方自由選擇依賴的版本。服務間的API和訊息定義在本質上是契約的共享,可以使用契約描述檔案代替共享程式碼,使用時自動從契約描述生成程式碼,這對於不同技術棧的服務會比較友好。

不合理編碼導致的耦合。例如耦合了所有功能的大而全的單例類,一般是一些全域性配置類或者是“建立一切”的工廠類等。這種情況需要對原有設計進行重構,對大而全的類進行拆分,將屬於不同服務的程式碼拆分到不同的類中,由各個服務領回屬於自己的程式碼。

2.共同繼承的介面或者類,這又分為如下幾種情況:

繼承是為了組合:需要將繼承的公共處理重構為支撐元件,由不同的服務根據需要自行選擇組合和使用方式。

繼承是為了面向介面程式設計,這時介面往往是為了配合某些公共業務處理而做的抽象。這些公共處理可以按照以下幾種情況進行重構:

介面背後的公共處理包含了複雜的業務邏輯,優先考慮將該公共處理變為一個服務。這時需要將繼承介面上的同步呼叫變為服務間的訊息介面。

介面背後的公共處理簡單或者並不穩定,可以考慮按照“Replication Over Reuse”的原則,由每個服務自行實現,減少服務間的程式碼共享。

介面背後的公共處理複雜,但是包含的業務邏輯相對穩定,如果不能將其獨立為服務(例如由於效能原因),可以將其打包成公共元件,由每個服務自行組合使用。

從既有系統到微服務演進,在具體的落地中會發現最基礎的工作主要是程式碼重構。而能否很好的實施程式碼重構是一個體現團隊基本軟體技能素質的過程,需要團隊提升軟體設計、程式碼重構、自動化測試方面的能力。

逐步完善配套

隨著自下向上的重構,新服務的程式碼逐漸解耦到獨立的目錄或者包中,這時就可以按需補齊服務化所欠缺的服務治理機制和各種工程實踐。在服務程式碼不具備獨立性的時候開始嘗試搭建各種服務治理機制和工程流水線,往往會引入很多偶發複雜度,對工具提出一些不切實際的要求。

服務治理

微服務作為面向服務架構當下能夠流行,原因之一在於隨著技術的進步各種服務治理工具都可以廉價獲得。服務註冊發現、API閘道器、訊息佇列、負載均衡、服務監控、叢集運維等每種需求都可以在網路上找到一批的開源工具,而團隊則需要根據自己的現狀進行合理的選擇。有經驗的團隊可以把各種不同的治理工作交給最合適的工具去做,而對於缺乏經驗的團隊來說可以先從某一工具入手累積經驗。曾經有一個團隊在開始不想引入過多工具複雜性,先選擇使用redis做快取和訊息佇列,隨後又使用redis做分散式配置以及服務的註冊與發現等等。後來隨著能力提升,轉而使用etcd替代redis做服務的註冊發現,使用kafka做訊息佇列。

對服務治理工具的選擇要避免陷入選擇困難症。每個團隊都會覺得自己的業務特殊,開源工具總是不能滿足自己的所有要求。帶著這種想法很容易裹足不前,一再浪費架構重構的合適時機。精益的做法是,先找到業界普遍使用的工具,一邊使用一邊解決問題,一旦開始了很多問題在實踐中總能迎刃而解。對於一些注重效能的系統,不可避免的需要對開源元件在特定業務場景下進行優化定製,也最好先開始使用然後在實踐中確定優化的方向。

持續交付

“服務有自己獨立程式碼庫和交付流水線,可以避免交付過程中的互相干擾,提高交付速度和質量”,遺憾的是上述描述其實是個偽命題!

真正減少團隊干擾、提高交付速度和質量的是“正確的解耦”本身。獨立的程式碼庫和流水線會將架構約束顯示化,讓團隊成員難以犯錯。但是如果過早的對不成熟或者不穩定的架構邊界進行固化,反而會降低團隊的效率,讓後續架構調整變得困難。另外在系統沒有合理解耦的情況下,獨立的程式碼庫和流水線只會讓互動變得更復雜,導致對構建和釋出工具提出一堆不合理的要求。

但是如果服務之間確實已經正交拆分,程式碼邊界和架構邊界一致並且是穩定的,這時獨立的程式碼庫和流水線就可以降低團隊在交付流水線上的互相干擾和排隊,此時就值得為新的服務建立獨立的程式碼庫和自動化流水線。考慮到服務之間的整合,往往需要多級流水線,這時選擇一款支援pipeline的持續整合工具是必要的。Jenkins2.x以及GoCD是此類產品的代表。

適應的組織結構和文化

康威定律經常被拿來說明組織結構和系統架構之間的互相作用關係。在對既有系統的服務化重構中,軟體架構和團隊結構同步進行調整會讓整個過程更加順暢。曾經有一個系統最初按照技術維度劃分團隊,後來為了提高響應市場的速度把團隊按照不同的業務型別進行了調整。重新劃分後的團隊開始發現他們會經常修改同一公共元件,這時他們自發的對該元件進行了解耦,將其中和業務相關的邏輯各自領了回去,然後將剩下的穩定邏輯下沉到了基礎設施中。

除了匹配的組織結構,還需要團隊逐漸調整自己的文化。專門的測試人員和運維人員在微服務架構下必然成為瓶頸,需要改變傳統的細分工的文化。團隊每個成員都要有意願和能力承擔起服務的測試和運維工作,這需要組織從文化建設到考核方式做對應的調整。

總結

對於既有系統做微服務演進,一旦第一個服務改造成功,後續的服務藉助前面的成功經驗和已有的基礎實施,會更加的容易拆分。不過第一個服務的拆分確實需要投入比較大的決心和精力,本文給出了一些建議,歸根結底總結起來就是:以精益的方式開展,以程式碼解耦為核心,以服務化技能做武裝,以組織結構和文化調整做基礎!

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:744677563

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!