本文首先介紹微服務架構存在的風險,然后針對如何避免微服務架構的故障,提出了多種有效的微服務架構中的方法和技術,其中例如服務降級、變更管理、健康檢查和修復、斷路器、限流器等。
微服務架構通過定義明確的服務邊界,能有效地隔離故障。 和其他分布式系統一樣,微服務在網絡、硬件和應用層上都會存在更多的問題。由于服務之間是互相依賴,因此任何組件都可能出錯導致用戶不能訪問。為盡可能減少部分中斷帶來的影響,我們需要構建容錯能力強的服務,以從容應對發生的某些中斷。
本文在RisingStack’s Node.js Consulting & Development experience一文基礎上,介紹了構建和運維高可用的微服務架構系統中最常用的技術和架構模式。
如果讀者不熟悉上文中的模式,那并沒什么大礙。構建可靠的系統不是一踞而就的。
微服務架構的風險
微服務架構將應用邏輯拆分成服務,服務之間通過網絡交互。由于是通過網絡調用,而不是在進程中調用,因此這給需要在多個物理和邏輯組件間進行協作的系統帶來了潛在的問題和復雜性。分布式系統變得越來越復雜,也導致網絡特定故障發生的可能性增大。
相比傳統應用龐大的結構,微服務架構最大的一個優點是團隊能獨立地設計、開發和部署各自的服務。團隊能掌控各自服務的整個生命周期。這也意味者團隊無法控制服務的依賴關系,因為這些依賴的服務可能是由其他團隊管理。在微服務架構體系下,我們要牢記提供的服務由于是其他人控制,因此可能會由于發布、配置、和其他變更等原因,從而導致服務暫時不可用,而且組件之間互相獨立。
優雅的服務降級
微服務架構最大的優點之一就是當組件出現故障時,能隔離這些故障并且能做到優雅地服務降級。比如,在圖片分享應用中,當出現故障時,用戶可能無法上傳圖片,但他們依然能瀏覽、編輯和分享已上傳的圖片。
微服務故障獨立(理論上)
在大多數情況下,是很難實現上圖這種優雅地服務降級的,因為在分布式環境下,應用都是互相依賴的,開發者需要實現若干錯誤處理的邏輯(該部分在本文稍后部分討論)去應對短暫的故障和中斷。
服務互相依賴,如果無故障轉移的邏輯,則會同時失效
變更管理
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工具。
總結
實施和運維可靠的服務并不容易。這需要你付出很多努力,還要花費公司更多的成本。
可靠性有很多層次和方面,因此針對你的團隊找出合適的解決方案是相當重要的。你應該將可靠性成為業務決策流程中的一個因素,并為此分配足夠的預算和時間。
要點
1.動態環境和分布式系統-如微服務將導致更高的故障機會。
2.服務應單獨失效,實現優雅的服務降級以提升用戶體驗。
3.70%的問題是由變更引起的,恢復可用代碼并不總是壞事。
4.快速,單獨地失敗。團隊無法控制其服務依賴關系。
5.架構模式和技術,如緩存、隔離技術、斷路器和限流器有助于構建可靠的微服務。
作者簡介:
Péter Márton,是RisingStack的CTO , 擅長使用nodejs來構建微服務。他的twitter帳號為
https://twitter.com/slashdotpeter。