對單體系統優缺點評判到位:拆分Shopify單體工程的經驗分享

banq發表於2019-04-29

Shopify是現存最大的Ruby on Rails程式碼庫之一。它已被超過一千名開發人員使用了十多年。它封裝了來自計費商家,管理第三方開發者應用程式,更新產品,處理運輸等許多不同功能。它最初是作為整體構建的,這意味著所有這些不同的功能都構建在相同的程式碼庫中,它們之間沒有邊界。多年來,這種架構為我們工作,但最終,我們達到了這樣一個臨界點,即單體monolith的缺點超過了好處。我們必須選擇如何進行分解。
微服務近年來大受歡迎,並被吹捧為解決所有單體問題的最終解決方案。然而,我們自己的集體經驗告訴我們,沒有一種尺寸適合所有最佳解決方案,微服務將帶來他們自己的一系列挑戰。我們選擇將Shopify發展為模組化單體,這意味著我們將所有程式碼儲存在一個程式碼庫中,但確保在不同元件之間定義和遵守邊界。
每個軟體體系結構都有自己的優缺點,根據應用程式的增長階段,不同的解決方案對於應用程式是有意義的。從單體到模組化是我們的下一個合乎邏輯的步驟。

單體架構

根據維基百科,monolith是一個軟體系統,其中功能上可區分的方面都是交織在一起的,而不是包含架構上獨立的元件。對於Shopify來說,這意味著處理計算運費的程式碼與處理結賬的程式碼一起存在,並且幾乎沒有阻止他們互相打電話。隨著時間的推移,這導致處理不同業務流程的程式碼之間的極高耦合。

單體系統的優點
單體架構是最容易實現的。如果沒有實施架構設計,一般結果可能就是一個單體。在Ruby on Rails中尤其如此,由於應用程式級別的所有程式碼的全域性可用性,非常適合構建單體。單體架構可以將應用程式推向極致,因為它易於構建,並允許團隊在一開始就非常快速地移動,以便更早地將產品提供給客戶。
將整個程式碼庫儲存在一個位置並將應用程式部署到一個位置具有許多優點。您只需要維護一個儲存庫,並且能夠輕鬆搜尋並查詢一個資料夾中的所有功能。它還意味著只需要維護一個測試和部署管道,這取決於應用程式的複雜性,可以避免很多開銷。這些管道的建立,定製和維護成本很高,因為它需要齊心協力才能確保所有管道的一致性。由於所有程式碼都部署在一個應用程式中,因此資料都可以儲存在單個共享資料庫中。每當需要一個資料時,它就是一個簡單的資料庫查詢來檢索它。 
由於單體部署在同一個地方,因此只需要管理一組基礎設施。大多數Ruby應用程式都帶有資料庫,Web伺服器,後臺作業功能,然後可能還有其他基礎架構元件,如Redis,Kafka,Elasticsearch等等。這些其他基礎架構還會增加可能的故障點,從而降低應用程式的彈性和安全性。
 在多個獨立服務上選擇單體架構最顯著的好處之一是,您可以直接呼叫不同的元件,而不需要透過Web服務API進行通訊,這意味著您不必擔心API版本管理和向後相容性,以及潛在的滯後呼叫。
 

單體系統的缺點
但是,如果應用程式達到一定規模或者團隊建設達到一定規模,它最終將超越單體架構。這發生在2016年的Shopify,由於構建和測試新功能的不斷增加的挑戰而顯而易見。具體來說,有幾件事情可以作為我們的絆腳石。
應用程式非常脆弱,新程式碼具有意想不到的影響。做出看似無害的變化可能會引發一系列無關的測試失敗。例如,如果計算我們的運費的程式碼被呼叫到計算稅率的程式碼中,那麼對我們計算稅率的方式進行更改可能會影響運費計算的結果,但這可能並不明顯。這是高耦合和缺乏邊界的結果,這也導致難以編寫的測試,並且在CI上執行非常慢。 
在Shopify中進行開發需要大量的上下文來進行看似簡單的更改。當新的Shopifolk上架並開始瞭解程式碼庫時,他們在生效之前需要獲取的資訊量是巨大的。例如,加入運輸團隊的新開發人員應該只需要瞭解運輸業務邏輯的實施,然後才能開始構建。然而,現實情況是,他們還需要了解訂單的建立方式,我們如何處理付款等等,因為一切都是如此交織在一起。這對於一個人來說只是為了釋出他們的第一個特徵而必須堅持下去的知識太多了。複雜的整體應用導致陡峭的學習曲線。
我們遇到的所有問題都是程式碼中不同功能之間缺乏界限的直接結果。很明顯,我們需要減少不同域之間的耦合。

