(干貨)如何通過分解和增量更改將單體遷移到微服務(wù)?
作者 | Sam Newman、Leandro Guimar?es
譯者 | 平川
本文基于 Sam 在倫敦 QCon 大會上的演講記錄,由 Leandro Guimar?es 整理,并由 Sam 審閱。
在倫敦 QCon 大會上,我談到了單體分解模式以及我們?nèi)绾芜_成微服務(wù)。我喜歡把它們比作令人討厭的水母,因為它們是一種亂糟糟的實體,會刺痛甚至可能殺死我們。這在通常的企業(yè)微服務(wù)遷移中很常見。
許多組織正在經(jīng)歷某種數(shù)字化轉(zhuǎn)型。隨便看下當前的任何數(shù)字化轉(zhuǎn)型,我們都會發(fā)現(xiàn)微服務(wù)的身影。我們知道,數(shù)字化轉(zhuǎn)型是一件大事,因為現(xiàn)在任何機場候機室都有大型 IT 咨詢公司的廣告推銷數(shù)字化轉(zhuǎn)型,包括德勤、DXC、埃森哲等公司。微服務(wù)非常流行。
不過,在談及微服務(wù)時,我關(guān)注的是結(jié)果,而不是我們用來實現(xiàn)它們的技術(shù)。我們選擇微服務(wù)架構(gòu)的原因有很多,但我反復(fù)提到的一個原因是其獨立部署的屬性。有一個功能,一個我們想要改變系統(tǒng)行為的更改。我們想要盡快實現(xiàn)這個更改。
圖 1:微服務(wù)方法示意圖
將微服務(wù)架構(gòu)與單體做下比較。我們認為,單體是一個單一的、無法透視的塊,我們無法對它作出任何更改。單體被認為是我們生活中最糟糕的東西,是難以擺脫的沉重負擔。我認為這非常不公平。最終,“單體”一詞在過去兩三年里取代了我們之前使用的“遺留問題(legacy)”一詞。這是一個根本性問題,因為有些人開始將單體視為遺留問題,是需要移除的東西。我認為這非常不合適。
單體有多種形式和規(guī)模。在討論單體應(yīng)用程序時,我主要是將單體作為部署單元來討論。考慮下經(jīng)典的單體,它是將所有代碼打包在單個進程中。它可能是 Tomcat 中的一個 WAR 文件,也可能是一個基于 PHP 的應(yīng)用程序,所有代碼都打包在一個可部署單元中,該單元會與數(shù)據(jù)庫通信。
這種單體類型可以看作是一個簡單的分布式系統(tǒng)。分布式系統(tǒng)是由多臺通過非本地網(wǎng)絡(luò)相互通信的計算機組成的系統(tǒng)。在這種情況下,所有的代碼都打包在一個進程中,重要的是,所有數(shù)據(jù)都保存在于一個運行在不同機器上的大型數(shù)據(jù)庫中。把所有數(shù)據(jù)都放在一個數(shù)據(jù)庫中,將來會給我們帶來很多痛苦。
圖 2:模塊化單體
我們還可以考慮下單進程單體的一種變體,稱為模塊化單體。這種模塊化單體使用了關(guān)于結(jié)構(gòu)化編程的前沿思想(誕生于 20 世紀 70 年代初,幾十年后,我們中的一些人仍在努力掌握這些思想!)。如圖 2 所示,我們將單進程單體應(yīng)用程序分解為模塊。如果我們正確地劃分了模塊邊界,我們就可以獨立地處理每個模塊。但是,本質(zhì)上,部署過程仍然是靜態(tài)鏈接的方法:我們必須鏈接好所有模塊才能進行部署。比如一個 Ruby 應(yīng)用程序,它由許多 GEM 文件、NuGet 包或通過 Maven 組裝的 JAR 文件組成。
雖然我們?nèi)匀皇菃误w部署,但模塊化單體有一些顯著的好處。把代碼分解成模塊確實可以讓我們在一定程度上獨立地完成工作。它可以方便不同的團隊一起工作,并處理系統(tǒng)的不同方面。我認為這是一個被嚴重低估的選項。這其中存在的問題是,人們往往不善于定義模塊邊界——更確切地說,即使他們擅長定義模塊邊界,他們也不擅長保持這些邊界。遺憾的是,結(jié)構(gòu)化編程或模塊化的概念往往會遭遇“泥球”問題。
對于我服務(wù)過的許多組織來說,使用模塊化單體比使用微服務(wù)架構(gòu)會更好。在過去的三年里,我對我一半的客戶說過:“微服務(wù)不適合你。“有些客戶甚至聽了我的話。對于它們中的許多來說,一種可以定義模塊邊界的好方法就足以滿足它們的需要。他們可以得到一個比較簡單的分布式系統(tǒng),以及一定程度上獨立、自主地工作。

