Medium 架構實踐:避免微服務綜合症

作者 | Xiao Ma

譯者 | 覃璐

微服務架構的目標是幫助工程師團隊更快、更安全、更高質量地交付產品。解耦服務使團隊能夠快速迭代,並儘可能降低對系統其餘部分的影響。

在 Medium,我們的技術棧始於 2012 年的單體 Node.js 應用程序。我們已經構建了幾個衛星通訊服務,但還沒有系統地制定一個採用微服務架構的策略。隨著系統變得越來越複雜並且團隊不斷髮展,我們在 2018 年初轉向了微服務架構。在這篇文章中,我們希望分享關於如何有效實現微服務架構並避免微服務綜合症的經驗。

什麼是微服務架構?

首先,讓我們花點時間思考微服務架構是什麼,不是什麼。“微服務”是那些超負荷和混亂的軟件工程的趨勢之一。在 Medium 我們認為:

在微服務架構中,多個松耦合的服務協同工作。每項服務都專注於一個目的,並對相關行為和數據高內聚。

該定義包括三個微服務設計原則:

  • 單一目的 - 每項服務應專注於一個目的並做好。
  • 松耦合 - 服務彼此間沒有太多聯繫。更改一項服務時不應要求更改其他服務。應僅通過公共服務接口進行服務之間的通信。
  • 高內聚 - 每個服務將所有相關行為和數據封裝在一起。如果我們需要構建新功能,所有更改僅限於一個服務。


Medium 架構實踐:避免微服務綜合症


當我們對微服務建模時,我們應該遵守這三個設計原則。這是實現微服務架構全部潛力的唯一途徑。缺少任何一個都會成為反模式。

缺少單一目的,每個微服務最終會做太多事情,成長為多個“單體”服務。我們不會從微服務架構中獲得全部好處,還要支付運營成本。

缺少松耦合,對一項服務的更改會影響其他服務,因此我們無法快速安全地發佈更改,這是微服務架構的核心優勢。更重要的是,緊耦合引起的問題可能是災難性的,例如數據不一致甚至數據丟失。

缺少高內聚,我們將最終得到一個分佈式的單體系統,一組混亂的服務,必須同時進行更改和部署才能構建單一功能。由於多個服務協調的複雜性和成本(有時跨多個團隊),分佈式單體系統通常比集中式單體系統差得多。

與此同時,瞭解微服務不是什麼也很重要:

  • 微服務不是代碼行數少或處理“微”任務的服務。這種誤解來自“ 微服務” 這個名稱。微服務架構的目標不是擁有儘可能多的小型服務。只要符合上述三項原則,服務也能夠是複雜而重要的。
  • 微服務並不總是由新技術構建。儘管微服務架構能夠讓團隊更輕鬆地驗證新技術,但它並不是微服務架構的主要目標。只要團隊從分離的服務中受益,就可以使用完全相同的技術棧構建新服務。
  • 微服務不是必須從頭開始構建的服務。如果你已經擁有一個架構良好的單體應用程序,請避免養成從頭開始構建每個新服務的習慣。也可以直接從單體服務中直接提取邏輯。同樣,應該仍然遵循上述三個原則。

為什麼是現在?

在 Medium,在做出重大產品或工程決策時,我們總是會問“為什麼是現在?”這個問題。“為什麼?”是一個顯而易見的問題,但它假設我們擁有無限的人、時間和資源,這是一個危險的假設。當你想到“為什麼是現在?”時,就突然有了更多的限制,對當前工作的影響、機會成本、分心的開銷等等。這個問題有助於我們更好地考慮優先級。

為什麼我們需要現在採用微服務,是因為我們的 Node.js 單體應用在多個方面成為了瓶頸。

首先,最緊迫和最重要的瓶頸是性能。某些計算繁重且 I/O 繁重的任務不適合 Node.js。我們一直在逐步改進這個單體應用程序,但事實證明收效甚微。它糟糕的性能使我們無法提供更好的產品,雖然應用程序不會變得更慢。

其次,另一個重要且有點緊迫的瓶頸是單體應用程序會降低產品開發速度。由於所有工程師都在單個應用程序中構建功能,因此它們通常是緊密耦合的。我們無法對系統某部分進行靈活地更改,因為它也可能影響其他部分。我們也害怕做出重大改變,因為影響太大,有時難以預測。整個應用程序作為一個整體進行部署,如果由於一次錯誤提交導致部署停滯,所有其他更改(即使它們完全正常工作)也無法完成。相比之下,微服務架構可以使團隊更快地交付、學習和迭代。他們可以專注於自己正在構建的功能,這些功能與複雜系統的其餘部分是分離的。變更可以更快地進入生產。他們可以靈活安全地嘗試重大變革。

