從服務雪崩到Hystrix

2019-11-09     sandag

一、前言

微服務架構以其輕量級、易擴展、穩定性高等特點,近幾年受到了熱烈的追捧,從網際網路項目到企業級系統,都紛紛開始採用微服務架構。 一般情況下,我們會按照業務對服務進行拆分,再通過接口實現服務之間的相互調用,從而實現服務的獨立部署和維護。 但隨著服務數量的增多,也會遇到一些單體服務中不會出現的問題,服務雪崩就是其中的典型。 今天我們就來聊一下服務雪崩產生的原因及其防治方法。

二、服務雪崩

2.1 定義

服務雪崩產生於服務堆積在同一個線程池中,因為所有的請求都是同一個線程池進行處理,這時候如果在高並發情況下,所有的請求全部訪問同一個接口,這時候可能會導致其他服務沒有線程進行接受請求,這就是服務雪崩。

2.2 產生過程

下面我們以五個服務為例(調用關係如下圖所示),演示一下服務雪崩發生的過程:

第一階段: 服務E由於故障或負載過高,無法及時向上游服務C和D返迴響應,服務C和D的請求線程處於等待狀態;

第二階段: 服務C和D收不到E的響應,開始加大重試,同時又收到上游服務請求,導致更多的線程處於等待狀態,最終線程池被耗盡,此時服務C和D也處於無法服務狀態。

第三階段: 隨著時間的推移,這種服務不可用的影響被逐漸放大,由於相同的原因,服務A和B也因為線程耗盡而無法對上游提供服務,於是整個調用鏈路崩潰,服務出現大面積癱瘓,服務雪崩就形成了。

2.3 產生原因

服務雪崩的每個階段都可能由不同的原因造成, 比如造成服務不可用的原因有:

  • 硬體故障

硬體故障可能為硬體損壞造成的伺服器主機宕機,網絡硬體故障造成的服務提供者的不可訪問。

  • 緩存擊穿

緩存擊穿一般發生在緩存應用重啟, 所有緩存被清空時,以及短時間內大量緩存失效時大量的緩存不命中,使請求直擊後端,造成服務提供者超負荷運行,引起服務不可用。

  • 用戶大量請求

在秒殺和大促開始前,如果準備不充分,用戶發起大量請求也會造成服務提供者的不可用。在服務提供者不可用後,用戶由於忍受不了介面上長時間的等待,而不斷刷新頁面甚至提交表單,導致後端服務壓力雪上加霜。服務調用端的會存在大量服務異常後的重試邏輯。

  • 程序BUG

如程序邏輯導致內存泄漏,JVM長時間Full GC等。

  • 同步等待

服務之間採用同步調用模式,同步等待造成的資源耗盡。

三、解決方案

雪崩效應之所以這麼被重視是因為它極容易在被人們忽視的情況下發生,對微服務而言,服務實例成百上千,我們很難一個個服務地檢查以保證每個服務的質量,並且很多情況下只有在達到一定壓力問題才會暴露,常規的代碼Reivew或是針對單個服務的壓力測試未必可以發現問題,再則這些依賴服務未必都是我們自己的服務,如果說我們自己服務尚有一定的排查優化方法的話那麼對三方服務依賴而言那幾乎只能是憑經驗了,只要我的依賴服務中存在一處不起眼的Bug,或是過少的連接池配置,抑或是網絡波動都有可能引發雪崩。 怎麼有效地避免呢?

3.1 服務隔離

從服務雪崩的過程我們可以知道,服務雪崩的根本原因是長期等待下游服務接口響應,而導致線程池被耗盡,從而無法處理上游請求。 如果為每個下游服務接口創建一個獨立的線程池,每個線程池互不影響,當某個下游服務接口無法響應時,只有其對應的線程池被耗盡,其他服務接口並不受到影響。

3.2 流量控制

當發現服務失敗數量達到某個閾值,拒絕訪問,限制更多流量的到來,防止過多失敗的請求將資源耗盡。

3.3 緩存