微服務架構
微服務是一種非常時髦的解決方案。微服務架構是一種應用程式開發方法,其中大型應用程式構建為一套獨立部署的小型服務。雖然微服務可以解決我們遇到的問題,但它們會帶來另一整套問題。 
我們必須維護多個不同的測試和部署管道,並承擔每項服務的基礎架構開銷,同時並不總是能夠在需要時訪問我們需要的資料。由於每個服務都是獨立部署的,因此服務之間的通訊意味著跨越網路,這會增加延遲並降低每次呼叫的可靠性。此外,跨多個服務的大型重構可能很繁瑣,需要對所有相關服務進行更改並協調部署。

模組化單體
我們想要一種解決方案,在不增加部署單元數量的情況下增加模組化,使我們能夠獲得單塊和微服務的優勢,而沒有太多的缺點。
模組化整體是一種系統,其中所有程式碼都為單個應用程式提供支援,並且在不同域之間存在嚴格的強制邊界。

Shopify的Modular Monolith實現:元件化
很明顯,我們已經超越了單體結構,並且它正在影響開發人員的生產力和幸福感,我們已經向在我們的核心繫統中工作的所有開發人員傳送了一項調查,以確定主要的難點。我們知道我們遇到了問題,但我們希望在提出解決方案時能夠獲得資料資訊,以確保它能夠真正解決我們遇到的問題,而不僅僅是傳聞中的問題。
該調查的結果告知我們決定拆分我們的程式碼庫。在2017年初,一個小而強大的團隊被組合起來解決這個問題。該專案最初被命名為“Break-Core-Up-Into-Multiple-Pieces”,最終演變為“元件化”。

程式碼組織
他們選擇解決的第一個問題是程式碼組織。目前,我們的程式碼組織得像典型的Rails應用程式:軟體概念(模型,檢視,控制器)。目標是透過真實世界的概念(如訂單,運輸,庫存和計費)對其進行重新組織,以便更容易找到程式碼,找到理解程式碼的人,並瞭解他們的個別部分。
每個元件都將構建為自己的迷你rails應用程式,目標是最終將它們命名為ruby模組。希望這個新組織能夠突出那些不必要耦合的領域。 
提出最初的元件清單涉及公司每個領域的利益相關者的大量研究和投入。我們透過在一個大型電子表格中列出每個ruby類(大約6000個)並手動標記它所屬的元件來完成此操作。即使在此過程中沒有更改程式碼,它仍然觸及整個程式碼庫,如果操作不正確可能存在風險。
我們在自動指令碼構建的一個大爆炸PR中實現了這一改革舉措。由於引入的更改只是檔案移動,因此可能發生的故障將導致我們的程式碼不知道在何處查詢物件定義,從而導致執行時錯誤。我們的程式碼庫經過了充分的測試,因此透過在本地和CI中執行我們的測試而不會出現故障,以及在本地和分段上執行儘可能多的功能,我們能夠確保沒有遺漏任何東西。我們選擇在一個PR中完成所有操作,因此我們只會儘可能少地破壞所有開發人員。這種變化的一個不幸的缺點是,當檔案移動被錯誤地跟蹤為刪除和建立而不是重新命名時,我們在Github中丟失了很多Git歷史記錄。我們仍然可以使用。來追蹤起源git`-follow`選項跟隨檔案移動的歷史,但是,Github不理解這一舉​​動。

