微服務遷移之旅 微服務拆分之道

微服務拆分之道

——Zhamak Dehghani

原文

解耦何物,何時解耦

當單體系統龐大到無法應付時,大多企業都會被迫把它們拆分成微服務。這是一次值得的旅行,但卻非坦途。我們學到了一些如何把這件事情做好的東西。我們需要從簡單的服務入手,然後把對業務重要的經常變化的垂直功能的服務挑出來。這些服務首先應該是大的,而且最好不依賴單體系統的其他部分。我們應該確保遷移的每一步都代表一個對整個架構的原子性改善。

遷移單體系統到一個微服務生態系統將會是一場史詩搬的旅行。開啓這段旅行的人們都希望能夠擴大運維的規模,加快變更的步伐以及避免變更的高昂代價。他們希望他們團隊的數量獲得增長,同時可以彼此獨立並行的交付價值;他們希望快速試驗覈心業務功能,更快地交付價值;他們也希望避免因對已有單體系統的改變產生高成本。

把一個單體系統拆分進微服務生態系統,解耦什麼功能,何時及如何增量遷移是架構挑戰的一部分。在這篇文章中,我將分享一些技巧以指導交付團隊——開發人員、架構師及技術經理——在這次旅途中如何拆分服務。

爲了闡明這些技巧,我要借用一個多層的在線零售系統。該系統把用戶接口,業務邏輯和數據層緊密結合在一起。我選擇這個例子的原因是因爲它的架構具有許多運行業務的單體應用的特徵,而且它採用的技術棧也是很先進的,非常適合分解,而不是完全重寫和替換。

目的地——微服務生態系統

在踏上旅途之前,擁有一個對微服務生態系統的一致的理解對我們來說是關鍵的。微服務生態系統就是一個服務平臺,其上的每個服務封裝了一個業務功能。業務功能代表在一個具體的域裏面爲了實現業務目標和職責而做的事情。每個微服務暴露一個API,開發者可以發現並以自服務方式使用它。微服務擁有獨立的生命週期。開發者可以獨立的構建、測試和發佈每個微服務。微服務生態系統實施的組織結構中,團隊是自治的且長期存在的,每個團隊負責一個或多個服務。與人們普遍的認知不同,與微服務這個詞中的‘微’字面意思不同,每個服務的大小並不重要,它會因組織運維成熟度不同而變化。正如Martin Fowler所說:“微服務是一個標籤而不是一種描述”。


旅行指南

在開始深入瞭解本指南之前,認識到拆分已有系統到微服務總體成本會很高而且可能需要多次迭代很重要。開發者和架構師必須仔細評估是不是有必要分拆單體系統,微服務本身是不是其真正的歸宿。講完這些,讓我們來看看本指南。

從簡單,能合理解耦的功能開始熱身

開始微服務之旅需要一個最低級別的運維就緒。它需要按需訪問部署環境,創建新類型的CI管道,獨立編譯,測試和部署可執行的服務。同時它還需要爲分佈式系統提供安全,調試以及監控的能力。無論是構建全新(greenfield)服務還是分解已有系統都需要成熟的運維就緒環境。關於運維就緒的更多信息,可以閱讀Martin的關於微服務前置條件的文章。不過,好消息是從Martin的那篇文章之後微服務架構運維技術得到快速的演進。其中包括誕生了Service Mesh——一個具體的運行快速、可靠及安全的微服務網格的基礎架構層,提供更高層面的部署架構抽象的容器編排系統,像GoCD這樣的以容器的方式編譯,測試和部署微服務的CD系統。

我的建議是開發和運維團隊構建底層基礎架構,爲第一個和第二個被分解出來或新構建的服務構建CI管道和API管理系統。我們從單體系統中能合理分解出來的功能開始。因爲,他們不需要改變許多正在使用單體系統的面向客戶的應用,可能也不需要數據存儲。在這個時候交付團隊需要優化他們交付方法,爲團隊成員提供技能培訓,建立最小化的基礎架構來交付安全的可獨立部署的暴露自服務API的服務。比如,對於在線零售系統,第一個服務可能是這個單體系統調用驗證用戶的“終端用戶驗證”服務。第二個可能是“用戶Profile”服務, 它是一個面板服務,爲新的客戶應用提供一個更好地消費者視圖。

首先,我建議解耦簡單的邊界服務。其次我們採用不同的方法來分解深度嵌入在單體系統中的功能。之所以建議一開始分解邊界服務是因爲在這段旅行之初,交互團隊最大的風險在於不能對微服務進行合適的運維。所以可以使用邊界服務來實踐他們所需的“運維前提”。一旦他們解決了運維先決條件相關的問題,他們就能夠應對拆分單體系統的關鍵問題。


最小化對單體系統的向後依賴