圖 3:模塊化單體的一種變體
模塊化單體也有變體。圖 3 看起來有點奇怪,但這是我多次提出的建議,特別是對于初創(chuàng)公司,我通常認為,他們最好不要著急上微服務(wù)。如圖 3 所示,我們使用了模塊化單體,并將后臺單個的整體數(shù)據(jù)庫進行了分解,這樣就可以單獨存儲和管理每個模塊的數(shù)據(jù)。
雖然這看起來很奇怪,但歸根結(jié)底這是一種對沖架構(gòu)。人們認識到,分解單體架構(gòu)時最困難的工作之一是處理數(shù)據(jù)層。如果我們能提前設(shè)計好與這些模塊相關(guān)聯(lián)的獨立數(shù)據(jù)庫,以后遷移到單獨的微服務(wù)就會更容易。如果我正在處理模塊 C,我對與模塊 C 關(guān)聯(lián)的數(shù)據(jù)具有完全的所有權(quán)和控制權(quán)。當模塊 C 變成一個單獨的服務(wù)時,遷移它應(yīng)該會更容易。
當我還在 ThoughtWorks 工作時,我的一位老同事 Peter Gillard-Moss 第一次向我展示了這種模式。這是他為我們正在開發(fā)的一個內(nèi)部系統(tǒng)設(shè)計的。他說,“我覺得這能行。我們不確定我們是否想要提供服務(wù),所以也許它應(yīng)該是一個單體。”
我說,“試一試。看看會發(fā)生什么。“大約 6 年過去了,去年我和 Peter 談過,ThoughtWorks 仍然沒有改變架構(gòu)。它仍然運行得很歡快。他們讓不同的人處理不同的模塊,即使是在這個級別上將數(shù)據(jù)分離開來,也給他們帶來了巨大的好處。

圖 4:分布式單體
現(xiàn)在,我們來看看最糟糕的單體——分布式單體。我們的應(yīng)用程序代碼現(xiàn)在運行在彼此通信的獨立進程上。不管出于什么原因,我們都必須將整個系統(tǒng)作為一個單元同步部署。經(jīng)常,這種情況的出現(xiàn)是因為我們弄錯了服務(wù)邊界。我們將業(yè)務(wù)邏輯胡亂地放在了不同的層上。我們沒有遵從關(guān)于耦合和內(nèi)聚的要點,現(xiàn)在,我們的結(jié)賬邏輯分布在服務(wù)棧中 15 個不同的地方。我們要做任何工作,都必須協(xié)調(diào)多個團隊。如果組織中存在大量的橫切更改,通常表明組織邊界或服務(wù)邊界定義的不對。
分布式單體的問題在于,它本質(zhì)上是一個更加分布式的系統(tǒng),但是對于所有相關(guān)的設(shè)計、運行和操作挑戰(zhàn),我們?nèi)匀恍枰獑误w需要的那些協(xié)調(diào)活動。我想在線部署,但我不能。我必須等你完成更改,但你也完不成,因為你在等別人。現(xiàn)在,我們一致同意:“好吧,7 月 5 日,我們將一起上線。每個人都準備好了嗎?三、二、一,部署。“當然,一切都很順利。對于這類系統(tǒng),我們從來沒有遇到過任何問題。
如果一個組織有一位全職的發(fā)布協(xié)調(diào)經(jīng)理或這方面的其他職位,那么他們可能有一個分布式單體。協(xié)調(diào)分布式系統(tǒng)的同步部署一點都不好玩。我們最終會付出更高的更改成本。部署的范圍會大很多,可能會有更多的地方出錯。這種難以避免的協(xié)調(diào)活動,不只存在于發(fā)布活動中,而且存在于一般部署活動中。
稍微看下精益生產(chǎn)的內(nèi)容就會發(fā)現(xiàn),減少交接是優(yōu)化生產(chǎn)力的關(guān)鍵。等待別人為我做點什么只會產(chǎn)生浪費。這會導(dǎo)致生產(chǎn)力瓶頸。為了更快地交付軟件,減少交接和協(xié)調(diào)非常關(guān)鍵。遺憾的是,分布式單體往往會創(chuàng)造出不得不進行協(xié)調(diào)的環(huán)境。
有時,我們的問題不在于服務(wù)邊界在哪里。有時,它完全是始于我們開發(fā)軟件的方式。有些人從根本上誤解了發(fā)布序列。發(fā)布序列一直被認為是一種治療性的發(fā)布技術(shù),而不是一種進取性的活動。我們會選擇像發(fā)布序列這樣的東西來幫助組織轉(zhuǎn)向持續(xù)交付。發(fā)布序列的概念以定期為基礎(chǔ),也許每四周,軟件的所有部分已經(jīng)準備就緒。如果軟件還沒有準備好,它就會被推遲到下一個發(fā)布序列。對許多組織來說,這是向前邁出了一步。我們應(yīng)該縮小發(fā)布序列之間的間隔,最終完全消除。然而,有太多的組織在采用了發(fā)布序列就再也沒有繼續(xù)前進。
當若干團隊都朝著同一個發(fā)布序列而努力時,所有已經(jīng)準備好的軟件都會在這個發(fā)布序列中交付——突然之間,我們會一次性部署大量的服務(wù)。這是真正的問題所在。當實踐發(fā)布序列時,最重要的一件事是,至少要將這些發(fā)布序列分解,使它們成為團隊發(fā)布序列。允許不同的團隊安排自己的發(fā)布序列。最終,我們應(yīng)該拋棄這些序列。它們應(yīng)該只是邁向持續(xù)交付的一個步驟。
遺憾的是,一些營銷敏捷的優(yōu)秀成果已經(jīng)將發(fā)布序列作為交付軟件的最終方式。我們知道他們已經(jīng)這么做了,因為許多公司組織里掛著的 SAFe 圖解上都印著“發(fā)布序列”的字樣。這不是好事。不管是對于 SAFe,還是你遇到的任何其他問題,發(fā)布序列始終都是一種補救技術(shù),是自行車的輔助輪。我們應(yīng)該向著持續(xù)交付繼續(xù)前進。問題是,如果我們使用這些發(fā)布序列時間太長,最終的架構(gòu)就會是一個分布式單體,因為我們已經(jīng)習(xí)慣了將所有服務(wù)部署在一起。要注意這一點。這可能不會在一夜之間發(fā)生。我們可以從支持獨立部署的架構(gòu)開始,但如果我們使用發(fā)布序列太久,我們的架構(gòu)就會開始圍繞這些發(fā)布實踐聚合在一起。
歸根結(jié)底,分布式單體是一個問題,因為它同時具有分布式系統(tǒng)的所有復(fù)雜性和單個部署單元的缺點。我們應(yīng)該跨越它,尋找更好的工作方式。分布式單體是一件很棘手的事情,關(guān)于如何處理這種情況的建議已經(jīng)有很多。有時,正確的答案是將其合并回單進程單體。但是,如果現(xiàn)如今我們有一個分布式單體,最好的辦法是弄清楚為什么我們會有這樣的單體,并且在添加任何新服務(wù)之前,著手讓架構(gòu)中的某些部分可以獨立部署。在這種情況下,添加新服務(wù)很可能會增加我們開展工作的難度。
我們使用微服務(wù)架構(gòu)是因為它具有獨立部署的特性。我們希望能夠在不改變其他任何東西的情況下將服務(wù)的更改部署到產(chǎn)品中。這是微服務(wù)的黃金法則。在演講或文章中,這似乎很容易。在現(xiàn)實生活中,要做到這一點要困難得多,尤其是考慮到大多數(shù)人并非從零開始。絕大多數(shù)人都覺得他們的系統(tǒng)太大了,想把它分成更小的部分。他們想知道從哪里開始。
領(lǐng)域驅(qū)動設(shè)計(DDD)有一些很好的方法可以幫助我們找出服務(wù)邊界。當與研究微服務(wù)遷移的組織合作時,我們通常是從在現(xiàn)有的單體應(yīng)用程序架構(gòu)上執(zhí)行 DDD 建模練習(xí)開始。我們這樣做是為了弄清楚單體應(yīng)用內(nèi)部發(fā)生了什么,并從業(yè)務(wù)域的角度確定工作單元。
盡管單體看起來像一個巨大的盒子,但當我們應(yīng)用 DDD,并將邏輯模型投射到該單體上時,我們意識到,其內(nèi)部被組織成訂單管理、PDF 渲染、客戶端通知等內(nèi)容。雖然代碼可能沒有圍繞這些概念進行組織,但從用戶或業(yè)務(wù)領(lǐng)域模型的角度來看,這些概念存在于代碼中。這些業(yè)務(wù)領(lǐng)域的邊界(DDD 中通常稱為“有界上下文”)就成為我們分解的單元,原因我這里就不展開討論了。

