避免大規模故障的微服務架構設計之道

微服務架構通過定義明確的服務邊界,能有效地隔離故障。 和其他分佈式系統一樣,微服務在網絡、硬件和應用層上都會存在更多的問題。由於服務之間是互相依賴,因此任何組件都可能出錯導致用戶不能訪問。爲儘可能減少部分中斷帶來的影響,我們需要構建容錯能力強的服務,以從容應對發生的某些中斷。

微服務架構的風險

微服務架構將應用邏輯拆分成服務,服務之間通過網絡交互。由於是通過網絡調用,而不是在進程中調用,因此這給需要在多個物理和邏輯組件間進行協作的系統帶來了潛在的問題和複雜性。分佈式系統變得越來越複雜,也導致網絡特定故障發生的可能性增大。

相比傳統應用龐大的結構,微服務架構最大的一個優點是團隊能獨立地設計、開發和部署各自的服務。團隊能掌控各自服務的整個生命週期。這也意味者團隊無法控制服務的依賴關係,因爲這些依賴的服務可能是由其他團隊管理。在微服務架構體系下,我們要牢記提供的服務由於是其他人控制,因此可能會由於發佈、配置、和其他變更等原因,從而導致服務暫時不可用,而且組件之間互相獨立。

優雅的服務降級

微服務架構最大的優點之一就是當組件出現故障時,能隔離這些故障並且能做到優雅地服務降級。比如,在圖片分享應用中,當出現故障時,用戶可能無法上傳圖片,但他們依然能瀏覽、編輯和分享已上傳的圖片。

圖片描述

微服務故障獨立(理論上)

在大多數情況下,是很難實現上圖這種優雅地服務降級的,因爲在分佈式環境下,應用都是互相依賴的,開發者需要實現若干錯誤處理的邏輯(該部分在本文稍後部分討論)去應對短暫的故障和中斷。

服務互相依賴,如果無故障轉移的邏輯,則會同時失效

變更管理

Google的網站可靠性團隊發現大概70%的故障都是由於變更而引起的。當對服務進行修改時……例如發佈代碼的新版本或者改變一些配置,則總會有可能引起故障或者引入新的錯誤。

在微服務架構中,服務是互相依賴的。這就是爲什麼你需要減少故障並且儘可能降低它們的負面影響。爲了應對變更帶來的問題,你可以實施變更策略管理並且實現其自動回滾。

比如,當部署新的代碼或者修改配置時,應該分步將這些變更部署到服務實例羣中的部分實例中,並且進行監控,如果發現關鍵指標出現問題則能自動進行回滾。

變更管理-回滾部署

另一個解決方案是運行兩套生產環境。部署的時候只部署變更的應用到其中一套環境中,並且在驗證了新發布的版本符合預期後,纔將負責均衡的流量指向新的應用,這種方法稱爲“藍-綠髮布”或者“紅-黑髮布”。

回退代碼並不是壞事情。你不應該在生產環境中部署有問題的代碼,並且應該琢磨哪裏出錯了。當必要時候應該果斷回退代碼,這越早越好。

健康檢查和負載均衡

因爲故障或部署、自動擴展等原因,服務實例會不停啓動,重新啓動及停止。這使得服務暫時或一直停用。爲了避免發生這些問題,在負載均衡中應該在路由中設置忽略這些實例,因爲它們無法爲子系統或用戶提供服務。

我們可以通過外部觀察去判斷應用實例是否健康。你可以多次調用Get/health的端點(endpoint)或者通過自身服務的報告獲得相關信息。現在的服務發現解決方案會持續從實例中收集健康信息,並且設置負載均衡的路由,讓其只指向健康的實例組件。

自我修復

自我修復能幫助恢復應用。我們討論下當應用遇到崩潰狀態後,如何通過相關的步驟去自我修復。在大多數情況下,是通過外部系統監控實例的狀態,當服務出現故障一段時間後則會重啓服務。在大多數情況下,自我修復的功能是相當有用的,然而,在某些情況下由於不斷地重啓服務會帶來相關的問題。例如當服務過載或者數據庫連接超時,則會導致應用不能反饋正確的服務健康狀態。

對於一些場景-比如數據庫鏈接丟失,這個時候實現高級的自我修復功能是頗爲棘手的。在這種情況下,需要爲應用添加額外的邏輯去處理這些特例,並且讓外部系統知道服務的實例不需要立即重新啓動。

