一個知名網站的微服務架構最佳實現

技術瑣話發表於2019-06-17

譯者:藍夢,十餘年研發經驗,現就職於某上市網際網路公司。

作者:小馬,Medium 首席架構師。

譯者有話說,如果你的專案正在從單體升級為微服務而憂心;或者你在實踐微服務過程中手忙腳亂,本文都是你不容錯過的好文。

微服務架構的目標是幫助工程團隊更快、更安全、更高質量地交付產品。拆分服務允許團隊快速迭代的同時,保證了對系統剩餘部分的最小影響。

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

什麼是微服務架構?

首先,讓我們花一點時間來思考微服務架構應該是怎麼樣到,不應該是怎麼到。 “微服務”是解決那些過載和混亂的軟體工程的手段之一。 我們在Medium至少這樣認為:

在微服務架構中,多個鬆散耦合的服務協同工作。 每項服務都專注於一個目的,並具有相關行為和資料的高度凝聚力。

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

  • 單一職責 - 每項服務應專注於一個目的並做得好。

  • 松耦合 - 服務對彼此知之甚少。 對一項服務的更改不應要求更改其他服務。 服務之間的通訊應僅透過公共服務介面進行。

  • 高內聚性 - 每項服務將所有相關行為和資料封裝在一起。 如果我們需要構建新功能,則所有更改應僅本地化為一個服務。


一個知名網站的微服務架構最佳實現



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

  1. 如果缺少單一職責,每個微服務最終會做太多事情,成長為多個“單體”服務。 我們不會從微服務架構中獲得全部好處,我們也會支付運營成本。

  2. 如果缺少鬆散耦合,對一個服務的更改會影響其他服務,因此我們無法快速安全地釋出更改,這本應該是微服務架構的核心優勢。 更重要的是,緊密耦合引起的問題可能是災難性的,例如資料不一致甚至資料丟失。

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

於此同時,認識到微服務不應該長成那樣,也同樣重要:

微服務不是具有少量程式碼行或“微”任務的服務。 這種誤解來自“微服務”這個名字。 微服務架構的目標不是擁有儘可能多的小型服務。 只有符合上述三項原則,服務的提煉就是恰當的。

微服務不是一直使用新技術構建的服務。 儘管微服務架構允許團隊更輕鬆地測試新技術,但它並不是微服務架構的主要目標。 只有從接偶的服務中受益,團隊才能使用完全相同的技術堆疊去構建新到服務。

微服務不是必須從頭開始構建的服務。 如果您已經擁有一個架構良好的單一應用程式,請避免養成從頭開始構建每個新服務的習慣。 可能有機會直接從單體服務中提取邏輯。 同樣,上述三個原則應該仍然有效。

為什麼是現在?

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

我們現在需要採用微服務的原因是我們的Node.js單體應用程式已經成為瓶頸。

首先,最緊迫和最重要的瓶頸是其效能。 某些計算量很大且I / O很重的任務不適合Node.js. 我們一直在逐步改進整體應用程式,但事實證明它是無效的。 它的低劣效能使我們無法提供更好的產品而不會使已經非常慢的應用程式變慢。

其次,重要且有點緊迫的瓶頸是單體應用程式會拖慢產品開發速度。 由於所有工程師都在單個應用程式中構建功能,因此它們通常緊密耦合。 我們無法靈活地改變系統的一部分,因為它也可能影響其他部分。 我們也害怕做出重大改變,因為影響太大,有時甚至難以預測。 整個應用程式作為一個整體進行部署,因此如果由於一次錯誤提交導致部署停滯,那麼所有其他更改(即使它們完全正常工作)也無法順利完成釋出。 相比之下,微服務架構允許團隊更快地開發和迭代。 因為這些功能與複雜系統是解耦的,所以工程師可以專注於他們正在構建的功能。 自然的,他們就可以輕易安全地做到重大變更。

在我們新的微服務架構中,功能修改後可以在一個小時內釋出上線。同樣的,工程師不必擔心它會如何影響系統的其他部分。 專案組的團隊還探索了在開發中安全使用生產資料的方法,這在多年前是無法實現的。 隨著我們的工程團隊的發展,我們有機會去突破現狀。

第三,單塊應用程式使系統很難針對特定任務進行擴充套件,也很難針對不同型別的任務分離資源問題。使用單一的單塊應用程式,我們必須在整個系統上下擴充套件,以完成更需要資源的任務,儘管這意味著系統對其他更簡單的任務的準備過度了。為了緩解這些問題,我們分解了不同型別的請求來分離Node.js程式。它們在一定程度上起作用,但不會擴充套件,因為,同樣,這些微版本的單片服務是緊密耦合的。

最後一點也同樣重要的是,單體架構阻礙了團隊嘗試新技術。 微服務架構的一個主要優點是每個服務都可以使用不同的技術堆疊,並與不同的技術整合。 這使我們能夠選擇最適合工作的工具,於此同時,我們還可以快速安全地完成工作。