圖 5:找出單體中的分解和依賴項單元
首先要做的是問下從哪里開始,什么事情可以優(yōu)先處理,我們的工作單元是什么。在圖 5 所示的初始單體中,我們有訂單管理、發(fā)票和通知。DDD 建模練習(xí)將使我們了解它們之間的關(guān)系。但愿我們能得出一個有向無環(huán)圖,來描述這些不同功能之間的依賴關(guān)系。(如果我們得到是一個依賴關(guān)系的循環(huán)圖,我們就得做更多的工作。)我們可以看到,在這個單體中,有很多東西都依賴于向客戶發(fā)送通知的能力。那似乎是領(lǐng)域的核心部分。
我們可以開始問問題了,比如我們應(yīng)該先提取什么。我可以完全從這個角度來看問題。我們可能會看到,通知被很多功能使用——如果微服務(wù)更好,那么提取系統(tǒng)中很多部分都在使用的東西將使更多的東西變得更好。也許我們應(yīng)該從那里開始。但是,看看所有的入站依賴關(guān)系。因為有太多的部分需要通知功能,所以我們很難將其從現(xiàn)有的單體架構(gòu)中剝離出來。在這個單體系統(tǒng)中,像結(jié)賬或訂單管理之類的概念似乎更加獨立。它們可能是更容易分解的東西。決定從哪一部分開始,從根本上講是一種漸進式分解方法。
首先要記住,單體并不是敵人。我希望大家都好好地思考一下。人們將任何單體系統(tǒng)都視為問題。在過去幾年里,我看到的最令人擔憂的事情之一是,微服務(wù)現(xiàn)在似乎成了許多人的默認選擇。
有人可能記得一句老話:“沒有人會因為購買 IBM 產(chǎn)品而被解雇。”意思是,因為其他人都在買 IBM 產(chǎn)品,你也可以買——如果你買的東西不適合你,那也不是你的錯,因為大家都在這么做。你沒必要冒險。現(xiàn)在每個人都在做微服務(wù),我們也面臨同樣的問題。每個人都吵著要做微服務(wù)。這對我很有好處:我寫關(guān)于該主題的書,但對你可能不是好事。
從根本上說,這取決于我們想要解決什么問題。我們想要達到而在當前的架構(gòu)下無法達到的目標是什么?也許微服務(wù)是答案,或者其他什么東西才是答案。理解我們想要達到的目標至關(guān)重要,否則,我們將很難確定如何遷移我們的系統(tǒng)。我們正在做的事情將改變我們分解系統(tǒng)的方式,以及我們?nèi)绾未_定工作的優(yōu)先級。
微服務(wù)遷移不像一個開關(guān),沒有開 / 關(guān)切換。這更像是轉(zhuǎn)動一個旋鈕。在采用微服務(wù)的過程中,我們轉(zhuǎn)動一下旋鈕,增加一兩個服務(wù)。我們想看看一個服務(wù)如何發(fā)揮作用,它是否提供了我們需要的東西,是否解決了我們的問題。如果是,而且我們也滿意,我們就可以繼續(xù)轉(zhuǎn)動旋鈕。
不過,我看到很多人都會轉(zhuǎn)動旋鈕,增加 500 項服務(wù),然后插上耳機,檢查音量。這是讓鼓膜破裂的好方法。我們不知道我們將要面對什么問題,那些問題在開發(fā)人員的筆記本電腦上碰不到。它們會在生產(chǎn)環(huán)境中出現(xiàn)。當我們從提供一個單體系統(tǒng)轉(zhuǎn)為一次性提供 500 個服務(wù)時,所有的問題都會同時出現(xiàn)。不管我們最后是提供一項、兩項還是五項服務(wù),還是像 Monzo 那樣擁有 800 項或 1500 項服務(wù),我們都必須從一個小轉(zhuǎn)變開始。我們需要選擇一些服務(wù)來啟動遷移。讓它們在生產(chǎn)環(huán)境中運行,積累經(jīng)驗,并盡快把這種經(jīng)驗付諸實踐。通過逐步調(diào)整,以漸進的方式創(chuàng)建和發(fā)布新的微服務(wù),我們可以更好地發(fā)現(xiàn)和處理出現(xiàn)的問題。每個項目將要面對的問題都會有所不同,這取決于許多不同的因素。
我們想要從單體系統(tǒng)中提取一些功能,讓它與單體系統(tǒng)的剩余部分通信并集成,并且要盡快完成。我們不想再進行大爆炸式的重寫了。我們過去是每年向用戶發(fā)布軟件,因為有一個為期 12 個月的窗口期,所以我們可以這樣說:“現(xiàn)有的系統(tǒng)太糟糕了,現(xiàn)在已經(jīng)無法使用了,但是我們還有 12 個月的時間來發(fā)布下一個版本。如果我們努力,完全可以重寫系統(tǒng),我們不會再犯過去犯過的錯誤,現(xiàn)有的功能一個都不會少,而且還會有更多的新功能,一切都會很好。”
當每年發(fā)布一次軟件時,我們從來沒有那樣做。當人們期望每月、每周或每天發(fā)布軟件時,我不知道該如何證明其合理性。套用 Martin Fowler 的話來說,“如果你要進行大爆炸式的重寫,你唯一能確定的就是大爆炸。”我喜歡動作片中的爆炸場面,但不喜歡我的 IT 項目里出現(xiàn)這種情況。我們需要從不同的角度思考如何做出這些更改。
我是架構(gòu)增量演進的忠實擁護者。我們不應(yīng)該認為我們的架構(gòu)是一成不變的。我們需要有一些模式來幫助我們以漸進的方式向微服務(wù)轉(zhuǎn)變。
我們首先看下應(yīng)用程序模式 Strangler Fig,它以一種植物命名,這種植物在樹冠上生根,然后卷須向下纏繞在樹干上。絞殺榕(strangler fig)靠自身無法爬到林冠層以獲得足夠的陽光,所以它不像普通樹木一樣從一棵小樹苗慢慢長大,而是包裹在現(xiàn)有的植物體上。它依賴于現(xiàn)有的樹的高度和力量。隨著時間的推移,這些絞殺榕成長起來,變得越來越大,能夠獨立生存了。如果下面的樹死了,腐爛了,就只剩下絞殺榕和一根空心的柱子。這些東西看起來就像蠟滴在其他樹上——看起來真的很令人不安。
但是作為應(yīng)用程序遷移策略的一種模式,這種思想是有用的。我們找一個現(xiàn)有的系統(tǒng)(它完成我們想要它做的所有事情,即現(xiàn)有的單體應(yīng)用程序),然后開始圍繞它封裝出我們的新系統(tǒng)。在這里,就是我們的微服務(wù)架構(gòu)。實現(xiàn) Strangler Fig 應(yīng)用程序有兩個關(guān)鍵。第一個是資產(chǎn)捕獲,即確定把哪些功能遷移到微服務(wù)架構(gòu)的過程。然后我們需要進行轉(zhuǎn)接。以前對于單體應(yīng)用程序的調(diào)用得轉(zhuǎn)接到新功能上。如果功能沒有遷移,調(diào)用就不需要轉(zhuǎn)接;非常簡單。
有些人對如何轉(zhuǎn)移功能感到困惑。如果我們真的夠幸運的話,也許可以簡單地復(fù)制代碼。如果結(jié)賬服務(wù)的代碼在單體代碼庫中一個叫“結(jié)賬”的漂亮盒子中,我們就可以剪切并粘貼到新服務(wù)中。我認為,如果代碼庫是這種狀態(tài),那你可能不需要任何幫助。更大的可能是,我們將不得不快速瀏覽系統(tǒng),設(shè)法收集所有與結(jié)賬相關(guān)的代碼。我們可能會做一些重構(gòu)前的活動。也許我們可以重用這些代碼,但在這種情況下,那將是復(fù)制粘貼,而不是剪切粘貼。我們想把這個功能留在這個單體應(yīng)用中,原因我將在后面討論。更常見的情況是,人們會進行一些重寫。
實現(xiàn) Strangler Fig 的方法有很多種。讓我們來看一種簡單的方法。
假設(shè)我們有一個基于 HTTP 的單體系統(tǒng)。這可能是一個無頭應(yīng)用程序。我們可以在用戶界面的后臺使用 API Boundary 攔截調(diào)用。我們需要的是可以將調(diào)用重定向的東西,因此,我們將使用某種 HTTP 代理。對于這類架構(gòu),HTTP 協(xié)議非常有效,這是因為它非常適合透明地重定向調(diào)用。通過 HTTP 發(fā)起的調(diào)用可以被轉(zhuǎn)接到許多不同的地方。有很多軟件可以幫你做到這一點,而且非常簡單。