作爲一個最基本的原則,交付團隊應該最小化新形成的微服務對原單體系統的依賴。微服務主要的一個優勢在於具有一個快速且獨立的發佈週期。與原單體系統的數據、邏輯或者API的依賴會把服務與單體系統耦合在一起,妨礙微服務優勢的發揮。通常我們從單體系統分解出服務的原因是與其綁定的功能變更的高成本和低效率。所以,我們希望通過消除對這個單體系統的依賴逐步解耦這些核心功能。假如團隊遵守這個原則構建功能服務,他們會發現依賴都是反向的從單體系統到服務。這是所期望的依賴方向,因爲這不會影響新的服務的變更速度。

考慮在線零售系統,“購買”和“促銷”都是核心功能。“購買”的功能結賬的時候會使用“促銷”功能根據用戶購買的產品爲他們提供有效的最佳優惠。假如我們需要決定先解耦這兩個功能的哪一個,我建議先解耦“促銷”,再解耦“購買”。因爲按照這個順序我們就能夠減少對這個單體系統的依賴。按照這個順序,“購買”起初還會維持在老的單體系統內,但卻是依賴新的“促銷”微服務。

下一個指導原則是關於開發者決定解耦服務順序的其他方式。這意味着他們不總是能夠避免要反向依賴原單體系統。如果新的服務最終需要回調單體系統,我建議從單體中暴露一個新的API,在新的服務中通過一個“防腐”層來調用該API以確保單體系統的概念不會浸染微服務。要儘量確保這個API按照定義良好的域慨念和結構定義,即使單體系統內部實現可能不同(注:該新的API可以理解成是一個適配器,它按照新的微服務定義良好的業務域概念設計,但它需要實現單體系統中老的域概念到新的域概念的適配。通過這個API使得拆分出來的微服務對單體系統中舊的域概念透明,防止了舊的域概念“入侵”。該API起到了防腐層隔離的作用)。在這種不好的情況下,交付團隊可能承受改變單體系統帶來的成本和難度的問題,需要配合單體系統的發佈一起測試發佈的新的系統。

黏連性功能早拆分

假設現在交付團隊可以很愉快的構建微服務了,並且準備好了解決棘手的問題。然他們發現接下來的功能拆分不依賴單體系統是不可能的。存在這個問題的根本原因常常是因爲單體系統內的功能域概念設計部完整,沒有良好定義,單體系統內許多其他的功能依賴它。爲了能夠繼續,開發者需要識別這類黏連性功能,分解成定義良好的域概念,然後把這些域概念具體化到不同的服務中。

比如,在一個Web單體系統中“會話”的概念是最常用的耦合因子之一。 在那個在線零售的例子中,會話常常就是一個籃子,裝有許多屬性,從涉及不同域邊界的用戶偏好如發貨和支付設置等,到用戶的意圖和交互,如當前訪問頁面,點擊的產品和意向清單等。除非我們對當前“會話”概念解耦,分解及具體化,否則我們將爲解耦未來的諸多功能而疲於奔命,因爲他們會因爲這個不嚴謹的會話概念和單體系統“糾纏”在一起。我也不鼓勵在單體系統之外創建“會話”服務,因爲它也只會導致和目前在單體進程中存在的相似的緊密耦合。而且,情況會更糟糕,因爲這種耦合是進程外跨網絡的。

開發人員可以從逐步從黏連性功能中提取出微服務,一次一個。比如, 重構“消費者意向清單”,並提取出來創建一個新的服務,然後重構“消費者支付偏好”,產生一個新的微服務。如此重複。


使用依賴和結構化代碼分析工具,比如Structure101,來識別單體系統中耦合度最高,約束最大的功能因子。

垂直拆分並儘早切分數據

從單體中把功能拆分出來主要是爲了能夠單獨發佈這些功能。第一個原則就是指導開發人員進行如何拆分。單體系統經常是由緊密集成的多層,甚至是彼此依賴脆弱而且需要同時發佈的多個(子)系統組成。比如,在線零售系統是由一個或多個面向客戶的在線購物應用,一個實現許多業務功能的後端系統以及用於保存狀態的中心化集成的數據存儲組成。

許多拆分都是企圖從提取出面向用戶的組件入手,同時拆分出幾個面板服務爲開發者提供友好的新式用戶界面的API。然而,數據依然維持在一個schema和一個存儲系統中。這種方法可以快速獲勝,比如可以更頻繁的改變用戶界面。但是當設計核心功能時,交互團隊只能以單體系統和其數據存儲變更最慢的部分效率進行。簡單來說,數據沒有拆分,就不是微服務架構。把所有數據保存在同一個數據存儲裏也是不符合去中心化數據管理微服務特徵。