微服務策略

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

  • 建立具有明確價值的新服務

  • 單體資料儲存被認為是有害的

  • 解耦“服務構建”和“服務治理”

  • 徹底和一致的可觀察性

  • 並非每項新服務都需要從頭開始構建

  • 尊重失敗因為它們會發生

  • 從第一天開始就避免使用“微服務綜合症”

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

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

產品價值應以我們可以為使用者提供的利益為代表。 與在單體Node.js應用程式中構建值相比,需要一項新服務來提供值或使其更快地交付值。 工程價值應該使工程團隊更好,更快。

如果構建新服務沒有產品價值或工程價值,我們將其留在單一的應用程式中。 如果十年內Medium仍然有一個支援某些表面的單體Node.js應用程式,那就完全沒了問題。 從單一應用程式開始實際上有助於我們戰略性地對微服務進行建模。

單體資料儲存被認為是有害的

構建微服務的很大一部分工作是對其持久資料儲存(例如,資料庫)進行建模。跨服務共享持久資料儲存通常似乎是將微服務整合在一起的最簡單方法,然而,它實際上是有害的,我們應該不惜一切代價避免它。這就是原因。

首先,持久資料儲存是關於實現細節的。跨服務共享資料儲存會將一個服務的實現細節暴露給整個系統。如果該服務更改了資料的格式,或者新增了快取層,或者切換到不同型別的資料庫,則還必須相應地更改許多其他服務。這違反了鬆散耦合的原則。

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

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

這可能聽起來很抽象,所以這是一個具體的例子。 假設我們正在構建一個新的推薦服務,它需要來自規範帖子表的一些資料,目前在AWS DynamoDB中。 我們可以透過兩種方式之一為新推薦服務提供釋出資料。

一個知名網站的微服務架構最佳實現

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

  1. 快取可能很棘手。如果推薦服務與單體應用程式共享相同的快取,我們也必須在推薦服務中複製快取實現細節;如果推薦服務使用自己的快取,當單體應用更新帖子資料時,我們將不知道何時使其快取無效。

  2. 如果單體應用程式決定更改為使用RDS而不是DynamoDB來儲存帖子資料,我們將不得不重新實現推薦服務中的邏輯以及訪問帖子資料的所有其他服務。

  3. 單體應用程式具有解釋帖子資料的複雜邏輯,例如,如何確定帖子是否應該對給定使用者不可見。我們必須在推薦服務中重新實現這些邏輯。一旦整體應用程式更改或新增新邏輯,我們也需要在任何地方進行相同的更改。

  4. 即使推薦服務是自己的資料訪問模式的錯誤選項,推薦服務仍然停留在DynamoDB上。

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

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

當人員配置有限時,還有一些更實用的方法。根據資料訪問模式,它們實際上可能是更好的方式。在選項B中,單一應用程式可讓推薦服務知道何時更新相關的帖子資料。通常,這不必立即發生,因此我們可以將其解除安裝到排隊系統。在選項C中,ETL管道生成推薦服務的釋出資料的只讀副本,以及可能對推薦有用的其他資料。在這兩個選項中,推薦服務完全擁有其資料,因此它可以靈活地快取資料或使用最適合的資料庫技術。

解耦“服務構建”和“服務治理”

如果構建微服務很難,那麼治理服務往往更難。 當治理服務與構建每個服務相結合時,它會減慢工程團隊的速度,團隊必須不斷重新發明這樣做。 我們希望讓每項服務都專注於自己的工作而不用擔心如何執行服務的複雜問題,包括網路,通訊協議,部署,可觀察性等。服務治理應該與每個服務的實現完全分離。

將“服務構建”和“服務治理”分離的策略是使執行服務任務與服務技術無關,並且使自己的意見,以便應用工程師可以完全專注於每個服務自己的業務邏輯。

由於最近在容器化,容器編排,服務網格,應用程式效能監控等方面的技術進步,“服務治理”的解耦變得比以往更容易實現。

網格化,網格(例如,服務發現,路由,負載平衡,流量路由等)是服務治理的關鍵部分。傳統方法是為每種平臺/語言提供庫。它工作但不理想,因為應用程式仍然需要非常繁瑣的工作來整合和維護庫。通常,應用程式仍然需要單獨實現某些邏輯。現代解決方案是在Service Mesh中執行服務。在Medium,我們使用Istio和Envoy作為sidecar 代理。構建服務的應用工程師根本不需要擔心網路問題。

通訊協議。無論您選擇哪種技術堆疊或語言來構建微服務,從一個高效,型別化(typed),跨平臺且需要最少開發開銷的成熟RPC解決方案開始是非常重要的。支援向後相容性的RPC解決方案也使部署服務更加安全,即使它們之間存在依賴關係。在Medium,我們選擇了gRPC。

一個常見的替代方案是RESTJSON over HTTP,長期以來,它一直是伺服器通訊的良好解決方案。但是,儘管該堆疊非常適合瀏覽器與伺服器通訊,但它對於伺服器到伺服器的通訊效率很低,尤其是當我們需要傳送大量請求時。如果沒有自動生成的存根和樣板程式碼,我們將不得不手動實現伺服器/客戶端程式碼。可靠的RPC實現不僅僅包裝網路客戶端。另外,REST是“固執己見的”,認知存在門檻,但總是讓每個人都對每個細節都達成一致很困難,例如,這個呼叫真的是REST,還是隻是一個RPC?這是一種資源還是一種操作,諸如此類!

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

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

良好且一致的可觀察性

可觀察性包括允許我們瞭解系統如何工作的過程,約定和工具,以及在不工作時對問題進行分類。可觀察性包括日誌記錄,效能跟蹤,指標,儀表板,警報,並且對於微服務架構的成功至關重要。

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

我們失去了可觀察性,因為它變得更難或更容易被忽視。

不同的團隊重新發明了輪子,我們最終得到了零碎的可觀察性,這實際上是低可觀察性,因為很難使用碎片資料連線點或分類任何問題。

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

並非每項新服務都需要從頭開始構建

在微服務架構中,每個服務都做一件事並且做得非常好。請注意,它與如何構建服務無關。如果您從單一服務遷移,請記住,如果您可以從單體應用程式中剝離微服務並不總是必須從頭開始構建。

在這裡,我們採取務實的態度。我們是否應該從頭開始構建服務取決於兩個因素:(1)Node.js適合該任務的程度如何;(2)在不同的技術堆疊中重新實現的成本是多少。

如果Node.js是一個很好的技術選項並且現有的實現很好,我們將程式碼從單體應用程式中刪除,並用它建立一個微服務。即使採用相同的實現,我們仍將獲得微服務架構的所有好處。

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

尊重失敗,因為他們會發生

在分散式環境中,更多的東西可能會失敗,而且它們會失敗。 如果處理不當,任務關鍵型服務的失敗可能是災難性的。 我們應該始終考慮如何測試故障並優雅地處理故障。

  • 首先,我們應該期待一切都會在某些時候失敗。

  • 對於RPC呼叫,需要付出額外的努力來處理故障情況。

  • 確保我們在發生故障時具有良好的可觀察性(如上所述)。

  • 線上提供新服務時始終測試失敗。 它應該是新服務檢查列表的一部分。

  • 儘可能構建自動恢復。

從第一天起避免使用微服務綜合症

微服務不是靈丹妙藥 - 它解決了一些問題,但創造了一些其他問題,我們將其稱為“微服務綜合症”。如果我們從第一天開始就不去考慮它們,那麼事情會變得很快,如果我們以後再照顧它們會花費更多。以下是一些常見症狀。

  • 建模不良的微服務造成的傷害大於好處,特別是當你有超過幾個時。

  • 允許太多不同的語言/技術選擇,這會增加運營成本並使工程組織分散。

  • 將運營服務與構建服務相結合,這大大增加了每項服務的複雜性並減慢了團隊的速度。

  • 忽略資料建模,最終得到具有單體資料儲存的微服務。

  • 缺乏可觀察性,這使得難以對效能問題或故障進行分類。

  • 當遇到問題時,團隊傾向於建立新服務而不是修復現有服務,即使後者可能是更好的選擇。

  • 儘管這些服務是鬆散耦合的,但缺乏對整個系統的全面瞭解可能會有問題。

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

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

雖然新技術支援得更好,但微服務架構仍然存在高度複雜性和複雜性。對於小型團隊來說,單一的應用程式通常仍然是更好的選擇。但是,請花些時間來構建單體應用程式,以便以後在系統和團隊成長時更容易遷移到微服務架構。

從單一體系結構開始是很好的,但要確保模組化並使用上述三種微服務原則(單一用途,鬆散耦合和高內聚)來構建它,除了“服務”在同一技術堆疊中實現,一起部署並在同一程式中執行。

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

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

我們的整體應用程式在較低階別封裝了資料儲存詳細資訊。每種資料型別(例如,資料庫表)具有兩層實現:資料層和服務層。

  • 資料層處理對一種特定型別資料的CRUD操作。

  • 服務層處理一種特定型別資料的高階邏輯,併為系統的其餘部分提供公共API。服務不共享它們之間的資料儲存。

  • 這有助於我們採用微服務架構,因為一種型別資料的實現細節完全隱藏在程式碼庫的其餘部分。建立新服務來處理某些型別的資料相對容易且安全。


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

總結

單體Node.js應用程式為我們服務了好幾年,但它開始減慢我們的迭代。我們開始系統地和戰略性地採用微服務架構。我們仍處於這一旅程的早期階段,但我們已經看到了它的優勢和潛力 - 它大大提高了開發效率,使我們能夠大膽地思考並實現大量的產品改進,並解鎖了工程團隊以安全地測試新技術。

謝謝閱讀。如果您有任何疑問或希望更多地討論我們如何開始採用微服務架構,請給我們留言。

原文:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562044/viewspace-2647903/,如需轉載,請註明出處,否則將追究法律責任。

相關文章