圖 6:HTTP 代理攔截對單體的調(diào)用,增加了一個網(wǎng)絡(luò)躍點
首先要做的是,在上游流量和下游單體系統(tǒng)之間放置一個代理,別的什么都不用做。我們將把這個代理部署到生產(chǎn)環(huán)境中。此時,它還沒有轉(zhuǎn)接任何調(diào)用。我們可以看下它在生產(chǎn)環(huán)境中是否有效。我們要擔心的一件事是網(wǎng)絡(luò)質(zhì)量,因為我們增加了一個網(wǎng)絡(luò)躍點。通常是直接調(diào)用單體系統(tǒng),但現(xiàn)在通過我們的代理。在這種情況下,延遲是殺手。通過代理轉(zhuǎn)接只會給現(xiàn)有的調(diào)用增加幾毫秒的開銷——少于 10 毫秒就很棒。如果額外增加一個網(wǎng)絡(luò)躍點增加了 200 毫秒的延遲,我們就需要暫停微服務(wù)遷移,因為我們還有其他需要首先解決的大問題。
準備好代理之后,我們接下來將處理新的結(jié)賬服務(wù)。我們將其部署到生產(chǎn)環(huán)境中。即使它功能還不全,也沒什么問題,因為它還沒有被使用。我們要在腦海中將部署到生產(chǎn)環(huán)境和使用這兩個概念分開。開始采用微服務(wù)后,我們希望定期地將功能部署到生產(chǎn)環(huán)境中,以確保我們的部署機制能夠正常工作。在添加功能時,我們可以單獨測試新服務(wù)。我們還沒有把它發(fā)布給用戶,但它已經(jīng)在生產(chǎn)環(huán)境中了。我們可以將它連接到我們的儀表板上,確保日志聚合正常,或者做其他我們想做的事。
關(guān)鍵是我們只對一個服務(wù)進行操作。我們甚至可以將那個服務(wù)的提取過程分解為許多小步驟:創(chuàng)建服務(wù)框架、實現(xiàn)方法、在生產(chǎn)環(huán)境中測試它,然后部署發(fā)布版本。準備就緒之后,當我們認為新實現(xiàn)已經(jīng)等同于舊系統(tǒng)時,我們只需重新配置代理,將調(diào)用從舊的單體功能轉(zhuǎn)到新的微服務(wù)。
事到如今,你可能認為現(xiàn)在應(yīng)該從這個單體中刪除舊功能。先別這么做!如果新創(chuàng)建的微服務(wù)在生產(chǎn)環(huán)境中出現(xiàn)問題,我們有一種非常快的補救技術(shù):我們只需還原代理配置,將流量轉(zhuǎn)到原功能所在的單體上。不過,要想實現(xiàn)這一點,我們必須考慮數(shù)據(jù)的作用——這點我們稍后討論。
我們希望,將這個功能提取成微服務(wù)是一種真正的重構(gòu),改變代碼的結(jié)構(gòu)而不是行為。在功能上,微服務(wù)應(yīng)該等同于單體中的同一功能。我們應(yīng)該能夠在它們之間切換,直到微服務(wù)正常工作為止。
如果我們想要保留切換的能力,那么在遷移完成、我們不再需要這種切換能力之前,我們就不應(yīng)該添加新的功能或更改現(xiàn)有的功能。
在很多情況下,這項簡單的 Strangler Fig 技術(shù)都出奇的好。這個例子使用了 HTTP,但是我也看過使用 FTP 的情況。我已經(jīng)用消息攔截器做到了這一點。我在上傳固定文件時就這么做了:我們插入固定文件,在新服務(wù)中剔除我們希望去掉的內(nèi)容,然后把剩下的內(nèi)容傳遞下去。