隔離依賴關係
下一步是透過將業務域彼此分離來隔離依賴關係。每個元件都定義了一個乾淨的專用介面,其域邊界透過公共API表示,並對其關聯資料進行獨佔所有權。雖然團隊無法在整個Shopify程式碼庫中實現這一點,因為它需要來自每個業務領域的專家,但他們確實定義了模式並提供了完成任務的工具。 
我們在內部開發了一個名為Wedge的工具,它跟蹤每個元件朝著隔離目標的進展。它突出顯示任何違反域邊界的行為(當透過除公共定義的API之外的任何元件訪問另一個元件時)以及跨邊界的資料耦合。為實現這一目標,我們編寫了一個工具,在CI期間掛鉤到Ruby跟蹤點以獲得完整的呼叫圖。然後,我們按元件對呼叫者和被呼叫者進行排序,僅選擇跨元件邊界的呼叫,並將它們傳送到Wedge。除了這些呼叫之外,我們還會從程式碼分析中傳送一些其他資料,例如ActiveRecord關聯和繼承。Wedge然後確定哪些跨元件事物(呼叫,關聯,繼承)是正確的,哪些是違反的。通常:

  • 跨元件關聯總是違反元件化
  • 呼叫只適用於明確公開的內容
  • 繼承將類似,但尚未完全實現

Wedge然後計算總分並列出每個元件的違規。下一步,我們將繪製隨時間變化的分數趨勢,並顯示有意義的差異,以便人們可以看到分數變化的原因和時間。

執行邊界
從長遠來看,我們希望更進一步,並以程式設計方式強制執行這些邊界。Dan Manges的這篇部落格文章  提供了一個應用團隊如何實現邊界實施的詳細示例。雖然我們仍在研究我們想要採用的方法,但高階計劃是讓每個元件僅載入其明確依賴的其他元件。如果它試圖訪問未宣告依賴的元件中的程式碼,則會導致執行時錯誤。當元件透過其公共API以外的任何其他方式訪問時,我們還可能觸發執行時錯誤或測試失敗。 
我們還想 透過刪除意外和迴圈依賴關係來解開域依賴關係圖。實現完全隔離是一項持續的任務,但是Shopify的所有開發人員都在投資,我們已經看到了一些預期的好處。例如,我們有一個傳統的稅務引擎,不再滿足我們商家的需求。在本文所述的努力之前,將舊系統更換為新系統幾乎是不可能完成的任務。但是,由於我們已經投入了大量精力來隔離依賴關係,我們能夠將我們的稅務引擎換成一個全新的稅收計算系統。
總之,在系統早期,沒有任何架構通常是最好的架構。這並不是說不實施良好的軟體實踐,而是花費數週和數月的時間來嘗試構建一個您還不知道的複雜系統。
Martin Fowler的Design Stamina Hypothesis  透過解釋在大多數應用程式的早期階段,您可以實施比較少的事先設計。將設計質量與上市時間進行權衡是切合實際的。一旦您可以新增特性和功能的速度開始減慢,那就是投資良好設計的時候了。 
重構和重新構建的最佳時間是儘可能晚,因為您在構建時不斷了解有關係統和業務領域的知識。在擁有領域專業知識之前設計一個複雜的微服務系統是一個冒險的舉措,太多的軟體專案都會陷入其中。根據Martin Fowler的說法,“幾乎所有我聽說過從頭開始構建為微服務系統的系統,它已經結束了嚴重的麻煩......你不應該開始一個帶微服務的新專案,即使你'確保你的應用程式足夠大,以使其值得“。良好的軟體架構是一項不斷髮展的任務,適合您應用的正確解決方案絕對取決於您的運營規模。隨著應用程式複雜性的增加,Monolith,模組化整體結構和麵向服務的體系結構將逐漸演化。每個架構都適用於不同規模的團隊/應用程式,並將被痛苦和痛苦的時期分開。當你開始體驗本文中強調的許多痛點時,那就是當你知道你已經超越當前的解決方案時,是時候進入下一個了。
 

相關文章