從單體架構到微服務的轉變:歷史與實踐

在本文中,我將討論我正在從事的專案如何從大型整體轉變為一組微服務。

這個專案的歷史始於很久以前,即 2000 年初。第一個版本是用 Visual Basic 6 編寫的。隨著時間的推移,很明顯,這種語言的開發在未來將難以支持,因為 IDE而且語言本身還很不發達。 2000 年代末,決定轉向更有前途的 C#。 新版本是與舊版的修訂版並行編寫的,逐漸越來越多的程式碼是用.NET寫的。 C#後端最初專注於服務架構,但在開發過程中,使用了具有邏輯的公共庫,並在單一進程中啟動服務。 結果是我們稱之為“服務單體”的應用程式。

這種組合的少數優點之一是服務能夠透過外部 API 相互呼叫。 過渡到更正確的服務以及未來的微服務架構有明確的先決條件。

我們在 2015 年左右開始了分解工作。 我們還沒有達到理想的狀態——大型專案中仍然有一些部分很難被稱為單體,但它們看起來也不像微服務。 儘管如此,進展還是顯著的。
我會在文章中講到。

從單體架構到微服務的轉變:歷史與實踐

Содержание

現有解決方案的架構和問題


最初,這個體系結構看起來是這樣的:UI 是一個單獨的應用程序,整體部分是用 Visual Basic 6 編寫的,.NET 應用程式是一組與相當大的資料庫一起工作的相關服務。

先前解決方案的缺點

單點故障
我們遇到了單點故障:.NET 應用程式在單一進程中運行。 如果任何模組失敗,整個應用程式就會失敗並且必須重新啟動。 由於我們為不同的使用者自動化了大量的流程,由於其中一個流程出現故障,每個人都無法工作一段時間。 如果出現軟體錯誤,即使備份也無濟於事。

改進佇列
這個缺點是相當有組織性的。 我們的應用程式有很多客戶,他們都希望盡快改進它。 以前,這是不可能並行進行的,所有顧客都要排隊。 這個過程對企業來說是負面的,因為他們必須證明他們的任務是有價值的。 開發團隊花了很多時間來組織這個隊列。 這花費了大量的時間和精力,產品最終無法像他們希望的那樣快速改變。

資源利用欠佳
當在單一進程中託管服務時,我們總是將配置從一個伺服器完全複製到另一個伺服器。 我們希望將負載最重的服務分開放置,以免浪費資源並更靈活地控制我們的部署方案。

現代科技難以實施
所有開發人員都熟悉的問題:希望將現代技術引入專案中,但沒有機會。 對於大型整體解決方案,目前庫的任何更新(更不用說過渡到新庫)都會變成一項相當重要的任務。 需要很長時間才能向隊長證明這會帶來比浪費精力更多的獎金。

發布變更困難
這是最嚴重的問題 - 我們每兩個月發布一次版本。
儘管開發人員進行了測試和努力,但每個版本都對銀行來說是一場真正的災難。 該企業了解到,在本周初,其部分功能將無法運作。 開發商明白,接下來一週的嚴重事件正在等待著他們。
每個人都渴望改變現狀。

對微服務的期望


準備好後發出組件。 準備好後透過分解解決方案並分離不同的流程來交付組件。

小型產品團隊。 這很重要,因為在舊的整體上工作的大型團隊很難管理。 這樣的團隊被迫按照嚴格的流程工作,但他們想要更多的創造力和獨立性。 只有小團隊才能負擔得起。

單獨進程中的服務隔離。 理想情況下,我希望將其隔離在容器中,但大量使用 .NET Framework 編寫的服務只能在特定環境下運行。 Windows基於 .NET Core 的服務現在開始出現,但數量仍然很少。

部署靈活性。 我們希望以我們需要的方式組合服務,而不是代碼強制的方式。

使用新技術。 這對任何程式設計師來說都很有趣。

過渡問題


當然,如果很容易將整體分解為微服務,則無需在會議上討論它並撰寫文章。 這個過程中有很多陷阱;我將描述阻礙我們的主要陷阱。