圖 7:微服務(wù)架構(gòu)的有向無環(huán)圖
Strangler Fig 對于結(jié)賬或訂單管理等功能非常有效,這些功能在我們的調(diào)用堆棧中處于更高的位置,如圖 7 所示的依賴關(guān)系圖。但是,進入單體系統(tǒng)的調(diào)用沒有哪個是為了獲得像忠誠獎勵積分或給客戶發(fā)送通知這樣的能力。進入單體的調(diào)用是“下訂單”或“付款”。只是作為這些操作的副作用,我們可能會獎勵積分或發(fā)送電子郵件。因此,我們無法在單體系統(tǒng)的外圍攔截對忠誠獎勵積分或通知的調(diào)用。那是在單體系統(tǒng)內(nèi)部完成的。
假設(shè)我們要把通知功能提取出來。我們必須提取這塊功能,并用一種增量的方式攔截這些入站鏈接,這樣我們就不會破壞系統(tǒng)的其余部分。
一種名為“抽象分支”的技術(shù)可以很好地完成這項工作。在基于主干的開發(fā)環(huán)境中,抽象分支是一種經(jīng)常討論的模式,這是一種很好的軟件開發(fā)方式。在這種情況下中,抽象分支作為一種模式也很有用。我們在現(xiàn)有的單體系統(tǒng)中創(chuàng)建了一個空間,同一功能的兩種實現(xiàn)可以在其中共存。在很多方面,這是里氏替換原則的一個例子。這是對完全相同的抽象做的一個獨立實現(xiàn)。對于本例,我們將從現(xiàn)有代碼中提取通知功能。