爲此,我們的策略就是把功能垂直移出,將核心功能和數據解耦,並且把所有前端應用都重定向到新的服務API。

多個應用共享中心化數據是切分拆分後服務所使用的數據的主要障礙。交付團隊需要採用適合他們環境的數據遷移策略,這取決於他們是否能夠同時重定向和遷移所有的數據讀寫者。Stripe的四階段數據遷移策略是其中之一,它被應用到要求逐步遷移與數據庫集成的應用,同時所有系統都可以持續運行的環境中。

避免僅僅拆分前端用戶面板和後端服務而不拆分數據。

拆分重要且頻繁變化的業務

把功能從單體系統中拆分出來是件困難的事情。我聽到過Neal Ford用周密的器官手術對此做過類比。在在線零售應用中,拆分出一個功能需要把該功能涉及的數據、邏輯和用戶接口組件仔細地剝離出來,然後重定向到新的服務中。這需要花費不少的工作量,開發者需要基於他們獲得的好處,比如提高效率,擴大規模等持續評估拆分的成本。舉例來說,如果交互團隊目標是加速對已有的被困於單體系統中功能的修改,那麼他們必須識別出一直被修改的最多的功能然後把它拿出來。把那些不斷變化的代碼分離出來。這些代碼正在獲得開發人員大量“關愛”,但又在束縛他們快速發佈價值。交互團隊可以分析代碼提交模式找出以往變化最多的代碼, 然後結合產品路線圖及其組合來了解最期望的在不久的將來亦最受關注的功能。開發人員需要與業務和產品經理來交談以瞭解對他們來說真正重要的差異化的功能。

拿在線零售系統的例子來說,“客戶個性化”功能需要反覆進行試驗以便爲客戶提供最好的體驗。所以它是一個好的拆分候選項。它是一項對業務,對用戶體驗很重要而且會頻繁變化的功能。

使用社交代碼分析工具如CodeScene發現最活躍的組件。如果編譯系統在每次代碼提交的時候恰好會觸及或者自動產生代碼,請確保過濾掉了這些“雜音信號”。把這些頻繁變化的代碼和產品路線圖即將發生的變更相對應然後找出需要拆分的交叉點。

拆分功能,而不是代碼

不管什麼時候,開發人員想從已有系統裏頭剝離服務有兩種方式:代碼萃取和功能重寫。

通常缺省的情況下,服務提取或者單體分解被假定爲重用原來已有的實現並把它摘取出來放入單獨的服務中。這樣做的部分原因是我們對自己設計和編寫的代碼存在認知偏差。不管過程如何痛苦結果如何不完美,付出的勞動會使我們對代碼產生感情。事實上,這被稱作爲宜家效應。不幸的是,這種認知偏差將會妨礙單體系統分解的工作。它會導致開發人員,甚至包括技術經理即使萃取成本高但價值低也要重用代碼。

作爲一種選擇,交互團隊可以重寫功能,而廢除老的代碼。重寫爲他們提供一個機會來重新審視業務功能,啓動與業務會話,簡化其往業務流程,挑戰隨着時間推移在老的系統形成的陳舊的假設和約束。這也爲技術更新,爲使用對具體服務最適合的技術棧及編程語言實現服務的機會。

就在線零售系統來說,“定價”和“促銷”功能是一塊複雜的智能化代碼。它能夠動態配置應用定價促銷規則,基於諸如消費者行爲、忠誠度及產品包等各種因素提供折扣和優惠。

上述功能比較適合代碼萃取重用。然而,“用戶Profile”是一個簡單的CRUD功能,其主要由系樣板式的系列化,處理存儲和配置的代碼組成。所以它比較適合棄用老代碼而重寫。

根據我的經驗,在大部分分解場景中,團隊最好重新實現新的功能而廢棄老的代碼。但因爲下面的原因,可以考慮高成本低價值的重用:

存在大量的處理環境依賴的樣板式代碼,比如在運行時訪問應用配置,訪問數據,緩存,或者是使用老的框架構建的。這些樣板式代碼大部分需要重寫。而新的運行微服務的框架與十幾年前的老框架差別巨大,需要使用幾乎不同的樣板式代碼;

很有可能已有的功能不是圍繞清晰的域慨念構建的。這會導致沒有反映新的域模型而需要進行大重構的數據結構傳輸和存儲;

長期遺留的代碼,經歷了許多次變更迭代,可能具有很高級別的代碼毒性和較低的重用價值。

除非是關聯的,和清晰的域概念保持一致的,具有高知識產權的功能,我強烈建議對其重寫,棄用老的代碼。


使用代碼毒性分析工具如CheckStyle來決定重用還是重寫

先大後小