第一個問題 大多數單體應用的典型特徵是:業務邏輯的一致性。 當我們編寫一個整體時,我們希望重複使用我們的類別,以免編寫不必要的程式碼。 當遷移到微服務時,這成為一個問題:所有程式碼都非常緊密耦合,並且很難分離服務。

工作開始時,該儲存庫擁有超過 500 個項目和超過 700 萬行程式碼。 這是一個相當重大的決定 第二個問題。 不可能簡單地將其劃分為微服務。

第三個問題 ——缺乏必要的基礎設施。 事實上,我們手動將原始程式碼複製到伺服器。

如何從整體遷移到微服務


配置微服務

首先,我們立即確定微服務的分離是一個迭代的過程。 我們總是被要求並行開發業務問題。 我們如何在技術上實現這一點已經是我們的問題了。 因此,我們準備了一個迭代過程。 如果您有一個大型應用程式並且最初尚未準備好重寫,那麼它不會以任何其他方式工作。

我們用什麼方法來隔離微服務?

第一種方法 — 將現有模組作為服務移動。 在這方面,我們很幸運:已經有使用 WCF 協定運行的註冊服務。 它們被分成單獨的組件。 我們單獨移植它們,為每個版本添加一個小型啟動器。 它是使用精彩的 Topshelf 庫編寫的,它允許您將應用程式作為服務和控制台運行。 這很方便調試,因為解決方案中不需要額外的項目。

這些服務根據業務邏輯進行連接,因為它們使用通用組件並使用通用資料庫。 它們很難被稱為純粹形式的微服務。 但是,我們可以在不同的流程中單獨提供這些服務。 僅此一點就可以減少它們之間的影響,減少並行開發和單點故障的問題。

與主機的彙編只是 Program 類別中的一行程式碼。 我們將 Topshelf 的工作隱藏在輔助類別中。

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner<Accounts>.Run("RBA.Services.Accounts.Host");

       }
    }
}

第二種分配微服務的方式是: 創建它們來解決新問題。 如果同時巨石沒有成長,那就已經很好了,這意味著我們正在朝著正確的方向前進。 為了解決新問題,我們嘗試建立單獨的服務。 如果有這樣的機會,那麼我們創建了更多「規範」服務,完全管理自己的資料模型,一個單獨的資料庫。

和許多人一樣,我們從身分驗證和授權服務開始。 他們非常適合這個。 它們是獨立的,通常,它們有單獨的資料模型。 他們本身不與巨石互動,只是巨石求助於他們來解決一些問題。 使用這些服務,您可以開始過渡到新的架構,調試它們上的基礎設施,嘗試一些與網路庫相關的方法等。 我們組織中沒有任何團隊無法建立身分驗證服務。

微服務的第三種分配方式我們使用的那個對我們來說有點特殊。 這是從 UI 層移除業務邏輯。 我們的主要 UI 應用程式是桌面;它與後端一樣,都是用 C# 編寫的。 開發人員定期犯錯誤,並將部分本應存在於後端並被重複使用的邏輯轉移到 UI。

如果您從 UI 部分的程式碼中查看一個真實的範例,您可以看到該解決方案的大部分內容都包含在其他流程中有用的真實業務邏輯,而不僅僅是用於建立 UI 表單。

從單體架構到微服務的轉變:歷史與實踐

真正的 UI 邏輯只存在於最後幾行。 我們把它轉移到伺服器上,以便可以重複使用,從而減少UI並實現正確的架構。

第四種也是最重要的微服務隔離方式,這使得減少整體成為可能,即透過處理刪除現有服務。 當我們按原樣取出現有模組時,結果並不總是符合開發人員的喜好,並且自功能創建以來業務流程可能已經過時。 透過重構,我們可以支援新的業務流程,因為業務需求不斷變化。 我們可以改進原始程式碼,消除已知缺陷,並創建更好的資料模型。 有很多好處。

將服務與處理分離與有界脈絡的概念密不可分。 這是領域驅動設計的概念。 它意味著領域模型的一部分,其中唯一定義了單一語言的所有術語。 讓我們來看看保險和帳單背景的例子。 我們有一個整體應用程序,我們需要使用保險帳戶。 我們希望開發人員在另一個程式集中找到現有的 Account 類,從 Insurance 類別中引用它,然後我們將獲得工作代碼。 DRY 原則將受到尊重,使用現有程式碼可以更快地完成任務。