圖 8:用于遷移到一個微服務(wù)的抽象分支
通知代碼散布在我們的系統(tǒng)中。我們要做的第一件事是為新服務(wù)收集所有這些代碼。我們將把服務(wù)隱藏在抽象點后面。我們希望結(jié)賬代碼和訂單代碼通過一個明確的抽象點來訪問這個功能。起初,我們有一個通知抽象的實現(xiàn)——它封裝了單體中當前所有與通知相關(guān)的功能。我們的所有調(diào)用——到 SMTP 庫、到 Twilio、發(fā)送 SMS——都被打包到這個實現(xiàn)中。
此時,我們所做的只是在代碼中創(chuàng)建了一個很好的抽象點。我們可以停了。我們已經(jīng)厘清了我們的代碼庫,并使其更容易測試,這已經(jīng)是改進了。這是一種很好的老式重構(gòu)。我們也創(chuàng)造了一個機會來更改結(jié)賬或訂單使用的通知功能的實現(xiàn)。我們可以用幾天或幾周的時間來完成這項重構(gòu)工作,同時做一些其他的事情,比如實際發(fā)布特性。
接下來,我們開始創(chuàng)建通知服務(wù)的新實現(xiàn)。這可以分成兩個部分。我們已經(jīng)在單體中實現(xiàn)了新接口,但這只是調(diào)用另一部分(新建的通知微服務(wù))的客戶端代碼。部署這些實現(xiàn)很安全,因為它們還沒有被使用。我們更頻繁地集成代碼,減少合并工作,并確保一切工作正常。
一旦單體內(nèi)部調(diào)用新服務(wù)的代碼和單體外部的通知服務(wù)可以正常工作,我們所需要做的就是切換我們正在使用的抽象實現(xiàn)。我們可以使用特性開關(guān)、文本文件、專用工具,或者任何我們希望使用的方式。我們還沒有刪除舊功能,所以如果有問題,我們可以輕松切回舊功能。同樣,這個服務(wù)的遷移被分解成許多小步驟,我們試圖通過所有這些步驟盡快將其部署到生產(chǎn)環(huán)境。
一切工作正常之后,我們就可以選擇清理代碼了。如果不再需要這個功能,我們可以刪除其特性標識,甚或刪除舊代碼。現(xiàn)在刪除舊代碼很容易了,因為我們已經(jīng)花了一些時間將所有代碼整理好。我們刪除了那個類,它消失了。我們把單體變小了,每個人都對自己感到滿意。
就代碼重構(gòu)而言,我強烈推薦 Michael Feathers 的著作《修改代碼的藝術(shù)》。他對遺留代碼的定義是沒有測試代碼的代碼。關(guān)于如何在不破壞現(xiàn)有系統(tǒng)的情況下,在代碼庫中找出并創(chuàng)建這些抽象,這本書提供了很多好主意。即使你不使用微服務(wù),僅僅創(chuàng)建這個抽象點就可能會使你的代碼處于更好、更可測試的狀態(tài)。
我已經(jīng)強調(diào)過,不要太早刪除舊實現(xiàn)。保留兩種實現(xiàn)有很多好處。它為我們?nèi)绾尾渴鸷蜕暇€軟件提供了有趣的方法。當調(diào)用進入抽象點時,它可以觸發(fā)對這兩個實現(xiàn)的調(diào)用。這叫做并行運行。這可以幫助我們確保新的微服務(wù)實現(xiàn)功能上等價。我們運行該功能的兩個副本,然后比較結(jié)果。
要做這個比較,只需運行這兩個實現(xiàn)并比較結(jié)果。我們必須指定其中之一作為真相來源,因為我們不希望把兩者串聯(lián)起來:例如,在發(fā)送通知時,我們只想發(fā)送一封郵件,結(jié)果卻發(fā)了兩封。并行運行是一種實用而直接的實時比較,不僅是功能等價性的比較,而且包含可接受的非功能等價性比較。我們不僅要測試是否創(chuàng)建了正確的電子郵件,并將其發(fā)送到正確的虛擬 SMTP 服務(wù)器,而且還要測試新服務(wù)的響應(yīng)速度是否同樣快,或者錯誤率在可接受范圍之內(nèi)。
通常,我們信任舊的功能實現(xiàn),并使用其結(jié)果。我們將它們并行運行一段時間,如果新實現(xiàn)提供了可接受的結(jié)果,我們最終將處理掉舊的。
GitHub 可以幫我們做這件事。他們創(chuàng)建了一個名為 GitHub Scientist 的庫,這是一個很小的 Ruby 庫,用于封裝不同的抽象并對它們進行評分。在重構(gòu)應(yīng)用程序中的關(guān)鍵代碼路徑時,我們可以使用它來進行實時比較。GitHub Scientist 已經(jīng)被移植到了很多不同的語言上,令人費解的是,Perl 有三種不同的移植:顯然,在 Perl 社區(qū),并行運行是一件很重要的事情。關(guān)于如何在應(yīng)用程序內(nèi)部并行運行,已經(jīng)有很多很好的建議。
從根本上說,我們需要將部署的概念與發(fā)布的概念分離開來。傳統(tǒng)上,我們認為這兩種活動是一回事,部署軟件和向生產(chǎn)環(huán)境用戶發(fā)布軟件是一回事。這就是為什么每個人都害怕生產(chǎn)環(huán)境會發(fā)生什么事情,這就是生產(chǎn)環(huán)境成為一個封閉環(huán)境的原因。
我們可以把這兩個概念分開。將某樣?xùn)|西部署到生產(chǎn)環(huán)境中與將它發(fā)布給我們的用戶是不一樣的。這個想法是人們現(xiàn)在所說的“漸進式交付”的基礎(chǔ),這是一個涵蓋了一系列不同技術(shù)的總稱,包括金絲雀發(fā)布、藍 / 綠部署、抹黑啟動等。我們可以快速推出軟件,但不必向任何客戶公開。我們可以把它放到生產(chǎn)環(huán)境中,在那里測試,然后自己承受出現(xiàn)的任何問題。
如果我們將部署與發(fā)布分開,那么部署的風險就會小很多。我們就會更加勇敢地進行更改。我們將能夠更頻繁地發(fā)布,而且發(fā)布的風險將更低。
RedMonk 聯(lián)合創(chuàng)始人 James Governor 在公司的博客上對漸進式交付做了很好的闡述。該文探討了漸進式交付,其中最重要的結(jié)論是,主動部署與主動發(fā)布不是一回事,并且你可以控制發(fā)布活動如何發(fā)生。
我們將現(xiàn)有的單體應(yīng)用程序和數(shù)據(jù)鎖定在系統(tǒng)中,如圖 9 所示。我們已經(jīng)決定提取結(jié)賬功能,但是它需要訪問數(shù)據(jù)。