從遺留的單體系統中尋找域邊界是一門藝術也是一門科學。作爲一般規則,應用域驅動技術尋找定義微服務邊界的“有界上下文”是一個很好的起點。我承認,我經常看到從大的單體到真正小的服務的“矯枉過正”。設計這些小服務是受已有的規範化的數據視圖啓發和驅動的。這種識別服務邊界的方法幾乎總會圍繞創建、讀取、修改和刪除資源產生大量的弱服務,從而形成“寒武紀生命大爆炸”。對於微服務架構的新手來說,這會產生一個高摩擦的環境,最終導致無法使那些服務進行獨立發佈和運行。這會創建一個難於調試的分佈式系統,一個跨事務因而難於保持一致性的分佈式系統,一個對於相對組織運維成熟度來說過於複雜的系統。儘管有一些關於微服務應該多“微”的探討,如團隊的大小,重寫服務的時間以及什麼樣的行爲必須封裝等,我的建議是微服務的大小取決於交互運維團隊能夠獨立發佈,監控和運維多少服務。開始可以是圍繞邏輯域概念的大服務,當團隊運維就緒的話,再分拆成多個服務。

就分解在線零售系統來說,開發人員開始可以使“購買”服務既包含包含購物袋上下文功能也包含購買的功能,比如“結賬”。隨着他們能夠成立更小的團隊,能夠發佈大量的服務,他們可以把“購物袋”分離出一個單獨的服務。


使用Richardson Maturity Model L3(REST成熟度模型)和超鏈接來確保未來的服務拆分不會影響調用者。比如,調用者能夠發現如何結賬而不需要提前知道。

以原子性演進方式遷移

傳統的單體應用被分解成設計優美的微服務,然後讓單體應用消失的無影蹤的想法有點荒唐,可以說是不可取的。任何經驗豐富的工程師都可以分享一些關於嘗試遺留系統遷移和改進的故事。這些嘗試都是在抱着對最終完成過分樂觀的狀態下計劃和開始的,但大部分都是在足夠好的時間點被放棄了。這些長期努力的計劃之所以取消是因爲情況發生了一些大的變化,比如項目費用用完了;組織目標轉移到其他方向上了;支持的領導層離職了。所以現實的做法是如何讓單體應用踏上微服務之旅。我們稱之爲“架構原子演進式遷移”,這種方式的遷移每一步都是完整的,也是可以回撤。這一點,在我們談到針對整個架構的改善和服務解耦的增量迭代的方法時尤爲重要。每個遷移增量必須使我們更好地朝架構目標前進一步。借用“演進式架構”適應度函數說法,每一次原子性遷移之後,架構適應度函數應該產生一個更接近於架構目標的值。

讓我來使用一個例子描述下這點。 想象一下,微服務架構目標是提高開發人員修改整體系統,交付價值的速度。團隊決定基於OAuth2.0協議把終端用戶驗證功能解耦到一個獨立服務中。這個服務用來替換已存在的(老的架構中的)客戶端應用用戶驗證功能和作爲新的微服務架構中的用戶驗證。讓我們把這個演進式的增量稱之爲“驗證服務引入”。引入該服務的一個方式就是先完成這些步驟:

(1) 構建Auth服務, 實現OAuth 2.0協議。

(2)增加一條新的驗證路徑到單體系統的後端調用新構建的Auth服務,處理終端用戶請求。

假如團隊到此爲止,轉而去構建其他的服務和功能,這會使得整個架構處於熵增狀態。在這種情況下,有兩種方法驗證用戶,一種是通過新的OAuth 2.0,一種是通過老的用戶密碼/會話。這個時候,實際上團隊離快速變化的總的目標更遠了。任何新的單體代碼開發人員都需要處理兩條代碼路徑,增加了他們理解代碼的認知負擔,降低了修改測試代碼的速度。

然而,作爲我們的一個原子性演進單元,團隊可以增加下面的步驟:

(3)替換老的用戶驗證調用

(4)去除單體系統中老的用戶驗證代碼

至此,我們可以說團隊已經接近架構目標了。


單體拆分的一個原子單元包括:

拆分新的服務

重定向所有消費者(服務消費者)到新的用戶

移除單體系統中老的代碼

反模式:解耦新的服務,新的消費者使用新的服務,但老的代碼永遠不退休。

我經常發現團隊只構建新的功能但不讓老的代碼退休就結束一個從單體系統功能的遷移,然後宣佈全部完成。這就是上面描述的反模式。之所以存在這種情況的原因是,a)只關注引入一個新功能的短期好處;b)退休老的代碼所需要總的工作量和構建新服務的優先級有競爭。爲了做正確的事情,我們要儘可能的努力按原子性步驟去做。

使用這種遷移方法,我們可以把遷移之旅分成一段段更短的旅途,能夠安穩的停下,然後重新開始,確保整個旅行的順利,最終“消滅”單體。

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