對下游服務正常響應的數據進行緩存,之後一段時間內直接向上游返回緩存中的數據。 這樣可以有效降低對下游服務質量的敏感度,在一定程度上提升服務的穩定性。

3.4 服務熔斷

當下游的服務因為某種原因突然變得不可用或響應過慢,上游服務為了保證自己整體服務的可用性,當目標服務超過設定最長等待時間未響應時,直接終止等待,快速釋放資源。 如果目標服務情況好轉則恢復調用。

3.5 服務降級

在高並發情況下,為防止用戶一直等待,可以使用服務降級的方式: 對於簡單的展示功能,如果有失敗的請求,返回默認值; 對於整個站點或客戶端,如果伺服器負載過高,將其他非核心業務停止,以讓出更多資源給其他服務使用。

四、Hystrix

前面介紹了服務隔離、流量控制、緩存、熔斷降級等解決方案,很多小夥伴可能感覺到頭大: 理想很豐滿,現實很骨感,手頭的需求都做不完,更別說再花時間去實現這些高大上的技術方案了。 很幸運的是,得益於偉大的開源精神,Netflix為我們帶來了一款開源的基於Java語言開發的服務雪崩預防神器: Hystrix。

4.1 介紹

Hystrix的中文含義是豪豬, 因其背上長滿了刺,而擁有自我保護能力。 Hystrix 是一個通過增加延遲容錯和容錯邏輯來控制分布式服務之間交互的一個庫。 Hystrix通過線程隔離,防止錯誤級聯傳遞,導致服務雪崩,從而提高服務穩定性。

4.2 Hystrix的主要目標

  1. 通過隔離第三方客戶端庫訪問依賴關係,防止和控制延遲和故障;
  2. 防止複雜分布式系統的級聯失敗;
  3. 快速響應失敗並迅速恢復;
  4. 提供回滾以及友好降級;
  5. 實現近實時監控,告警和操作控制。

4.3 Hystrix設計原則

  1. 防止單個依賴耗盡了服務容器的用戶線程;
  2. 降低負載以及快速失敗,而不是排隊;
  3. 當可以阻止服務的失敗時提供回退策略;
  4. 使用隔離技術減少任意依賴的影響;
  5. 通過近實時指標、監控和告警優化發現時間;
  6. 在Hystrix的大多數方面,通過配置更改的低延遲和對動態屬性更改的支持,使得可以在低延遲的情況下進行實時修改操作,從而優化恢復時間;
  7. 防止整個依賴關係客戶端執行中的故障,而不僅僅是網絡流量。

4.4 Hystrix如何做到上面的目標

  1. 所有外部的調用都封裝到 HystrixCommand 或 HystrixObservableCommand 對象,這些對象通常在單獨的線程下執行;
  2. 超時調用的時間,超過定義的閾值。有一個默認值,但是對於大多數的依賴,你可以自定義該屬性使得略高於每個依賴測量的99.5%的性能;
  3. 為每一個依賴項維護一個線程池(或者信號),如果依賴項的線程池滿了,新的依賴請求不會繼續排隊等待,而是馬上被拒絕訪問;
  4. 計算成功、失敗、超時和線程拒絕的數量;
  5. 如果依賴服務的失敗百分比超過閾值,則手動或自動啟動斷路器,在一段時間內停止對指定服務的所有請求;
  6. 為請求失敗、被拒絕、超時或短路情況提供回退邏輯;
  7. 近乎實時地監控指標和配置更改。

4.5 Hystrix處理流程

Hystrix整個工作流如下:

!圖片來源Hystrix官網https://github.com/Netflix/Hystrix/wiki

  1. 構造一個 HystrixCommand 或 HystrixObservableCommand對象,用於封裝請求,並在構造方法配置請求被執行需要的參數;
  2. 執行命令,Hystrix提供了4種執行命令的方法,後面詳述;
  3. 判斷是否使用緩存響應請求,若啟用了緩存,且緩存可用,直接使用緩存響應請求。Hystrix支持請求緩存,但需要用戶自定義啟動;
  4. 判斷熔斷器是否打開,如果打開,跳到第8步;
  5. 判斷線程池/隊列/信號量是否已滿,已滿則跳到第8步;
  6. 執行HystrixObservableCommand.construct()或HystrixCommand.run(),如果執行失敗或者超時,跳到第8步;否則,跳到第9步;
  7. 統計熔斷器監控指標;
  8. 走Fallback備用邏輯;
  9. 返回請求響應。

