記得在三年前公司由於業務發展須要,就曾經將單體應用遷移到分佈式框架上來。當時就遇到了這樣一個問題:系統僅有一個控制單元,它會調用多個運算單元,若是某個運算單元(做爲服務提供者)不可用,將致使控制單元(做爲服務調用者)被阻塞,最終致使控制單元崩潰,進而致使整個系統都面臨着癱瘓的風險。數據庫
那個時候還不知道這其實就是服務的雪崩效應,雪崩效應比如就是蝴蝶效應,說的都是一個小因素的變化,卻每每有着無比強大的力量,以致於最後改變總體結構、產生意想不到的結果。雪崩效應也是咱們目前研發的產品直面的一道坎,下面咱們來看有哪些場景會引起雪崩,又如何避免?對於沒法避免的雪崩效應,咱們又有哪些應對措施?緩存
近年來,微服務就象一把燎原的大火,竄了出來並在整個技術社區燒了起來,微服務架構被認爲是IT軟件服務化架構演進的目標。爲何微服務這麼火,微服務能給企業帶來什麼價值?安全
咱們以耕種爲例來看如何充分利用一塊田地的:服務器
先在地裏種植了一排排玉米;網絡
後來發現玉米腳下空地能夠利用,再間隔一段距離再種上豆角,豆角長大後順着玉米杆往上爬,最後牢牢地纏繞在玉米杆上;架構
再後來發現每排玉米之間的空隙地還能夠再種些土豆,土豆蔓藤之後會交織在一塊兒,肆虐在玉米腳下吞食養分物質;併發
表面看來一塊土地獲得了充分利用,實際上各農做物得不到充分的光照和適宜的養分,如此一來加大了後期除草、鬆土、施肥、灌溉及收割的成本。負載均衡
下面的耕植思路是否是更好點呢? 一整塊地根據須要分配爲若干大小土地塊,每塊地之間清晰分界,這樣就有了玉米地、土豆地、豆角地,再想種什麼劃塊地再耕做就能夠了。框架
這樣種植好處不少,好比玉米、豆角和土豆須要的養分物質是不同的,可由專業技術人員施肥;玉米,豆角和土豆分離,避免豆角藤爬上玉米,纏繞玉米不能自由生長。土豆又汲取玉米鬚要的養分物質等等問題。分佈式
軟件系統實現與農做物的種植方式其實也很相似,傳統的應用在擴展性,可靠性,維護成本上表現都不盡人意。如何充分利用大量系統資源,管理和監控服務生命週期都是頭疼的事情,軟件系統設計迫切須要上述的「土地分割種植法」。微服務架構應運而生:在微服務系統中,各個業務系統間經過對消息(字符序列)的處理都很是友好的RestAPI進行消息交互。如此一來,各個業務系統根據Restful架構風格統一成一個有機系統。
泰坦尼克號曾經是世界最大的客輪,在當時被稱爲是」永不沉沒「的,但卻在北大西洋撞上冰山而沉沒。咱們每每只看到它浮出水面的絢麗多彩,水下的基礎設施如資源規劃、服務註冊發現、部署升級,灰度發佈等都是須要考慮的因素。
複雜應用分解:複雜的業務場景可被分解爲多個業務系統,每一個業務系統的每一個服務都有一個用消息驅動API定義清楚的邊界。
契約驅動:每一個業務系統可自由選擇技術,組建技術團隊利用Mock服務提供者和消費者,並行開發,最終實現依賴解耦。
自由擴展:每一個系統可根據業務須要獨自進行擴展。
獨立部署:每一個業務系統互相獨立,可根據實際須要部署到合適的硬件機器上。
良好隔離:一個業務系統資源泄漏不會致使整個系統宕掉,容錯性較好。
服務管理:敏捷迭代後的微服務可能愈來愈多,各個業務系統之間的交互也愈來愈多,如何作高效集羣通訊方案也是問題。
應用管理: 每一個業務系統部署後對應着一個進程,進程能夠啓停。若是機器掉電或者宕機了,如何作無縫切換都須要強大的部署管理機制。
負載均衡:爲應對大流量場景及提供系統可靠性,同一個業務系統也會作分佈式部署即一個業務實例部署在多臺機器上。若是某個業務系統掛掉了,如何按需作自動伸縮分佈式方案方案也須要考慮。
問題定位:單體應用的日誌集中在一塊兒,出現問題定位很方便,而分佈式環境的問題定界定位,日誌分析都較爲困難。
雪崩問題:分佈式系統都存在這樣一個問題,因爲網絡的不穩定性,決定了任何一個服務的可用性都不是 100% 的。當網絡不穩定的時候,做爲服務的提供者,自身可能會被拖死,致使服務調用者阻塞,最終可能引起雪崩效應。
Michael T. Nygard 在精彩的《Release It!》一書中總結了不少提升系統可用性的模式,其中很是重要的兩條是:使用超時策略和使用熔斷器機制。
超時策略:若是一個服務會被系統中的其它部分頻繁調用,一個部分的故障可能會致使級聯故障。例如,調用服務的操做能夠配置爲執行超時,若是服務未能在這個時間內響應,將回復一個失敗消息。然而,這種策略可能會致使許多併發請求到同一個操做被阻塞,直到超時期限屆滿。這些阻塞的請求可能會存儲關鍵的系統資源,如內存、線程、數據庫鏈接等。所以,這些資源可能會枯竭,致使須要使用相同的資源系統的故障。在這種狀況下,它將是優選的操做當即失敗。設置較短的超時可能有助於解決這個問題,可是一個操做請求從發出到收到成功或者失敗的消息須要的時間是不肯定的。
熔斷器模式:熔斷器的模式使用斷路器來檢測故障是否已獲得解決,防止請求反覆嘗試執行一個可能會失敗的操做,從而減小等待糾正故障的時間,相對與超時策略更加靈活。
一年一度的雙十一已經悄然來臨,下面將介紹某購物網站一個Tomcat容器在高併發場景下的雪崩效應來探討Hystrix的線程池隔離技術和熔斷器機制。
咱們先來看一個分佈式系統中常見的簡化的模型。Web服務器中的Servlet Container,容器啓動時後臺初始化一個調度線程,負責處理Http請求,而後每一個請求過來調度線程從線程池中取出一個工做者線程來處理該請求,從而實現併發控制的目的。
Servlet Container是咱們的容器,如Tomcat。一個用戶請求有可能依賴其它多個外部服務。考慮到應用容器的線程數目基本都是固定的(好比Tomcat的線程池默認200),當在高併發的狀況下,若是某一外部依賴的服務(第三方系統或者自研系統出現故障)超時阻塞,就有可能使得整個主線程池被佔滿,增長內存消耗,這是長請求擁塞反模式(一種單次請求時延變長而致使系統性能惡化甚至崩潰的惡化模式)。
更進一步,若是線程池被佔滿,那麼整個服務將不可用,就又可能會重複產生上述問題。所以整個系統就像雪崩同樣,最終崩塌掉。
流量激增:好比異常流量、用戶重試致使系統負載升高;
緩存刷新:假設A爲client端,B爲Server端,假設A系統請求都流向B系統,請求超出了B系統的承載能力,就會形成B系統崩潰;
程序有Bug:代碼循環調用的邏輯問題,資源未釋放引發的內存泄漏等問題;
硬件故障:好比宕機,機房斷電,光纖被挖斷等。
線程同步等待:系統間常常採用同步服務調用模式,核心服務和非核心服務共用一個線程池和消息隊列。若是一個核心業務線程調用非核心線程,這個非核心線程交由第三方系統完成,當第三方系統自己出現問題,致使核心線程阻塞,一直處於等待狀態,而進程間的調用是有超時限制的,最終這條線程將斷掉,也可能引起雪崩;
針對上述雪崩情景,有不少應對方案,但沒有一個萬能的模式可以應對全部場景。
針對流量激增,採用自動擴縮容以應對突發流量,或在負載均衡器上安裝限流模塊。
針對緩存刷新,參考Cache應用中的服務過載案例研究
針對硬件故障,多機房容災,跨機房路由,異地多活等。
針對同步等待,使用Hystrix作故障隔離,熔斷器機制等能夠解決依賴服務不可用的問題。
經過實踐發現,線程同步等待是最多見引起的雪崩效應的場景,本文將重點介紹使用Hystrix技術解決服務的雪崩問題。後續再分享流量激增和緩存刷新等應對方案。
Hystrix 是由Netflix發佈,旨在應對複雜分佈式系統中的延時和故障容錯,基於Apache License 2.0協議的開源的程序庫,目前託管在GitHub上。
Hystrix採用了命令模式,客戶端須要繼承抽象類HystrixCommand並實現其特定方法。爲何使用命令模式呢?使用過RPC框架都應該知道一個遠程接口所定義的方法可能不止一個,爲了更加細粒度的保護單個方法調用,命令模式就很是適合這種場景。
命令模式的本質就是分離方法調用和方法實現,在這裏咱們經過將接口方法抽象成HystricCommand的子類,從而得到安全防禦能力,並使得的控制力度下沉到方法級別。
Hystrix核心設計理念基於命令模式,命令模式UML以下圖:
可見,Command是在Receiver和Invoker之間添加的中間層,Command實現了對Receiver的封裝。那麼Hystrix的應用場景如何與上圖對應呢?
API既能夠是Invoker又能夠是Reciever,經過繼承Hystrix核心類HystrixCommand來封裝這些API(例如,遠程接口調用,數據庫的CRUD操做可能會產生延時),就能夠爲API提供彈性保護了。
Hystrix之因此可以防止雪崩的本質緣由,是其運用了資源隔離模式,咱們能夠用蓄水池作比喻來解釋什麼是資源隔離。生活中一個大的蓄水池由一個一個小的池子隔離開來,這樣若是某一個水池的水被污染,也不會波及到其它蓄水池,若是隻有一個蓄水池,水池被污染,整池水都不可用了。軟件資源隔離一模一樣,若是採用資源隔離模式,將對遠程服務的調用隔離到一個單獨的線程池後,若服務提供者不可用,那麼受到影響的只會是這個獨立的線程池。
(1)線程池隔離模式:使用一個線程池來存儲當前的請求,線程池對請求做處理,設置任務返回處理超時時間,堆積的請求堆積入線程池隊列。這種方式須要爲每一個依賴的服務申請線程池,有必定的資源消耗,好處是能夠應對突發流量(流量洪峯來臨時,處理不完可將數據存儲到線程池隊裏慢慢處理)。這個你們都比較熟悉,參考Java自帶的ThreadPoolExecutor線程池及隊列實現。線程池隔離參考下圖:
線程隔離的優勢:
請求線程與依賴代碼的執行線程能夠徹底隔離第三方代碼;
當一個依賴線程由失敗變成可用時,線程池將清理後並當即恢復可用;
線程池可設置大小以控制併發量,線程池飽和後能夠拒絕服務,防止依賴問題擴散。
線程隔離的缺點:
增長了處理器的消耗,每一個命令的執行涉及到排隊(默認使用SynchronousQueue避免排隊)和調度;
增長了使用ThreadLocal等依賴線程狀態的代碼複雜性,須要手動傳遞和清理線程狀態。
(2)信號量隔離模式:使用一個原子計數器來記錄當前有多少個線程在運行,請求來先判斷計數器的數值,若超過設置的最大線程個數則丟棄該類型的新請求,若不超過則執行計數操做請求來計數器+1,請求返回計數器-1。這種方式是嚴格的控制線程且當即返回模式,沒法應對突發流量(流量洪峯來臨時,處理的線程超過數量,其餘的請求會直接返回,不繼續去請求依賴的服務),參考Java的信號量的用法。
Hystrix默認採用線程池隔離機制,固然用戶也能夠配置 HystrixCommandProperties爲隔離策略爲ExecutionIsolationStrategy.SEMAPHORE。
信號隔離的特色:
信號隔離與線程隔離最大不一樣在於執行依賴代碼的線程依然是請求線程,該線程須要經過信號申請;
若是客戶端是可信的且能夠快速返回,可使用信號隔離替換線程隔離,下降開銷。
線程池隔離和信號隔離的區別見下圖,使用線程池隔離,用戶請求了15條線程,10條線程依賴於A線程池,5條線程依賴於B線程池;若是使用信號量隔離,請求到C客戶端的信號量若設置了15,那麼圖中左側用戶請求的10個信號與右邊的5個信號量須要與設置閾值進行比較,小於等於閾值則執行,不然直接返回。
建議使用的場景:根據請求服務級別劃分不一樣等級業務線程池,甚至能夠將核心業務部署在獨立的服務器上。
熔斷器與家裏面的保險絲有些相似,當電流過大時,保險絲自動熔斷以保護咱們的電器。假設在沒有熔斷器機制保護下,咱們可能會無數次的重試,勢必持續加大服務端壓力,形成惡性循環;若是直接關閉重試功能,當服務端又可用的時候,咱們如何恢復?
熔斷器正好適合這種場景:當請求失敗比率(失敗/總數)達到必定閾值後,熔斷器開啓,並休眠一段時間,這段休眠期事後熔斷器將處與半開狀態(half-open),在此狀態下將試探性的放過一部分流量(Hystrix只支持single request),若是這部分流量調用成功後,再次將熔斷器閉合,不然熔斷器繼續保持開啓並進入下一輪休眠週期。
建議使用場景:Client端直接調用遠程的Server端(server端因爲某種緣由不可用,從client端發出請求到server端超時響應之間佔用了系統資源,如內存,數據庫鏈接等)或共享資源。
不建議的場景以下:
應用程序直接訪問如內存中的數據,若使用熔斷器模式只會增長系統額外開銷。
做爲業務邏輯的異常處理替代品。
本文從本身曾經開發的項目應用的分佈式架構引出服務的雪崩效應,進而引出Hystrix(固然了,Hystrix還有不少優秀的特性,如緩存,批量處理請求,主從分擔等,本文主要介紹了資源隔離和熔斷)。主要分三部分進行說明:
第一部分:以耕種田地的思想引出軟件領域設計的微服務架構, 簡單的介紹了其優勢,着重介紹面臨的挑戰:雪崩問題。
第二部分:以Tomcat Container在高併發下崩潰爲例揭示了雪崩產生的過程,進而總結了幾種誘發雪崩的場景及各類場景的應對解決方案,針對同步等待引出了Hystrix框架。
第三部分:介紹了Hystrix背景,資源隔離(總結了線程池和信號量特色)和熔斷機制工做過程,並總結各自使用場景。
如Martin Fowler 在其文中所說,儘管微服務架構將來須要經歷時間的檢驗,但咱們已經走在了微服務架構轉型的道路上,對此咱們能夠保持謹慎的樂觀,這條路依然值得去探索。