在我們新的微服務架構中,變更會在一小時內發佈到生產環境中,工程師不必擔心它會影響系統的其他部分。團隊還探索了在開發中安全使用生產數據的方法,在這麼多年以來這件事就像是白日夢。隨著我們的工程團隊的發展,所有這些都非常重要。

第三,單體應用程序很難為特定任務擴展系統,也不能為不同類型的任務進行資源隔離。對於消耗大量資源的任務,單體應用程序只能對整個系統進行伸縮,這意味著對於其他的簡單任務存在超配。為了緩解這些問題,我們對不同類型的請求進行分片,以分離 Node.js 進程。在一定程度上這起到了些作用,但伸縮依然受限,因為這些單體服務的微型版本是緊耦合的。

最後,一個重要且即將成為緊迫的瓶頸是它阻礙我們嘗試新技術。微服務架構的一個主要優點是每個服務都可以使用不同的技術棧構建,並與不同的技術集成。這使我們能夠選擇最適合工作的工具,更重要的是,這樣可以快速安全地完成任務。

微服務策略

採用微服務架構並非易事。它可能會出錯,使得實際上損害工程生產力。在本節中,我們將分享早期階段對我們有益的七個策略:

  • 構建具有明確價值的新服務
  • 單一的持久存儲是有害的
  • 解耦“構建服務”和“運行服務”
  • 詳盡和一致的可觀察性
  • 無需從頭構建新服務
  • 重視發生的故障
  • 從一開始就避免“微服務綜合症”

構建具有明確價值的新服務

有人可能會認為,採用新的服務端架構意味著長時間暫停產品開發和對所有內容的大量重寫。這其實是錯誤的做法,我們絕不應該為了構建新的服務而構建。我們每次構建新服務或採用新技術時,都必須具有明確的產品價值或工程價值。

產品價值就是我們可以帶給用戶的好處。相對於單體的 Node.js 應用而言,新服務需要提供更多的價值,或者是更快的提供價值。工程價值意味著讓工程團隊更快、更好的工作。

如果構建新服務既沒有產品價值,也沒有工程價值,我們將仍然停留在單體應用中。如果十年後 Medium 仍然有單體 Node.js 應用在提供服務,那也沒什麼問題。實際上單體應用能夠幫助我們更有策略地對微服務進行建模。

單一的持久存儲是有害的

建模微服務的很大一部分工作是對其持久化存儲的數據(例如,數據庫)進行建模。跨服務共享持久數據存儲似乎是將微服務集成在一起的最簡單方法,然而,它實際上是有害的,我們應該不惜一切代價避免它。下面解釋一下為什麼。

首先,持久數據存儲事關實現細節。跨服務共享數據存儲會將服務的實現細節暴露給整個系統。如果服務更改了數據的格式,或者添加了緩存層,或者切換到不同類型的數據庫,則還必須相應地更改許多其他服務。這違反了松耦合的原則。

其次,持久數據存儲,即如何修改、描述和使用數據,不是服務行為。如果我們跨服務共享數據存儲,則意味著其他服務也必須複製這些服務行為。這違反了高內聚的原則,給定域中的行為洩露給了多個服務。如果我們修改一個行為,我們將不得不一起修改所有這些服務。

在微服務架構中,特定類型的數據只應該有一個服務負責。所有其他服務應該通過該服務的 API 請求數據,或者保留數據的只讀非規範(可能具體化)副本。

這聽起來可能有點抽象,下面是一個具體的例子。假設我們正在構建一個新的推薦服務,它需要來自發帖數據表中的一些數據,目前存儲在 AWS DynamoDB 中。我們可以通過下面兩種方式為新推薦服務提供發佈數據。

Medium 架構實踐:避免微服務綜合症


在單一存儲模型中,推薦服務可以直接訪問單體應用的持久存儲。這是一個壞主意,因為:

  • 緩存可能很棘手。如果推薦服務與單體應用共享相同的緩存,我們將不得不在推薦服務中複製緩存實現細節; 如果推薦服務使用自己的緩存,當單體應用更新帖子數據時,我們不知道什麼時候使其緩存無效。
  • 如果單片應用程序決定改為使用 RDS 而不是 DynamoDB 來存儲帖子數據,我們將不得不重新實現推薦服務中的邏輯以及訪問帖子數據的所有其他服務。
  • 單體應用具有描述帖子數據的複雜邏輯,例如,如何確定帖子是否對給定用戶可見。我們必須在推薦服務中重新實現這些邏輯。一旦單體應用更改或添加新邏輯,我們也需要在所有地方進行相同的更改。
  • 儘管推薦服務的數據訪問模式是錯的,它仍然和在 DynamoDB 耦合在一起。