故障轉移緩存(Failover Caching)

因爲網絡問題和系統中的變更,服務通常會出現故障。然而,這些故障中斷大多是暫時的,這要歸功於自我修復和高級負載平衡的功能,我們應該找到一個解決方案,能使服務即使在出現故障的時候也能工作。這就是故障轉移緩存(Failover Caching),它能幫助爲我們的應用提供必需的數據。

失效轉移緩存通常使用兩個不同的過期日期:其中更短的日期指示在正常情況下能使用緩存的時間,而更長的一個日期則指示在故障失效的時候,能使用緩存中的數據時長。

圖片描述

故障轉移緩存

特別需要提醒的是,只有當提供過時的數據比沒有數據更好的情況下,才能使用故障轉移緩存。

要設置緩存和故障轉移緩存,可以在HTTP中使用標準響應頭。

例如,使用max-age頭可以指定某個資源爲新資源的最大時間(譯者注:意即設定max-age後,瀏覽器不再發送請求到服務器)。可以使用stale-if-error 頭去確定在出現故障的情況下,從緩存獲取資源的時間長短。

現在的CDN和負載均衡器提供了各種緩存和故障轉移的解決方案,但是你也可以在你的公司中建立一個共享庫,其中包括這些標準的可靠性解決方案。

重試邏輯(Retry Logic)

在某些情況下,我們可能無法緩存數據,或者想對數據進行變更,但是操作最終失敗了。在這種情況下,我們就可以選擇重試操作,因爲我們可以預期資源將在一段時間後恢復,或者負載均衡會將請求發送到健康的實例上。

你應該小心地爲應用程序和客戶端添加重試邏輯,因爲更大量的重試操作可能會使事情變得更糟,甚至阻止應用程序恢復。

在分佈式系統中,微服務系統重試可能會觸發多個其他請求或重試操作,並導致級聯效應。爲減少重試帶來的影響,你應該減少重試的數量,並使用指數退避算法(exponential backoff algorithm)來持續增加重試之間的延遲時間,直到達到最大限制。

由於重試是由客戶端(瀏覽器,其他微服務等)發起的,並且客戶端在處理請求前後是不知道草走失敗的,你應該爲你的應用程序提供冪等處理能力。例如,當你重試購買操作時,不應該向客戶收兩次錢。給每個事務使用唯一的冪等鍵(idempotency-key)是解決重試問題的方法。

限流器和負載開關(Rate Limiters and Load Shedders)

限流是指在一段時間內,定義某個客戶或應用可以接收或處理多少個請求的技術。例如,通過限流,你可以過濾掉產生流量峯值的客戶和微服務,或者可以確保你的應用程序在自動擴展(Auto Scaling)失效前都不會出現過載的情況。

你還可以阻止較低優先級的流量,以便爲關鍵事務提供足夠的資源。

限流器可以阻止流量峯值

另外有一種限流器,稱爲 “併發請求限流器(concurrent request limiter)”。當你有一些比較昂貴和重要的端點(endpoint),希望它不應該被調用超過指定的次數,但仍然想要提供流量服務時,這個限流器就十分有用了。

使用負載開關可以確保對於關鍵的事務總能提供足夠的資源保障。它爲高優先級的請求保留一些資源,並且不允許低優先級的事務去佔用這些資源。負載開關會根據系統的整體狀態做出決定,而不是基於單個用戶的請求桶(request bucket)大小。負載設備有助於你的系統恢復,因爲它們在持續發生故障事件時,依然能保持核心功能正常工作。

關於更多限流器和負載開關的知識,建議讀者參考Stripe的相關文章。

快速且單獨失效(Fail Fast and Independently)

在微服務體系架構中,我們希望服務可以快速、單獨地失效。爲了在服務層面隔離故障,我們可以使用隔板模式(bulkhead pattern)。可以在本文稍後看到相關介紹。

我們也希望我們的組件能夠快速失效(fail fast),因爲我們不希望等到斷開的實例直到超時。沒有什麼比掛起的請求和無響應的界面更令人失望。這不僅浪費資源,而且還會讓用戶體驗變得更差。我們的服務是互相調用的,所以在這些延遲疊加前,應該特別注意防止那些超時的操作。