從流程圖上可知道,第5步線程池/隊列/信號量已滿時,還會執行第7步邏輯,更新熔斷器統計信息,而第6步無論成功與否,都會更新熔斷器統計信息。

4.6 Hystrix簡單使用

第一步,繼承HystrixCommand實現自己的command,在command的構造方法中需要配置請求被執行需要的參數,並組合實際發送請求的對象,代碼如下:

第二步,調用HystrixCommand的執行方法發起實際請求。

4.7 執行命令的幾種方法

Hystrix提供了4種執行命令的方法,execute()和queue() 適用於HystrixCommand對象,而observe()和toObservable()適用於HystrixObservableCommand對象。

execute()

以同步堵塞方式執行run(),只支持接收一個值對象。 Hystrix 會從線程池中取一個線程來執行run(),並等待返回值。

queue()

以異步非阻塞方式執行run(),只支持接收一個值對象。 調用queue()就直接返回一個Future對象。 可通過 Future.get()拿到run()的返回結果,但Future.get()是阻塞執行的。 若執行成功,Future.get()返回單個返回值。 當執行失敗時,如果沒有重寫fallback,Future.get()拋出異常。

observe()

事件註冊前執行run()/construct(),支持接收多個值對象,取決於發射源。 調用observe()會返回一個hot Observable,也就是說,調用observe()自動觸發執行run()/construct(),無論是否存在訂閱者。

如果繼承的是HystrixCommand, Hystrix 會從線程池中取一個線程以非阻塞方式執行run(); 如果繼承的是HystrixObservableCommand,將以調用線程阻塞執行construct()。

observe()使用方法:

  1. 調用observe()會返回一個Observable對象
  2. 調用這個Observable對象的subscribe()方法完成事件註冊,從而獲取結果

toObservable()

事件註冊後執行run()/construct(),支持接收多個值對象,取決於發射源。 調用toObservable()會返回一個cold Observable,也就是說,調用toObservable()不會立即觸發執行run()/construct(),必須有訂閱者訂閱Observable時才會執行。

如果繼承的是HystrixCommand, Hystrix 會從線程池中取一個線程以非阻塞方式執行run(),調用線程不必等待run(); 如果繼承的是HystrixObservableCommand,將以調用線程堵塞執行construct(),調用線程需等待construct()執行完才能繼續往下走。

toObservable()使用方法:

  1. 調用observe()會返回一個Observable對象
  2. 調用這個Observable對象的subscribe()方法完成事件註冊,從而獲取結果

需注意的是,HystrixCommand也支持toObservable()和observe(),但是即使將HystrixCommand轉換成Observable,它也只能發射一個值對象。 只有HystrixObservableCommand才支持發射多個值對象。

4.8 幾種方法的關係

  • execute()實際是調用了queue().get()
  • queue()實際調用了toObservable().toBlocking().toFuture()
  • observe()實際調用toObservable()獲得一個cold Observable,再創建一個ReplaySubject對象訂閱Observable,將源Observable轉化為hot Observable。因此調用observe()會自動觸發執行run()/construct()。

Hystrix總是以Observable的形式作為響應返回,不同執行命令的方法只是進行了相應的轉換。

五、總結

微服務架構 相對於單體架構 的確有很多優勢,但它並不是 銀彈, 如果我們能 對微服務架構可能出現的問題 進行 預防, 使它的優勢充分發揮出來,相信它會給我們帶來很多的收益 。通過今天的學習,相信大家對服務雪崩的 產生原因及其防治方法已經有了一定的了解,希望本篇文章能為大家在以後工作需要時帶來一些幫助。

文章來源: https://twgreatdaily.com/zh-tw/PXuUUW4BMH2_cNUgBbXW.html