在解耦存儲模型中,推薦服務不能直接訪問發貼數據,也不能訪問任何其他新服務。發貼數據的實現細節僅保留在一個服務中。有不同的方法來實現這一目標。

理想情況下,發帖數據應該有一個發帖服務,其他服務只能通過發帖服務的 API 訪問發帖數據。但是,為所有核心數據模型構建新服務可能是一項昂貴的前期投資。

當人員配備有限時,還有一些更實用的方法。根據數據訪問模式,它們實際上可能是更好的方式。在選項 B 中,單體應用可以讓推薦服務知道相關的發帖數據何時更新。通常,這種時效性不高,因此我們可以將其發送到消息隊列系統。在選項 C 中,ETL 管道為推薦服務生成一份發帖數據的只讀副本,以及其他可能對推薦服務有用的數據。在這兩個選項中,推薦服務完全擁有數據,因此它可以靈活地緩存數據或使用最合適的數據庫技術。

解耦“構建服務”和“運行服務”

如果構建微服務很難,那麼運行服務往往更難。當運行服務需要構建每個服務,並不斷的重複這件事時,會拖慢工程團隊的速度。我們希望讓每項服務都專注於自己的工作而不用擔心如何運行服務這一複雜問題,包括網絡、通信協議、部署、可觀察性等。服務管理應該與每個服務的實現完全分離。

解耦“構建服務”和“運行服務”的策略是使運行服務任務與服務技術無關,並且獨立,以便應用工程師可以完全專注於每個服務自己的業務邏輯。

感謝近年來容器化、容器編排、服務網格、應用性能監控等方面的技術進步,“運行服務”的解耦變得比以往更容易實現。

網絡。網絡(例如,服務發現,路由,負載均衡,流量路由等)是運行服務的關鍵部分。傳統方法是為每種平臺 / 語言提供庫。它可以正常工作但不夠理想,因為應用仍然需要非常繁瑣的工作來集成和維護庫。不僅如此,應用還需要單獨實現某些邏輯。現代解決方案是在服務網格中運行服務。在 Medium,我們使用 Istio 和 Envoy 作為邊車代理。構建服務的應用工程師根本不需要擔心網絡問題。

通信協議。無論選擇哪種技術棧或語言來構建微服務,選擇一個高效、典型、跨平臺且需要最少開發工作量的成熟 RPC 解決方案都是非常重要的。即使它們之間存在依賴關係,支持向後兼容性的 RPC 解決方案也使部署服務更安全。在 Medium,我們選擇了 gRPC。

一種常見的替代方案是基於 HTTP 的 REST + JSON,這種解決方案長期以來一直是服務器通信的好手段。但是,儘管該技術棧非常適合瀏覽器與服務器通信,但它對於服務器之間的通信效率很低,尤其是當我們需要發送大量請求時。如果沒有自動生成的客戶端和模板代碼,我們將不得不手動實現服務器 / 客戶端代碼。可靠的 RPC 實現不僅僅包裝網絡客戶端。另外,REST 是“自描述的”,但它很難讓人對每個細節都形成共識,例如,這個調用真的是 REST,還是隻是一個 RPC?這是一種資源還是一種操作?等等。

部署。擁有一致的方法來構建、測試、打包、部署和管理服務非常重要。Medium 所有的微服務都在容器中運行。目前,我們的編排系統是 AWS ECS 和 Kubernetes 的混合體,但正在向全 Kubernetes 遷移。

我們構建了自己的系統來構建、測試、打包和部署服務,稱為 BBFD。它在跨服務的一致性和為特定服務提供採用不同技術棧的靈活性之間取得平衡。它的工作方式是讓每個服務提供基本信息,例如,要監聽的端口,構建 / 測試 / 啟動服務的命令等,BBFD 將負責其餘的工作。

詳盡和一致的可觀察性

可觀察性包括允許我們瞭解系統如何工作的過程,約定和工具,以及在故障時對問題進行鑑定。可觀察性包括日誌記錄、性能跟蹤、指標、儀表盤、警報,它對微服務架構成功與否至關重要。

當我們從單個服務遷移到具有許多服務的分佈式系統時,可能會發生兩件事:

  1. 我們失去了可觀察性,因為它實現起來更難,更容易被忽視。
  2. 不同的團隊重複造輪子,最終得到了碎片化的可觀察性,這種可觀察性實際上很低,因為很難使用碎片數據對問題進行關聯和鑑定。

從一開始就具有良好且一致的可觀察性非常重要,因此我們的 DevOps 團隊提出了一致的可觀察性策略,並構建了支持實現這一目標的工具。每個服務都會自動獲取詳細的 DataDog 儀表盤、警報和日誌搜索,這些服務在所有服務中是一致的。我們還大量使用 LightStep 來了解系統的性能。