你想到的第一個辦法,可能是對每個服務的調用都定義超時的級別。這種做法的問題是,你不能真正知道到底什麼是恰當的超時值,因爲當網絡故障和其他問題發生時,某些情況下只會影響一兩次操作。在這種情況下,如果只有其中一些發生超時,你可能不想拒絕所有這些請求。

我們可以說,通過使用超時(timeout)來實現微服務中的快速失敗是一種反模式,這是應該避免的。可以使用基於操作的成功/失敗統計次數的熔斷模式,而不是使用超時。

艙壁模式(Bulkheads)

在工業領域中,常使用艙壁將劃分爲幾個部分,以便在有某部分船體發生破裂時,其他部分依然能密封安然無恙。

艙壁的概念也可以在軟件開發中用於隔離資源。

通過使用艙壁模式,我們可以保護有限的資源不被用盡。例如,如果我們有兩種類型的操作的話,它們都是和同一個數據庫實例進行通信,並且數據據庫限制連接數,這時我們可以使用兩個連接池而不是使用一個共享的連接池。由於這種客戶端和資源分離,超時或過度使用池的操作不會令所有其他操作失效。

泰坦尼克號沉沒的主要原因之一是其艙壁設計失敗,水可以通過上面的甲板倒在艙壁的頂部,最後整個船淹沒。

泰坦尼克號故障的艙壁

斷路器(Circuit Breakers)

爲了限制操作的持續時間,我們可以使用超時。超時可以防止掛起操作並保證系統可以響應。然而,在微服務架構通信中使用靜態、微調的超時是一種反模式,因爲我們處於高度動態的環境中,幾乎不可能確定在每種情況下都能正常工作的準確的時間限制。

我們可以使用斷路器來處理錯誤,而不是使用小型和特定基於事務的靜態超時機制。斷路器以現實世界的電子元件命名,因爲它們的行爲是都是相同的。你可以保護資源,並通過使用斷路器協助它們進行恢。斷路器在分佈式系統中非常有用,因爲重複的故障可能會導致雪球效應,並使整個系統崩潰。

當在短時間內多次發生指定類型的錯誤,斷路器會開啓。開啓的斷路器可以拒絕接下來更多的請求 – 就像防止真實的電子流動一樣。斷路器通常在一定時間後關閉,以便爲底層服務提供足夠的空間來恢復。

請記住,並不是所有的錯誤都應該觸發斷路器。例如,你可能希望忽略客戶端問題,比如4xx響應代碼的請求,但要包括5xx服務器端故障。一些斷路器還可以有半開關狀態。在這種狀態下,服務發送第一個請求以檢查系統的可用性,同時讓其他請求失敗。如果這個第一個請求成功,則將斷路器恢復到關閉狀態並繼續接受流量。否則,保持打開狀態。

斷路器

故障測試(Testing for Failures)

你應該持續地測試系統的常見問題,以確保你的服務可各類故障環境下運行。你應經常測試故障,以讓你的團隊對可能發生的事故有所準備。

關於測試,你可以使用外部服務來識別服務實例組,並隨機終止運行組中的一個實例。通過使用這個方法,可以針對單個實例故障進行測試,你甚至可以關閉整個服務組來模擬雲提供商層面的故障中斷。

最流行的測試解決方案之一是Netflix的ChaosMonkey工具。

總結

實施和運維可靠的服務並不容易。這需要你付出很多努力,還要花費公司更多的成本。

可靠性有很多層次和方面,因此針對你的團隊找出合適的解決方案是相當重要的。你應該將可靠性成爲業務決策流程中的一個因素,併爲此分配足夠的預算和時間。

要點

  • 動態環境和分佈式系統-如微服務將導致更高的故障機會。
  • 服務應單獨失效,實現優雅的服務降級以提升用戶體驗。
  • 70%的問題是由變更引起的,恢復可用代碼並不總是壞事。
  • 快速,單獨地失敗。團隊無法控制其服務依賴關係。
  • 架構模式和技術,如緩存、隔離技術、斷路器和限流器有助於構建可靠的微服務。

原文:Designing a Microservices Architecture for Failure 
作者: Péter Márton(Twitter),RisingStack CTO,擅長使用nodejs來構建微服務 
譯者: Jackyrong

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章