結果表明,帳戶和保險的背景是相關的。 隨著新需求的出現,這種耦合將幹擾開發,增加本已複雜的業務邏輯的複雜性。 要解決這個問題,您需要找到程式碼中上下文之間的邊界並消除它們的違規行為。 例如,在保險方面,很可能 20 位中央銀行帳號和開戶日期就足夠了。

為了將這些有界上下文相互分離,並開始將微服務與整體解決方案分離,我們使用了一種方法,例如在應用程式中建立外部 API。 如果我們知道某個模組應該成為微服務,並在流程中進行某種修改,那麼我們立即透過外部呼叫來呼叫屬於另一個有限上下文的邏輯。 例如,透過 REST 或 WCF。

我們堅定地決定,我們不會避免需要分散式事務的程式碼。 在我們的例子中,事實證明遵循這條規則非常容易。 我們還沒有遇到真正需要嚴格分散式事務的情況——模組之間的最終一致性就已經足夠了。

我們來看一個具體的例子。 我們有協調器的概念——處理「應用程式」實體的管道。 他依序創建了一個客戶、一個帳戶和一張銀行卡。 如果客戶和帳戶建立成功,但建立卡片失敗,申請不會進入「成功」狀態,仍處於「未建立卡片」狀態。 將來,後台活動將拾取它並完成它。 一段時間以來,系統一直處於不一致的狀態,但我們對此整體感到滿意。

如果出現需要持續保存部分資料的情況,我們很可能會整合服務,以便在一個流程中處理它。

讓我們來看一個分配微服務的範例。 如何才能相對安全地將其投入生產? 在此範例中,我們有系統的一個單獨部分 - 工資服務模組,我們希望將其程式碼部分之一製作為微服務。

從單體架構到微服務的轉變:歷史與實踐

首先,我們透過重寫程式碼來創建一個微服務。 我們正在改進一些我們不滿意的方面。 我們實施客戶的新業務要求。 我們在 UI 和後端之間的連接中新增一個 API 網關,它將提供呼叫轉送。

從單體架構到微服務的轉變:歷史與實踐

接下來,我們將此配置投入運行,但處於試點狀態。 我們的大多數用戶仍然使用舊的業務流程。 對於新用戶,我們正在開發一個新版本的單體應用程序,不再包含此過程。 本質上,我們將單體應用和微服務結合起來作為試點。

從單體架構到微服務的轉變:歷史與實踐

透過成功的試點,我們了解到新的配置確實可行,我們可以從方程式中刪除舊的整體,並用新的配置取代舊的解決方案。

從單體架構到微服務的轉變:歷史與實踐

總的來說,我們幾乎使用所有現有的方法來拆分單體的原始碼。 所有這些都允許我們減少應用程式各部分的大小並將它們轉換為新的庫,從而製作更好的原始程式碼。

使用資料庫


資料庫的劃分比原始碼更糟糕,因為它不僅包含當前模式,還包含累積的歷史資料。

與許多其他資料庫一樣,我們的資料庫有另一個重要缺點 - 規模龐大。 該資料庫是根據整體複雜的業務邏輯以及各種有界上下文的表之間累積的關係來設計的。

在我們的例子中,除了所有的麻煩(大型資料庫、許多連接、有時表之間的邊界不清晰)之外,還出現了許多大型專案中出現的問題:共享資料庫模板的使用。 資料透過視圖、複製從表中獲取,然後傳送到需要複製的其他系統。 因此,我們無法將這些表移動到單獨的模式中,因為它們正在被積極使用。

程式碼中有限上下文的同樣劃分有助於我們進行分離。 它通常讓我們很好地了解如何在資料庫層級分解資料。 我們了解哪些表屬於一個有界上下文,哪些表屬於另一個。

我們使用了兩種全域的資料庫分區方法:現有表格分區和處理分區。

如果資料結構良好、滿足業務需求並且每個人都對此感到滿意,那麼拆分現有表是一個很好的方法。 在這種情況下,我們可以將現有表分成單獨的模式。

當業務模式發生很大變化,表格已經無法滿足我們的時候,就需要一個有處理的部門。