無需從頭構建新服務

在微服務架構中,每個服務都專注於做好一件事。請注意,它與如何構建服務無關。從單體服務遷移時,如果可以從單體應用中剝離,就無需從頭開始構建微服務。

務實的說,是否應該從頭開始構建服務取決於兩個因素:

  1. Node.js 是否適合新任務;
  2. 在不同的技術棧中重新實現的成本是多少。

如果 Node.js 是一個不錯的技術選擇,並且現有的實現方式沒有問題,我們就將代碼從單體應用中刪除,並用它創建一個微服務。即使採用相同的實現,我們仍將獲得微服務架構的所有好處。

我們的 Node.js 單體應用的架構使我們可以相對輕鬆地使用現有實現構建單獨的服務。我們將在本文稍後討論如何正確構建單體。

重視發生的故障

在分佈式環境中,故障的可能性會更高。如果處理不當,關鍵服務的故障可能是災難性的。我們應該始終考慮如何測試故障並優雅地處理故障。

  • 首先,要明白故障無處不在。
  • 對於 RPC 調用,需要對故障進行額外處理。
  • 確保發生故障時具有良好的可觀察性(如上所述)。
  • 新服務上線前必須進行故障測試。它應該是新服務檢查列表的一部分。
  • 儘可能自動恢復。

從一開始就避免微服務綜合症

微服務不是靈丹妙藥,它解決了一些問題,但也創造了一些其他問題,我們將其稱為“ 微服務綜合症 ”。如果我們一開始就不去考慮它們,那麼事情很快會變得很麻煩,處理起來也要付出更多代價。以下是一些常見症狀。

  • 建模不良的微服務弊大於利,特別是在數量龐大時。
  • 允許選擇太多不同的語言 / 技術,這會增加運維成本並使工程團隊碎片化。
  • 將運行服務與構建服務相結合,這極大地增加了服務的複雜性並拖慢了團隊的速度。
  • 忽略數據建模,最終得到具有單體數據存儲的微服務。
  • 缺乏可觀察性,難以對性能問題和故障進行定位。
  • 遇到問題時,團隊傾向於創建新服務而不是修復現有服務,即使後者可能是更好的選擇。
  • 即使服務松耦合,缺乏系統的整體視角也可能存在問題。

我們應該停止構建單體服務嗎?

隨著最近的技術創新,採用微服務架構變得更加容易。這是否意味著我們都應該停止構建單體服務?

答案是否定的。雖然新技術可以更好的支持微服務架構,但仍然存在較高的複雜度和難度。對於小型團隊來說,單體應用仍然是更好的選擇。但是,可以花時間對單體應用進行更好的架構設計,以便於在系統和團隊成長後,可以更方便的遷移到微服務架構中。

從單體結構開始沒什麼問題,但確保模塊化並使用上述三種微服務原則(單一目的、松耦合和高內聚)來構建它,除了“服務”在同一技術棧中實現,部署在一起並在同一個進程中運行。

在 Medium,我們在早期的單體應用中做出了一些很好的架構決策。

我們的單體應用的組件是高度模塊化的,儘管它已經發展成為一個非常複雜的應用,包括 Web 服務器、後端服務和離線事件處理器。離線事件處理器單獨運行,但使用完全相同的代碼。這使得將一大塊業務邏輯剝離到單獨的服務相對容易,只要新服務提供與原始實現相同(上層)的接口即可。

我們的單體應用程序在較低級別封裝了數據存儲的細節。每種數據類型(例如,數據庫表)具有兩層實現:數據層和服務層。

  • 數據層處理一個特定類型數據的 CRUD 操作。
  • 服務層處理一個特定類型數據的上層邏輯和為系統的其餘部分提供公共 API。服務之間不共享數據存儲。

這有助於我們採用微服務架構,因為數據類型的實現細節對其他部分代碼完全隱藏。創建新服務來處理某一類型的數據相對容易且安全。

單體應用還可以幫助我們對微服務進行建模,讓我們能夠靈活地專注於系統中最重要的部分,而不是從頭開始為所有微服務建模。

結 論

單體 Node.js 應用已經為我們服務了好幾年,但它開始拖慢我們交付大項目和快速迭代的速度。我們開始系統性和戰略性地採用微服務架構。我們仍然處於這一旅程的早期階段,但我們已經看到了它的優勢和潛力,它極大的提高了開發效率,使我們能夠大膽思考並大幅提升產品,同時解放工程團隊,能夠安全地測試新技術。

英文原文

https://medium.engineering/microservice-architecture-at-medium-9c33805eb74f



分享到:


相關文章: