如何通過分解和增量更改將單體遷移到微服務

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文要點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務遷移不是一個小更改。你必須搞清楚它是否真的能解決你的問題,否則你可能會創建一個會殺死你的、亂糟糟的實體。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單體有不同類型,其中一些可能是有效的,足以滿足業務需求。單體不是一個應該被殺死的敵人。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務關乎獨立部署。有一些分解和增量更改模式可以幫助你評估並遷移到微服務架構。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當你開始使用微服務時,你會意識到隨之而來的是一系列非常複雜的挑戰。所以不應該將微服務作爲默認選擇。你得仔細考慮它們是否適合你。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文基於Sam在倫敦QCon大會上的"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/presentations\/microservices-principles-patterns\/","title":null,"type":null},"content":[{"type":"text","text":"演講記錄"}]},{"type":"text","text":",由Leandro Guimarães整理,並由Sam審閱。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在倫敦QCon大會上,我談到了"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/presentations\/microservices-principles-patterns\/","title":null,"type":null},"content":[{"type":"text","text":"單體分解模式以及我們如何達成微服務"}]},{"type":"text","text":"。我喜歡把它們比作令人討厭的水母,因爲它們是一種亂糟糟的實體,會刺痛甚至可能殺死我們。這在通常的企業微服務遷移中很常見。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"許多組織正在經歷某種數字化轉型。隨便看下當前的任何數字化轉型,我們都會發現微服務的身影。我們知道,數字化轉型是一件大事,因爲現在任何機場候機室都有大型IT諮詢公司的廣告推銷數字化轉型,包括德勤、DXC、埃森哲等公司。微服務非常流行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,在談及微服務時,我關注的是結果,而不是我們用來實現它們的技術。我們選擇微服務架構的原因有很多,但我反覆提到的一個原因是其獨立部署的屬性。有一個功能,一個我們想要改變系統行爲的更改。我們想要儘快實現這個更改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f5\/f50c1390d79529401118c01495de858f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖1:微服務方法示意圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將微服務架構與單體做下比較。我們認爲,單體是一個單一的、無法透視的塊,我們無法對它作出任何更改。單體被認爲是我們生活中最糟糕的東西,是難以擺脫的沉重負擔。我認爲這非常不公平。最終,“單體”一詞在過去兩三年裏取代了我們之前使用的“遺留問題(legacy)”一詞。這是一個根本性問題,因爲有些人開始將單體視爲遺留問題,是需要移除的東西。我認爲這非常不合適。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單體的類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單體有多種形式和規模。在討論單體應用程序時,我主要是將單體作爲部署單元來討論。考慮下經典的單體,它是將所有代碼打包在單個進程中。它可能是Tomcat中的一個WAR文件,也可能是一個基於PHP的應用程序,所有代碼都打包在一個可部署單元中,該單元會與數據庫通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種單體類型可以看作是一個簡單的分佈式系統。分佈式系統是由多臺通過非本地網絡相互通信的計算機組成的系統。在這種情況下,所有的代碼都打包在一個進程中,重要的是,所有數據都保存在於一個運行在不同機器上的大型數據庫中。把所有數據都放在一個數據庫中,將來會給我們帶來很多痛苦。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/34\/3495c6606214dd622e5ffb592c73d05a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖2:模塊化單體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還可以考慮下單進程單體的一種變體,稱爲模塊化單體。這種模塊化單體使用了關於結構化編程的前沿思想(誕生於20世紀70年代初,幾十年後,我們中的一些人仍在努力掌握這些思想!)。如圖2所示,我們將單進程單體應用程序分解爲模塊。如果我們正確地劃分了模塊邊界,我們就可以獨立地處理每個模塊。但是,本質上,部署過程仍然是靜態鏈接的方法:我們必須鏈接好所有模塊才能進行部署。比如一個Ruby應用程序,它由許多GEM文件、NuGet包或通過Maven組裝的JAR文件組成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然我們仍然是單體部署,但模塊化單體有一些顯著的好處。把代碼分解成模塊確實可以讓我們在一定程度上獨立地完成工作。它可以方便不同的團隊一起工作,並處理系統的不同方面。我認爲這是一個被嚴重低估的選項。這其中存在的問題是,人們往往不善於定義模塊邊界——更確切地說,即使他們擅長定義模塊邊界,他們也不擅長保持這些邊界。遺憾的是,結構化編程或模塊化的概念往往會遭遇“泥球”問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於我服務過的許多組織來說,使用模塊化單體比使用微服務架構會更好。在過去的三年裏,我對我一半的客戶說過:“微服務不適合你。“有些客戶甚至聽了我的話。對於它們中的許多來說,一種可以定義模塊邊界的好方法就足以滿足它們的需要。他們可以得到一個比較簡單的分佈式系統,以及一定程度上獨立、自主地工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2b\/2bc6c3d62e43fbdf21047a167a6f538c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖3:模塊化單體的一種變體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊化單體也有變體。圖3看起來有點奇怪,但這是我多次提出的建議,特別是對於初創公司,我通常認爲,他們最好不要着急上微服務。如圖3所示,我們使用了模塊化單體,並將後臺單個的整體數據庫進行了分解,這樣就可以單獨存儲和管理每個模塊的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然這看起來很奇怪,但歸根結底這是一種對沖架構。人們認識到,分解單體架構時最困難的工作之一是處理數據層。如果我們能提前設計好與這些模塊相關聯的獨立數據庫,以後遷移到單獨的微服務就會更容易。如果我正在處理模塊C,我對與模塊C關聯的數據具有完全的所有權和控制權。當模塊C變成一個單獨的服務時,遷移它應該會更容易。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我還在ThoughtWorks工作時,我的一位老同事"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/petegillardmoss","title":null,"type":null},"content":[{"type":"text","text":"Peter Gillard-Moss"}]},{"type":"text","text":"第一次向我展示了這種模式。這是他爲我們正在開發的一個內部系統設計的。他說,“我覺得這能行。我們不確定我們是否想要提供服務,所以也許它應該是一個單體。”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我說,“試一試。看看會發生什麼。“大約6年過去了,去年我和Peter談過,ThoughtWorks仍然沒有改變架構。它仍然運行得很歡快。他們讓不同的人處理不同的模塊,即使是在這個級別上將數據分離開來,也給他們帶來了巨大的好處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/14\/14cd7db9d8a854712dd4bbe013d89ebd.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖4:分佈式單體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我們來看看最糟糕的單體——分佈式單體。我們的應用程序代碼現在運行在彼此通信的獨立進程上。不管出於什麼原因,我們都必須將整個系統作爲一個單元同步部署。經常,這種情況的出現是因爲我們弄錯了服務邊界。我們將業務邏輯胡亂地放在了不同的層上。我們沒有遵從關於耦合和內聚的要點,現在,我們的結賬邏輯分佈在服務棧中15個不同的地方。我們要做任何工作,都必須協調多個團隊。如果組織中存在大量的橫切更改,通常表明組織邊界或服務邊界定義的不對。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分佈式單體的問題在於,它本質上是一個更加分佈式的系統,但是對於所有相關的設計、運行和操作挑戰,我們仍然需要單體需要的那些協調活動。我想在線部署,但我不能。我必須等你完成更改,但你也完不成,因爲你在等別人。現在,我們一致同意:“好吧,7月5日,我們將一起上線。每個人都準備好了嗎?三、二、一,部署。“當然,一切都很順利。對於這類系統,我們從來沒有遇到過任何問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果一個組織有一位全職的發佈協調經理或這方面的其他職位,那麼他們可能有一個分佈式單體。協調分佈式系統的同步部署一點都不好玩。我們最終會付出更高的更改成本。部署的範圍會大很多,可能會有更多的地方出錯。這種難以避免的協調活動,不只存在於發佈活動中,而且存在於一般部署活動中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"稍微看下精益生產的內容就會發現,減少交接是優化生產力的關鍵。等待別人爲我做點什麼只會產生浪費。這會導致生產力瓶頸。爲了更快地交付軟件,減少交接和協調非常關鍵。遺憾的是,分佈式單體往往會創造出不得不進行協調的環境。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有時,我們的問題不在於服務邊界在哪裏。有時,它完全是始於我們開發軟件的方式。有些人從根本上誤解了發佈序列。發佈序列一直被認爲是一種治療性的發佈技術,而不是一種進取性的活動。我們會選擇像發佈序列這樣的東西來幫助組織轉向持續交付。發佈序列的概念以定期爲基礎,也許每四周,軟件的所有部分已經準備就緒。如果軟件還沒有準備好,它就會被推遲到下一個發佈序列。對許多組織來說,這是向前邁出了一步。我們應該縮小發布序列之間的間隔,最終完全消除。然而,有太多的組織在採用了發佈序列就再也沒有繼續前進。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當若干團隊都朝着同一個發佈序列而努力時,所有已經準備好的軟件都會在這個發佈序列中交付——突然之間,我們會一次性部署大量的服務。這是真正的問題所在。當實踐發佈序列時,最重要的一件事是,至少要將這些發佈序列分解,使它們成爲團隊發佈序列。允許不同的團隊安排自己的發佈序列。最終,我們應該拋棄這些序列。它們應該只是邁向持續交付的一個步驟。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遺憾的是,一些營銷敏捷的優秀成果已經將發佈序列作爲交付軟件的最終方式。我們知道他們已經這麼做了,因爲許多公司組織裏掛着的SAFe圖解上都印着“發佈序列”的字樣。這不是好事。不管是對於SAFe,還是你遇到的任何其他問題,發佈序列始終都是一種補救技術,是自行車的輔助輪。我們應該向着持續交付繼續前進。問題是,如果我們使用這些發佈序列時間太長,最終的架構就會是一個分佈式單體,因爲我們已經習慣了將所有服務部署在一起。要注意這一點。這可能不會在一夜之間發生。我們可以從支持獨立部署的架構開始,但如果我們使用發佈序列太久,我們的架構就會開始圍繞這些發佈實踐聚合在一起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"歸根結底,分佈式單體是一個問題,因爲它同時具有分佈式系統的所有複雜性和單個部署單元的缺點。我們應該跨越它,尋找更好的工作方式。分佈式單體是一件很棘手的事情,關於如何處理這種情況的建議已經有很多。有時,正確的答案是將其合併回單進程單體。但是,如果現如今我們有一個分佈式單體,最好的辦法是弄清楚爲什麼我們會有這樣的單體,並且在添加任何新服務之前,着手讓架構中的某些部分可以獨立部署。在這種情況下,添加新服務很可能會增加我們開展工作的難度。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"如何將單體遷移到微服務架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們使用微服務架構是因爲它具有獨立部署的特性。我們希望能夠在不改變其他任何東西的情況下將服務的更改部署到產品中。這是微服務的黃金法則。在演講或文章中,這似乎很容易。在現實生活中,要做到這一點要困難得多,尤其是考慮到大多數人並非從零開始。絕大多數人都覺得他們的系統太大了,想把它分成更小的部分。他們想知道從哪裏開始。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"領域驅動設計(DDD)有一些很好的方法可以幫助我們找出服務邊界。當與研究微服務遷移的組織合作時,我們通常是從在現有的單體應用程序架構上執行DDD建模練習開始。我們這樣做是爲了弄清楚單體應用內部發生了什麼,並從業務域的角度確定工作單元。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管單體看起來像一個巨大的盒子,但當我們應用DDD,並將邏輯模型投射到該單體上時,我們意識到,其內部被組織成訂單管理、PDF渲染、客戶端通知等內容。雖然代碼可能沒有圍繞這些概念進行組織,但從用戶或業務領域模型的角度來看,這些概念存在於代碼中。這些業務領域的邊界(DDD中通常稱爲“有界上下文”)就成爲我們分解的單元,原因我這裏就不展開討論了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1a\/1a3c3d152c94d84433d8ce6935bb1caa.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖5:找出單體中的分解和依賴項單元"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先要做的是問下從哪裏開始,什麼事情可以優先處理,我們的工作單元是什麼。在圖5所示的初始單體中,我們有訂單管理、發票和通知。DDD建模練習將使我們瞭解它們之間的關係。但願我們能得出一個有向無環圖,來描述這些不同功能之間的依賴關係。(如果我們得到是一個依賴關係的循環圖,我們就得做更多的工作。)我們可以看到,在這個單體中,有很多東西都依賴於向客戶發送通知的能力。那似乎是領域的核心部分。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"檢查點:對於我遇到的問題,微服務是合適的解決方案嗎?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以開始問問題了,比如我們應該先提取什麼。我可以完全從這個角度來看問題。我們可能會看到,通知被很多功能使用——如果微服務更好,那麼提取系統中很多部分都在使用的東西將使更多的東西變得更好。也許我們應該從那裏開始。但是,看看所有的入站依賴關係。因爲有太多的部分需要通知功能,所以我們很難將其從現有的單體架構中剝離出來。在這個單體系統中,像結賬或訂單管理之類的概念似乎更加獨立。它們可能是更容易分解的東西。決定從哪一部分開始,從根本上講是一種漸進式分解方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先要記住,單體並不是敵人。我希望大家都好好地思考一下。人們將任何單體系統都視爲問題。在過去幾年裏,我看到的最令人擔憂的事情之一是,微服務現在似乎成了許多人的默認選擇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有人可能記得一句老話:“沒有人會因爲購買IBM產品而被解僱。”意思是,因爲其他人都在買IBM產品,你也可以買——如果你買的東西不適合你,那也不是你的錯,因爲大家都在這麼做。你沒必要冒險。現在每個人都在做微服務,我們也面臨同樣的問題。每個人都吵着要做微服務。這對我很有好處:我寫關於該主題的書,但對你可能不是好事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從根本上說,這取決於我們想要解決什麼問題。我們想要達到而在當前的架構下無法達到的目標是什麼?也許微服務是答案,或者其他什麼東西纔是答案。理解我們想要達到的目標至關重要,否則,我們將很難確定如何遷移我們的系統。我們正在做的事情將改變我們分解系統的方式,以及我們如何確定工作的優先級。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務遷移不像一個開關,沒有開\/關切換。這更像是轉動一個旋鈕。在採用微服務的過程中,我們轉動一下旋鈕,增加一兩個服務。我們想看看一個服務如何發揮作用,它是否提供了我們需要的東西,是否解決了我們的問題。如果是,而且我們也滿意,我們就可以繼續轉動旋鈕。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,我看到很多人都會轉動旋鈕,增加500項服務,然後插上耳機,檢查音量。這是讓鼓膜破裂的好方法。我們不知道我們將要面對什麼問題,那些問題在開發人員的筆記本電腦上碰不到。它們會在生產環境中出現。當我們從提供一個單體系統轉爲一次性提供500個服務時,所有的問題都會同時出現。不管我們最後是提供一項、兩項還是五項服務,還是像Monzo那樣擁有800項或1500項服務,我們都必須從一個小轉變開始。我們需要選擇一些服務來啓動遷移。讓它們在生產環境中運行,積累經驗,並儘快把這種經驗付諸實踐。通過逐步調整,以漸進的方式創建和發佈新的微服務,我們可以更好地發現和處理出現的問題。每個項目將要面對的問題都會有所不同,這取決於許多不同的因素。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們想要從單體系統中提取一些功能,讓它與單體系統的剩餘部分通信並集成,並且要儘快完成。我們不想再進行大爆炸式的重寫了。我們過去是每年向用戶發佈軟件,因爲有一個爲期12個月的窗口期,所以我們可以這樣說:“現有的系統太糟糕了,現在已經無法使用了,但是我們還有12個月的時間來發佈下一個版本。如果我們努力,完全可以重寫系統,我們不會再犯過去犯過的錯誤,現有的功能一個都不會少,而且還會有更多的新功能,一切都會很好。”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當每年發佈一次軟件時,我們從來沒有那樣做。當人們期望每月、每週或每天發佈軟件時,我不知道該如何證明其合理性。套用Martin Fowler的話來說,“"},{"type":"link","attrs":{"href":"https:\/\/www.youtube.com\/watch?v=wgdBVIX9ifA","title":null,"type":null},"content":[{"type":"text","text":"如果你要進行大爆炸式的重寫,你唯一能確定的就是大爆炸。"}]},{"type":"text","text":"”我喜歡動作片中的爆炸場面,但不喜歡我的IT項目裏出現這種情況。我們需要從不同的角度思考如何做出這些更改。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"部署來自單體的第一個微服務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我是架構增量演進的忠實擁護者。我們不應該認爲我們的架構是一成不變的。我們需要有一些模式來幫助我們以漸進的方式向微服務轉變。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們首先看下應用程序模式Strangler Fig,它以一種植物命名,這種植物在樹冠上生根,然後卷鬚向下纏繞在樹幹上。絞殺榕(strangler fig)靠自身無法爬到林冠層以獲得足夠的陽光,所以它不像普通樹木一樣從一棵小樹苗慢慢長大,而是包裹在現有的植物體上。它依賴於現有的樹的高度和力量。隨着時間的推移,這些絞殺榕成長起來,變得越來越大,能夠獨立生存了。如果下面的樹死了,腐爛了,就只剩下絞殺榕和一根空心的柱子。這些東西看起來就像蠟滴在其他樹上——看起來真的很令人不安。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是作爲應用程序遷移策略的一種模式,這種思想是有用的。我們找一個現有的系統(它完成我們想要它做的所有事情,即現有的單體應用程序),然後開始圍繞它封裝出我們的新系統。在這裏,就是我們的微服務架構。實現Strangler Fig應用程序有兩個關鍵。第一個是資產捕獲,即確定把哪些功能遷移到微服務架構的過程。然後我們需要進行轉接。以前對於單體應用程序的調用得轉接到新功能上。如果功能沒有遷移,調用就不需要轉接;非常簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些人對如何轉移功能感到困惑。如果我們真的夠幸運的話,也許可以簡單地複製代碼。如果結賬服務的代碼在單體代碼庫中一個叫“結賬”的漂亮盒子中,我們就可以剪切並粘貼到新服務中。我認爲,如果代碼庫是這種狀態,那你可能不需要任何幫助。更大的可能是,我們將不得不快速瀏覽系統,設法收集所有與結賬相關的代碼。我們可能會做一些重構前的活動。也許我們可以重用這些代碼,但在這種情況下,那將是複製粘貼,而不是剪切粘貼。我們想把這個功能留在這個單體應用中,原因我將在後面討論。更常見的情況是,人們會進行一些重寫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現Strangler Fig的方法有很多種。讓我們來看一種簡單的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們有一個基於HTTP的單體系統。這可能是一個無頭應用程序。我們可以在用戶界面的後臺使用API Boundary攔截調用。我們需要的是可以將調用重定向的東西,因此,我們將使用某種HTTP代理。對於這類架構,HTTP協議非常有效,這是因爲它非常適合透明地重定向調用。通過HTTP發起的調用可以被轉接到許多不同的地方。有很多軟件可以幫你做到這一點,而且非常簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1a\/1a3c3d152c94d84433d8ce6935bb1caa.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖6:HTTP代理攔截對單體的調用,增加了一個網絡躍點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先要做的是,在上游流量和下游單體系統之間放置一個代理,別的什麼都不用做。我們將把這個代理部署到生產環境中。此時,它還沒有轉接任何調用。我們可以看下它在生產環境中是否有效。我們要擔心的一件事是網絡質量,因爲我們增加了一個網絡躍點。通常是直接調用單體系統,但現在通過我們的代理。在這種情況下,延遲是殺手。通過代理轉接只會給現有的調用增加幾毫秒的開銷——少於10毫秒就很棒。如果額外增加一個網絡躍點增加了200毫秒的延遲,我們就需要暫停微服務遷移,因爲我們還有其他需要首先解決的大問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"準備好代理之後,我們接下來將處理新的結賬服務。我們將其部署到生產環境中。即使它功能還不全,也沒什麼問題,因爲它還沒有被使用。我們要在腦海中將部署到生產環境和使用這兩個概念分開。開始採用微服務後,我們希望定期地將功能部署到生產環境中,以確保我們的部署機制能夠正常工作。在添加功能時,我們可以單獨測試新服務。我們還沒有把它發佈給用戶,但它已經在生產環境中了。我們可以將它連接到我們的儀表板上,確保日誌聚合正常,或者做其他我們想做的事。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關鍵是我們只對一個服務進行操作。我們甚至可以將那個服務的提取過程分解爲許多小步驟:創建服務框架、實現方法、在生產環境中測試它,然後部署發佈版本。準備就緒之後,當我們認爲新實現已經等同於舊系統時,我們只需重新配置代理,將調用從舊的單體功能轉到新的微服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事到如今,你可能認爲現在應該從這個單體中刪除舊功能。先別這麼做!如果新創建的微服務在生產環境中出現問題,我們有一種非常快的補救技術:我們只需還原代理配置,將流量轉到原功能所在的單體上。不過,要想實現這一點,我們必須考慮數據的作用——這點我們稍後討論。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們希望,將這個功能提取成微服務是一種真正的重構,改變代碼的結構而不是行爲。在功能上,微服務應該等同於單體中的同一功能。我們應該能夠在它們之間切換,直到微服務正常工作爲止。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們想要保留切換的能力,那麼在遷移完成、我們不再需要這種切換能力之前,我們就不應該添加新的功能或更改現有的功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在很多情況下,這項簡單的Strangler Fig技術都出奇的好。這個例子使用了HTTP,但是我也看過使用FTP的情況。我已經用消息攔截器做到了這一點。我在上傳固定文件時就這麼做了:我們插入固定文件,在新服務中剔除我們希望去掉的內容,然後把剩下的內容傳遞下去。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/95\/955ef04d3392aafaee19779863f997e6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖7:微服務架構的有向無環圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用“抽象分支“模式來逐步完成單體遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Strangler Fig對於結賬或訂單管理等功能非常有效,這些功能在我們的調用堆棧中處於更高的位置,如圖7所示的依賴關係圖。但是,進入單體系統的調用沒有哪個是爲了獲得像忠誠獎勵積分或給客戶發送通知這樣的能力。進入單體的調用是“下訂單”或“付款”。只是作爲這些操作的副作用,我們可能會獎勵積分或發送電子郵件。因此,我們無法在單體系統的外圍攔截對忠誠獎勵積分或通知的調用。那是在單體系統內部完成的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們要把通知功能提取出來。我們必須提取這塊功能,並用一種增量的方式攔截這些入站鏈接,這樣我們就不會破壞系統的其餘部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種名爲“抽象分支”的技術可以很好地完成這項工作。在基於主幹的開發環境中,抽象分支是一種經常討論的模式,這是一種很好的軟件開發方式。在這種情況下中,抽象分支作爲一種模式也很有用。我們在現有的單體系統中創建了一個空間,同一功能的兩種實現可以在其中共存。在很多方面,這是"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Liskov_substitution_principle","title":null,"type":null},"content":[{"type":"text","text":"里氏替換原則"}]},{"type":"text","text":"的一個例子。這是對完全相同的抽象做的一個獨立實現。對於本例,我們將從現有代碼中提取通知功能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/99\/99398e9f07a5fed8af14178e87dc6446.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖8:用於遷移到一個微服務的抽象分支"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通知代碼散佈在我們的系統中。我們要做的第一件事是爲新服務收集所有這些代碼。我們將把服務隱藏在抽象點後面。我們希望結賬代碼和訂單代碼通過一個明確的抽象點來訪問這個功能。起初,我們有一個通知抽象的實現——它封裝了單體中當前所有與通知相關的功能。我們的所有調用——到SMTP庫、到Twilio、發送SMS——都被打包到這個實現中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時,我們所做的只是在代碼中創建了一個很好的抽象點。我們可以停了。我們已經釐清了我們的代碼庫,並使其更容易測試,這已經是改進了。這是一種很好的老式重構。我們也創造了一個機會來更改結賬或訂單使用的通知功能的實現。我們可以用幾天或幾周的時間來完成這項重構工作,同時做一些其他的事情,比如實際發佈特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們開始創建通知服務的新實現。這可以分成兩個部分。我們已經在單體中實現了新接口,但這只是調用另一部分(新建的通知微服務)的客戶端代碼。部署這些實現很安全,因爲它們還沒有被使用。我們更頻繁地集成代碼,減少合併工作,並確保一切工作正常。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一旦單體內部調用新服務的代碼和單體外部的通知服務可以正常工作,我們所需要做的就是切換我們正在使用的抽象實現。我們可以使用特性開關、文本文件、專用工具,或者任何我們希望使用的方式。我們還沒有刪除舊功能,所以如果有問題,我們可以輕鬆切回舊功能。同樣,這個服務的遷移被分解成許多小步驟,我們試圖通過所有這些步驟儘快將其部署到生產環境。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一切工作正常之後,我們就可以選擇清理代碼了。如果不再需要這個功能,我們可以刪除其特性標識,甚或刪除舊代碼。現在刪除舊代碼很容易了,因爲我們已經花了一些時間將所有代碼整理好。我們刪除了那個類,它消失了。我們把單體變小了,每個人都對自己感到滿意。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"並行運行驗證微服務遷移"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就代碼重構而言,我強烈推薦"},{"type":"link","attrs":{"href":"https:\/\/twitter.com\/mfeathers","title":null,"type":null},"content":[{"type":"text","text":"Michael Feathers"}]},{"type":"text","text":"的著作《"},{"type":"link","attrs":{"href":"https:\/\/www.amazon.com\/Working-Effectively-Legacy-Robert-Martin\/dp\/0131177052\/","title":null,"type":null},"content":[{"type":"text","text":"修改代碼的藝術"}]},{"type":"text","text":"》。他對遺留代碼的定義是沒有測試代碼的代碼。關於如何在不破壞現有系統的情況下,在代碼庫中找出並創建這些抽象,這本書提供了很多好主意。即使你不使用微服務,僅僅創建這個抽象點就可能會使你的代碼處於更好、更可測試的狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我已經強調過,不要太早刪除舊實現。保留兩種實現有很多好處。它爲我們如何部署和上線軟件提供了有趣的方法。當調用進入抽象點時,它可以觸發對這兩個實現的調用。這叫做並行運行。這可以幫助我們確保新的微服務實現功能上等價。我們運行該功能的兩個副本,然後比較結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要做這個比較,只需運行這兩個實現並比較結果。我們必須指定其中之一作爲真相來源,因爲我們不希望把兩者串聯起來:例如,在發送通知時,我們只想發送一封郵件,結果卻發了兩封。並行運行是一種實用而直接的實時比較,不僅是功能等價性的比較,而且包含可接受的非功能等價性比較。我們不僅要測試是否創建了正確的電子郵件,並將其發送到正確的虛擬SMTP服務器,而且還要測試新服務的響應速度是否同樣快,或者錯誤率在可接受範圍之內。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常,我們信任舊的功能實現,並使用其結果。我們將它們並行運行一段時間,如果新實現提供了可接受的結果,我們最終將處理掉舊的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GitHub可以幫我們做這件事。他們創建了一個名爲GitHub Scientist的庫,這是一個很小的Ruby庫,用於封裝不同的抽象並對它們進行評分。在重構應用程序中的關鍵代碼路徑時,我們可以使用它來進行實時比較。GitHub Scientist已經被移植到了很多不同的語言上,令人費解的是,Perl有三種不同的移植:顯然,在Perl社區,並行運行是一件很重要的事情。關於如何在應用程序內部並行運行,已經有很多很好的建議。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"將部署與發佈分離:根本性的改變"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從根本上說,我們需要將部署的概念與發佈的概念分離開來。傳統上,我們認爲這兩種活動是一回事,部署軟件和向生產環境用戶發佈軟件是一回事。這就是爲什麼每個人都害怕生產環境會發生什麼事情,這就是生產環境成爲一個封閉環境的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以把這兩個概念分開。將某樣東西部署到生產環境中與將它發佈給我們的用戶是不一樣的。這個想法是人們現在所說的“漸進式交付”的基礎,這是一個涵蓋了一系列不同技術的總稱,包括金絲雀發佈、藍\/綠部署、抹黑啓動等。我們可以快速推出軟件,但不必向任何客戶公開。我們可以把它放到生產環境中,在那裏測試,然後自己承受出現的任何問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們將部署與發佈分開,那麼部署的風險就會小很多。我們就會更加勇敢地進行更改。我們將能夠更頻繁地發佈,而且發佈的風險將更低。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RedMonk聯合創始人James Governor在公司的博客上對漸進式交付做了很好的"},{"type":"link","attrs":{"href":"https:\/\/redmonk.com\/jgovernor\/2018\/08\/06\/towards-progressive-delivery\/","title":null,"type":null},"content":[{"type":"text","text":"闡述"}]},{"type":"text","text":"。該文探討了漸進式交付,其中最重要的結論是,主動部署與主動發佈不是一回事,並且你可以控制發佈活動如何發生。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"用微服務方法遷移簡單的數據訪問"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們將現有的單體應用程序和數據鎖定在系統中,如圖9所示。我們已經決定提取結賬功能,但是它需要訪問數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2f\/2f791311d707747e405ebd108db169fa.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖9:從新服務訪問舊數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選項一是直接訪問單體的數據。如果我們仍然在測試並在單體中的結賬功能和微服務裏的結賬功能之間進行切換,我們會希望這兩種實現之間具有數據兼容性和一致性,這種方式可以保證這一點。這在短時間內是可以接受的,但它違背了數據庫的黃金規則之一:不共享數據庫。這不是可以長期依賴的東西,因爲它會導致根本性的耦合問題。我們希望保持獨立部署的能力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/43\/439eeecb0aadc09195d1304accaac031.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖10:從新服務直接訪問舊數據"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖10所示,我們有一個Shipping服務和數據庫,我們允許其他人訪問我們的數據。我們已經向外部公開了內部實現細節。這使得Shipping服務的開發人員很難知道哪些內容可以安全地更改。哪些數據要共享,哪些數據要隱藏,並沒有做區分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"20世紀70年代,David Parnas提出了“信息隱藏”的概念,我們就是以此爲基礎考慮模塊分解。我們希望在模塊或微服務的邊界內隱藏儘可能多的信息。如果我們創建一個定義良好的服務接口來共享數據,而不是直接公開數據庫,那麼這個接口就讓Shipping服務的開發人員可以明確知道這個契約以及他們可以向外界公開什麼。只要遵守該契約,開發人員就可以在Shipping服務中做任何他們想做的事情。也就是說,這些服務可以獨立演進和開發。不要直接訪問數據庫,除非是在極其特殊的情況下。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拋開直接訪問,我們有兩種選擇:要麼訪問別人的數據,要麼保存自己的數據。對於這個例子,假如我們已經確定新開發的結賬微服務已經足夠好,可以作爲我們的真相來源。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時,如果我們想要使用別人的數據,那麼這可能意味着數據屬於單體,我們必須向單體請求數據。我們在單體上創建某種顯式的服務接口(在我們的示例中是一個API),通過它獲取我們想要的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/51\/51bf6c2544804654d997d6219865849c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖11:新開發的微服務使用單體顯式提供的一個服務接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們是結賬服務,而不是訂單服務,但我們可能需要訂單數據。訂單功能存在於單體中,因此我們將從那裏獲取數據。這樣來說,我們需要在單體上定義服務接口以公開不同的數據集,而且,在這樣做時,我們可以看到其他實體從單體中顯現出來。我們可能會發現,訂單服務正等待着從單體中噴發出來,就像《異形》中的異形幼體,在電影中,單體是由John Hurt扮演的,它會死去。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種選擇是保存服務自己的數據——在本例中,是單體數據庫中的結賬數據。至此,我們必須把數據移到一個結賬數據庫,這真的很難。從現有系統(尤其是關係數據庫)中提取數據會帶來很多麻煩。我們將看一個簡單的例子,看看它帶來的挑戰。我將帶大家深入瞭解一下,如何處理連接。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"最後的挑戰:處理連接操作"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/48\/4869978539c5b13a661c93a7461edc50.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖12:在線銷售光盤的單體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖12描述了一個現有的、在線銷售光盤的單體應用程序。(你可以看出我這個例子已經用了多久了。)Catalog功能知道某些東西多少錢,並將信息存儲在Line Items表中。Finance功能管理我們的財務交易,並將數據存儲在Ledger表中。我們要做的其中一件事是,生成一個每週銷量前10的專輯列表。在這種情況下,我們需要做一個簡單的連接操作。我們從Ledger表上查出10個最暢銷的。我們根據行和其他東西來限制這個查詢。這樣我們就能得到ID列表了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/b5\/b59e8e77885c311c0bc976fd5210be00.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖13:在線銷售光盤的微服務架構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在進入微服務領域後,我們需要在應用層執行連接操作。我們從Finance數據庫中提取財務交易數據。關於我們出售的物品的信息則存在於Catalog數據庫中。爲了生成銷量前10名列表,我們必須從Ledger表中提取最暢銷商品的ID,然後轉到Catalog微服務,查詢所銷售商品的信息。我們過去在關係層中執行的連接操作轉移到了應用程序層。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"延遲可能會變得令人震驚。現在,我不是做一個一次往返的連接操作,而是要調用Finance服務獲取銷量前10的ID,然後調用另一個Catalog服務請求這10個ID的信息,然後Catalog服務從Catalog數據庫中取得這些ID,然後我們纔得到響應。圖13說明了這個過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2f\/2f035b99c493adffe65b6653e8e72158.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"圖14:微服務架構會導致更多的躍點和延遲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還沒有涉及到像缺乏數據完整性這樣的問題(在這種情況下,關係型數據庫如何實現引用完整性)。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你想深入研究諸如處理延遲和數據一致性之類的問題,我在《從單體到微服務》一書中進行了深入的闡述。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論你是否決定繼續自己的微服務遷移之旅,我都建議你仔細考慮下,自己正在做什麼以及爲什麼要這樣做。不要把注意力都放在創建微服務上。相反,你要清楚自己想要達到的結果。你認爲微服務會帶來什麼結果?專注於這一點——你可能會發現,你可以在不進入複雜的微服務世界的情況下實現同樣的結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者簡介:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Sam Newman"},{"type":"text","text":"是一名獨立顧問,專門幫助人們快速交付軟件。他在雲計算、持續交付和微服務方面做了大量工作,特別關注如何更輕鬆地將可以工作的軟件部署到生產環境中。在過去幾年裏,他一直在探索微服務架構的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/www.infoq.com\/articles\/migrating-monoliths-to-microservices-with-decomposition\/","title":null,"type":null},"content":[{"type":"text","text":"Migrating Monoliths to Microservices With Decomposition and Incremental Changes"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章