拆分現有表。 我們需要確定要分離什麼。 如果沒有這些知識,什麼都行不通,這裡程式碼中限界上下文的分離將對我們有所幫助。 通常,如果您可以理解原始程式碼中上下文的邊界,那麼哪些表應包含在部門清單中就會變得很清楚。

讓我們想像一下,我們有一個解決方案,其中兩個整體模組與一個資料庫互動。 我們需要確保只有一個模組與分隔表的部分進行交互,而另一個模組開始透過 API 與其進行交互。 首先,僅透過 API 進行錄製就足夠了。 這是我們談論微服務獨立性的必要條件。 只要沒有大問題,讀取連線就可以保留。

從單體架構到微服務的轉變:歷史與實踐

下一步是我們可以將處理單獨表的程式碼部分(無論是否經過處理)分離到單獨的微服務中,並在單獨的進程(容器)中運行它。 這將是一項單獨的服務,可連接到整體資料庫以及與其不直接相關的表。 整體仍然與可拆卸部分交互以進行讀取。

從單體架構到微服務的轉變:歷史與實踐

稍後我們將刪除此連接,即從單一應用程式中讀取分離表的資料也將傳輸到 API。

從單體架構到微服務的轉變:歷史與實踐

接下來,我們將從通用資料庫中選擇僅新微服務適用的表。 我們可以將表格移到單獨的模式,甚至移動到單獨的實體資料庫。 微服務和整體資料庫之間仍然存在讀取連接,但沒有什麼可擔心的,在這種配置下它可以存活相當長的時間。

從單體架構到微服務的轉變:歷史與實踐

最後一步是完全刪除所有連接。 在這種情況下,我們可能需要從主資料庫遷移資料。 有時我們希望在多個資料庫中重複使用從外部系統複製的一些資料或目錄。 這種情況會定期發生在我們身上。

從單體架構到微服務的轉變:歷史與實踐

加工部。 此方法與第一種方法非常相似,只是順序相反。 我們立即分配一個新的資料庫和一個透過 API 與整體互動的新微服務。 但同時,仍然存在一組我們希望將來刪除的資料庫表。 我們不再需要它;我們在新模型中替換了它。

從單體架構到微服務的轉變:歷史與實踐

為了讓這個計劃發揮作用,我們可能需要一個過渡期。

那麼有兩種可能的方法。

第一:我們複製新舊資料庫中的所有資料。 在這種情況下,我們可能會出現資料冗餘和同步問題。 但我們可以接受兩個不同的客戶。 一個將使用新版本,另一個將使用舊版本。

第二:我們根據一些業務標準來劃分資料。 例如,我們的系統中有 5 個產品儲存在舊資料庫中。 我們將第六個任務放入新資料庫中的新業務任務。 但我們需要一個 API 網關來同步這些資料並向客戶端顯示從哪裡獲取資料以及從哪裡獲取資料。

兩種方法都有效,根據情況選擇。

在我們確定一切正常後,可以停用與舊資料庫結構一起使用的整體部分。

從單體架構到微服務的轉變:歷史與實踐

最後一步是刪除舊的資料結構。

從單體架構到微服務的轉變:歷史與實踐

總而言之,我們可以說我們的資料庫有問題:與原始碼相比,使用它很困難,共享更困難,但可以而且應該這樣做。 我們已經找到了一些方法,可以讓我們非常安全地做到這一點,但資料仍然比原始程式碼更容易出錯。

使用原始碼


這就是我們開始分析單體專案時原始碼圖的樣子。

從單體架構到微服務的轉變:歷史與實踐

大致可分為三層。 這是啟動的模組、插件、服務和單獨活動的層。 事實上,這些是整體解決方案中的入口點。 它們都被一層普通層緊密地密封。 它具有服務共享的業務邏輯和大量連接。 每個服務和插件最多使用 10 個或更多通用程序集,具體取決於它們的大小和開發人員的良心。

我們很幸運擁有可以單獨使用的基礎設施庫。

有時會出現一些常見物件其實並不屬於這一層,而是基礎設施庫的情況。 透過重命名解決了這個問題。