圖 9:從新服務(wù)訪問舊數(shù)據(jù)
選項一是直接訪問單體的數(shù)據(jù)。如果我們?nèi)匀辉跍y試并在單體中的結(jié)賬功能和微服務(wù)里的結(jié)賬功能之間進行切換,我們會希望這兩種實現(xiàn)之間具有數(shù)據(jù)兼容性和一致性,這種方式可以保證這一點。這在短時間內(nèi)是可以接受的,但它違背了數(shù)據(jù)庫的黃金規(guī)則之一:不共享數(shù)據(jù)庫。這不是可以長期依賴的東西,因為它會導(dǎo)致根本性的耦合問題。我們希望保持獨立部署的能力。

圖 10:從新服務(wù)直接訪問舊數(shù)據(jù)
如圖 10 所示,我們有一個 Shipping 服務(wù)和數(shù)據(jù)庫,我們允許其他人訪問我們的數(shù)據(jù)。我們已經(jīng)向外部公開了內(nèi)部實現(xiàn)細節(jié)。這使得 Shipping 服務(wù)的開發(fā)人員很難知道哪些內(nèi)容可以安全地更改。哪些數(shù)據(jù)要共享,哪些數(shù)據(jù)要隱藏,并沒有做區(qū)分。
20 世紀 70 年代,David Parnas 提出了“信息隱藏”的概念,我們就是以此為基礎(chǔ)考慮模塊分解。我們希望在模塊或微服務(wù)的邊界內(nèi)隱藏盡可能多的信息。如果我們創(chuàng)建一個定義良好的服務(wù)接口來共享數(shù)據(jù),而不是直接公開數(shù)據(jù)庫,那么這個接口就讓 Shipping 服務(wù)的開發(fā)人員可以明確知道這個契約以及他們可以向外界公開什么。只要遵守該契約,開發(fā)人員就可以在 Shipping 服務(wù)中做任何他們想做的事情。也就是說,這些服務(wù)可以獨立演進和開發(fā)。不要直接訪問數(shù)據(jù)庫,除非是在極其特殊的情況下。
拋開直接訪問,我們有兩種選擇:要么訪問別人的數(shù)據(jù),要么保存自己的數(shù)據(jù)。對于這個例子,假如我們已經(jīng)確定新開發(fā)的結(jié)賬微服務(wù)已經(jīng)足夠好,可以作為我們的真相來源。
此時,如果我們想要使用別人的數(shù)據(jù),那么這可能意味著數(shù)據(jù)屬于單體,我們必須向單體請求數(shù)據(jù)。我們在單體上創(chuàng)建某種顯式的服務(wù)接口(在我們的示例中是一個 API),通過它獲取我們想要的數(shù)據(jù)。

