從既有系統到微服務架構


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

現實中任何軟件架構都是諸多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是此類產品的代表。

適應的組織結構和文化

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

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

總結

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

原文出處:https://www.jianshu.com/p/1d229212e655
相關文章