最令人擔憂的是有界上下文。 碰巧 3-4 個上下文混合在一個 Common 程式集中,並在相同的業務功能中相互使用。 有必要了解可以在哪裡進行劃分、沿著什麼邊界進行劃分,以及下一步如何將這種劃分映射到原始程式碼程式集中。

我們為程式碼分割過程製定了幾條規則。

第一:我們不再希望在服務、活動和外掛之間共享業務邏輯。 我們希望使業務邏輯在微服務中獨立。 另一方面,微服務理想地被認為是完全獨立存在的服務。 我認為這種方法有點浪費,而且很難實現,因為,例如,C# 中的服務無論如何都會透過標準庫連接。 我們的系統是用C#寫的;我們還沒有使用其他技術。 因此,我們決定有能力使用通用的技術組件。 最主要的是它們不包含任何業務邏輯片段。 如果您在正在使用的 ORM 上有一個方便的包裝器,那麼將其從一個服務複製到另一個服務的成本非常昂貴。

我們的團隊是領域驅動設計的粉絲,因此洋蔥架構非常適合我們。 我們服務的基礎不是資料存取層,而是具有領域邏輯的元件,它只包含業務邏輯,與基礎設施沒有任何關聯。 同時我們可以獨立修改領域元件來解決框架相關的問題。

在這個階段,我們遇到了第一個嚴重的問題。 服務必須引用一個域程序集,我們希望邏輯獨立,而 DRY 原則在這裡極大地阻礙了我們。 開發人員希望重複使用相鄰程式集中的類別以避免重複,因此,網域開始再次連結在一起。 我們分析了結果,認為問題可能還出在原始碼儲存設備的區域。 我們有一個包含所有原始程式碼的大型儲存庫。 整個專案的解決方案很難在本地機器上組裝。 因此,為專案的各個部分創建了單獨的小型解決方案,並且沒有人禁止在其中添加一些公共或網域程式集並重複使用它們。 唯一不允許我們這樣做的工具是程式碼審查。 但有時也失敗了。

然後我們開始轉向具有單獨存儲庫的模型。 業務邏輯不再從一個服務流向另一個服務,網域已經真正變得獨立。 有界上下文得到更明確的支持。 我們如何重複使用基礎設施庫? 我們將它們分離到一個單獨的儲存庫中,然後將它們放入 Nuget 套件中,然後將其放入 Artifactory 中。 對於任何更改,組裝和發布都會自動發生。

從單體架構到微服務的轉變:歷史與實踐

我們的服務開始以與外部基礎設施包相同的方式引用內部基礎設施包。 我們從 Nuget 下載外部程式庫。 為了與放置這些套件的 Artifactory 一起使用,我們使用了兩個套件管理器。 在小型儲存庫中,我們也使用 Nuget。 在具有多個服務的儲存庫中,我們使用 Paket,它提供了模組之間更多的版本一致性。

從單體架構到微服務的轉變:歷史與實踐

因此,透過處理原始程式碼、稍微改變架構並分離儲存庫,我們使我們的服務更加獨立。

基礎設施問題


遷移到微服務的大部分缺點都與基礎設施有關。 您將需要自動化部署,您將需要新的庫來運行基礎架構。

環境中手動安裝

最初,我們手動安裝了環境解決方案。 為了自動化此過程,我們創建了 CI/CD 管道。 我們選擇持續交付流程是因為從業務流程的角度來看,持續部署對我們來說尚不可接受。 因此,操作發送是使用按鈕進行的,測試發送是自動進行的。

從單體架構到微服務的轉變:歷史與實踐

我們使用 Atlassian、Bitbucket 進行原始碼存儲,並使用 Bamboo 進行建置。 我們喜歡在 Cake 中編寫建置腳本,因為它與 C# 相同。 現成的套件到達 Artifactory,Ansible 會自動到達測試伺服器,之後可以立即進行測試。

從單體架構到微服務的轉變:歷史與實踐

單獨記錄


曾經,單體應用的想法之一是提供共享日誌記錄。 我們還需要了解如何處理磁碟上的各個日誌。 我們的日誌被寫入文字檔。 我們決定使用標準 ELK 堆疊。 我們沒有直接透過提供者寫入 ELK,而是決定最終確定文字日誌,並將追蹤 ID 作為標識符寫入其中,添加服務名稱,以便稍後可以解析這些日誌。