圖 11:新開發(fā)的微服務(wù)使用單體顯式提供的一個服務(wù)接口
我們是結(jié)賬服務(wù),而不是訂單服務(wù),但我們可能需要訂單數(shù)據(jù)。訂單功能存在于單體中,因此我們將從那里獲取數(shù)據(jù)。這樣來說,我們需要在單體上定義服務(wù)接口以公開不同的數(shù)據(jù)集,而且,在這樣做時,我們可以看到其他實體從單體中顯現(xiàn)出來。我們可能會發(fā)現(xiàn),訂單服務(wù)正等待著從單體中噴發(fā)出來,就像《異形》中的異形幼體,在電影中,單體是由 John Hurt 扮演的,它會死去。
另一種選擇是保存服務(wù)自己的數(shù)據(jù)——在本例中,是單體數(shù)據(jù)庫中的結(jié)賬數(shù)據(jù)。至此,我們必須把數(shù)據(jù)移到一個結(jié)賬數(shù)據(jù)庫,這真的很難。從現(xiàn)有系統(tǒng)(尤其是關(guān)系數(shù)據(jù)庫)中提取數(shù)據(jù)會帶來很多麻煩。我們將看一個簡單的例子,看看它帶來的挑戰(zhàn)。我將帶大家深入了解一下,如何處理連接。

圖 12:在線銷售光盤的單體
圖 12 描述了一個現(xiàn)有的、在線銷售光盤的單體應(yīng)用程序。(你可以看出我這個例子已經(jīng)用了多久了。)Catalog 功能知道某些東西多少錢,并將信息存儲在 Line Items 表中。Finance 功能管理我們的財務(wù)交易,并將數(shù)據(jù)存儲在 Ledger 表中。我們要做的其中一件事是,生成一個每周銷量前 10 的專輯列表。在這種情況下,我們需要做一個簡單的連接操作。我們從 Ledger 表上查出 10 個最暢銷的。我們根據(jù)行和其他東西來限制這個查詢。這樣我們就能得到 ID 列表了。

圖 13:在線銷售光盤的微服務(wù)架構(gòu)
在進入微服務(wù)領(lǐng)域后,我們需要在應(yīng)用層執(zhí)行連接操作。我們從 Finance 數(shù)據(jù)庫中提取財務(wù)交易數(shù)據(jù)。關(guān)于我們出售的物品的信息則存在于 Catalog 數(shù)據(jù)庫中。為了生成銷量前 10 名列表,我們必須從 Ledger 表中提取最暢銷商品的 ID,然后轉(zhuǎn)到 Catalog 微服務(wù),查詢所銷售商品的信息。我們過去在關(guān)系層中執(zhí)行的連接操作轉(zhuǎn)移到了應(yīng)用程序?qū)印?/p>
延遲可能會變得令人震驚。現(xiàn)在,我不是做一個一次往返的連接操作,而是要調(diào)用 Finance 服務(wù)獲取銷量前 10 的 ID,然后調(diào)用另一個 Catalog 服務(wù)請求這 10 個 ID 的信息,然后 Catalog 服務(wù)從 Catalog 數(shù)據(jù)庫中取得這些 ID,然后我們才得到響應(yīng)。圖 13 說明了這個過程。

圖 14:微服務(wù)架構(gòu)會導(dǎo)致更多的躍點和延遲
我們還沒有涉及到像缺乏數(shù)據(jù)完整性這樣的問題(在這種情況下,關(guān)系型數(shù)據(jù)庫如何實現(xiàn)引用完整性)。
如果你想深入研究諸如處理延遲和數(shù)據(jù)一致性之類的問題,我在《從單體到微服務(wù)》一書中進行了深入的闡述。
無論你是否決定繼續(xù)自己的微服務(wù)遷移之旅,我都建議你仔細考慮下,自己正在做什么以及為什么要這樣做。不要把注意力都放在創(chuàng)建微服務(wù)上。相反,你要清楚自己想要達到的結(jié)果。你認為微服務(wù)會帶來什么結(jié)果?專注于這一點——你可能會發(fā)現(xiàn),你可以在不進入復(fù)雜的微服務(wù)世界的情況下實現(xiàn)同樣的結(jié)果。

(干貨)吐血總結(jié):技術(shù)大佬都是怎么學(xué)習(xí)的?

面試官:如何發(fā)現(xiàn) Redis 熱點 Key ,解決方案有哪些?