從單體架構到微服務的轉變:歷史與實踐

借助 Filebeat,我們可以從以下位置收集日誌: 服務器然後轉換這些數據,使用 Kibana 在使用者介面中建立查詢,並查看服務之間的呼叫路由情況。追蹤 ID 對此非常有用。

測試、調試相關服務


最初,我們並不完全了解如何調試正在開發的服務。 對於單體應用來說一切都很簡單;我們在本機上運行它。 起初他們嘗試對微服務做同樣的事情,但有時要完全啟動一個微服務,您需要啟動其他幾個微服務,這很不方便。 我們意識到我們需要遷移到一個模型,在該模型中,我們只將我們想要偵錯的一個或多個服務留在本地電腦上。 其餘服務則從與 prod 配置相符的伺服器使用。 偵錯後,在測試過程中,對於每個任務,僅將變更的服務發佈到測試伺服器。 因此,該解決方案以未來在生產中出現的形式進行測試。

有些伺服器只運行生產版本的服務。 需要這些伺服器以防發生事故、部署前檢查交付情況以及內部培訓。

我們使用流行的 Specflow 庫添加了自動化測試流程。 從 Ansible 部署後,測試立即使用 NUnit 自動執行。 如果任務覆蓋是全自動的,那麼就不需要手動測試。 儘管有時仍然需要額外的手動測試。 我們使用 Jira 中的標籤來確定要針對特定問題執行哪些測試。

此外,負載測試的需求也有所增加;以前僅在極少數情況下進行。 我們使用 JMeter 運行測試,使用 InfluxDB 儲存測試,使用 Grafana 建立流程圖。

我們取得了什麼成就?


首先,我們擺脫了「發布」的概念。 當這個龐然大物部署在生產環境中、暫時擾亂業務流程時,兩個月的巨大發布已經一去不復返了。 現在我們平均每1,5天部署一次服務,並分組,因為它們需要經過批准才能運作。

我們的系統不存在致命故障。 如果我們發布一個有錯誤的微服務,那麼與之相關的功能將被破壞,並且所有其他功能都不會受到影響。 這極大地提高了用戶體驗。

我們可以控制部署模式。 如有必要,您可以從解決方案的其餘部分中單獨選擇服務組。

此外,我們透過大量改進顯著減少了這個問題。 我們現在擁有獨立的產品團隊,獨立處理某些服務。 Scrum 流程已經很適合這裡了。 特定團隊可能有一個單獨的產品負責人,負責分配任務給其。

總結

  • 微服務非常適合分解複雜的系統。 在這個過程中,我們開始了解我們的系統中有什麼,有哪些有限的上下文,它們的邊界在哪裡。 這使您可以在模組之間正確分配改進並防止程式碼混亂。
  • 微服務提供組織效益。 它們通常僅作為架構來討論,但任何架構都需要解決業務需求,而不是單獨存在。 因此,我們可以說,鑑於Scrum現在非常流行,微服務非常適合解決小團隊的問題。
  • 分離是一個迭代過程。 您不能將應用程式簡單地劃分為微服務。 最終的產品不太可能發揮作用。 在奉獻微服務時,重寫現有的遺留物是有益的,即將其變成我們喜歡的、在功能和速度上更好地滿足業務需求的程式碼。

    一個小警告: 遷移到微服務的成本相當可觀。 光是解決基礎設施問題就花了很長時間。 因此,如果您有一個不需要特定擴展的小型應用程序,除非您有大量客戶爭奪您團隊的注意力和時間,那麼微服務可能不是您今天所需要的。 這是相當昂貴的。 如果您從微服務開始該流程,那麼最初的成本將高於透過開發單體應用程式開始相同專案的成本。

    PS 一個更感性的故事(就好像針對你個人而言) - 根據 鏈接.
    這是報告的完整版本。

來源: www.habr.com

為具有 DDoS 保護、VPS VDS 服務器的站點購買可靠的主機 🔥 購買具備 DDoS 防護的可靠網站寄存服務,包括 VPS 和 VDS 伺服器 | ProHoster