天天看點

反向代理服務如何做好過載保護

假設有一個反向代理服務(如下圖所示), 負責将上遊請求按照一定規則轉發給下遊,并将下遊傳回結果再傳回給上遊,如何才能使Proxy更好的自我保護及保護後端服務,防止出現過載,甚至出現雪崩呢?

反向代理服務如何做好過載保護

【過載介紹】

什麼是過載:

在伺服器開發中,過載指的是外部請求量已經超過了系統的最大處理能力。比如,假設某系統每秒最多處理100條請求,但是它每秒收到的請求有200條,這時,我們就說系統已經過載。

過載時表現:

過載時,每個請求響應的時間都比以往所需要的時間更久,如果系統在過載的時候沒有做到相應保護,導緻曆史積累的逾時請求達到一定規模,像雪球一樣形成惡性循環,會導緻系統處理的每個請求都因為逾時而無效,系統對外呈現的服務能力為0,且這種情況下不能自動恢複。進一步,如果整個系統由多個相關聯的子系統組成,某子系統的故障通過耦合關系會引起其他子系統發生故障,最終會導緻整個系統可用性嚴重下降甚至完全不可用。(我們稱這種現象為相繼故障,或級聯故障,英文名cascading failure, 有時候也稱為雪崩。)

【過載案例】

下面通過一個具體案例來分析下過載現象。

如下圖所示, 子產品A是一個使用Reactor網絡程式設計模式的純轉發系統,采用多線程并行的方式将使用者的請求轉發到子產品B,并同步得到子產品B的傳回結果,傳回給使用者。

反向代理服務如何做好過載保護

上圖中展現了我們這次分析中需要了解的相關内部結構,其中:

  • 核心為每個連接配接都建立了一個Recv-Q和Send-Q。
  • (IO多路複用+非阻塞)元件為網絡架構的内部實作方式。

對于單個請求,它的處理邏輯可以如下描述:

  Step1: 從Socket緩沖區(Recv-Q)接受使用者請求并解析

  Step2: 進行本地邏輯處理

  Step3: 發送請求到後端子產品B

  Step4: 同步等待後端子產品B傳回

  Step5: 接收後端子產品B的應答

  Step6: 應答前端使用者

正常情況下,假設:

  • 使用者請求子產品A的封包大小100Bytes,假設隻有一個使用者請求子產品A,采用長連接配接形式,請求峰值QPS為800。
  • 子產品A采用10個線程并行處理請求,每個連接配接設定的接受緩沖區(Recv-Q)大小為:229376Bytes(此值為某線上機器的預設值)。
  • 子產品A在處理請求時,做純轉發操作,本地運算耗時非常少,可以忽略不計。
  • 後端子產品B的處理能力很高,可以處理的極限QPS為10000次以上,且請求處理延遲不超過10ms。
  • 上遊對子產品A定義的讀逾時時間為2s,子產品A對子產品B定義的讀逾時時間為1s。

根據前面的假設,我們可以得到以下資料:

  • 子產品A在正常情況下可以處理的極限QPS為:1000。計算方法:單線程每秒可以處理1000(1s) / 10 = 100個請求,10個線程并行處理則可以處理10 * 100 = 1000。
  • 子產品A的Recv-Q平均可以緩存的請求數為:22937個。計算方法:Recv-Q大小 / 每個請求包大小。

【過載分析】

導火索:

某天子產品B進行了新特性釋出,每個請求處理延時從10ms增長到40ms,這時,随着時間推移,發現所有經過子產品A的請求都逾時, 子產品A的對外處理能力變為0。

分析:

正常情況下子產品A最大處理QPS為1000, 而使用者的請求峰值QPS是800,子產品A足以将其處理完成。當子產品B的每個請求處理延時從10ms增長到40ms時,這時候子產品A的最大處理QPS變成了(1000 / 40) * 10 = 250,遠小于800qps。因為請求量和處理能力的差距,每秒鐘有550個(800-250)請求無法及時處理,被緩存到Recv-Q,并且使得緩沖區在4s内被填滿(每秒550個積壓請求,每個請求100占100位元組,緩沖區一共229376位元組,229376 / (550*100) = 4s)。在壓力不變的情況下,子產品A的緩沖區将一直保持滿的狀态。 這意味着,一個請求被追加到緩沖區裡後,要等待91秒(緩存22937個請求,每秒處理250個,需要91秒)才能被子產品A取出來處理,這時候使用者早就已經逾時了。也就是說,程序A每次處理的請求都已經是91s以前産生的,子產品A一直在做無用功。對外處理能力表現為0。下圖可以比較直覺的展現Recv-Q中請求被處理的延遲。

反向代理服務如何做好過載保護

其中,最短等待處理時間“指的是請求從“發送到子產品A”到“被子產品A開始處理”時等待的最短時間。比如:第1-250條請求最短等待時間為0,如果請求是同一時刻發送過來的,那麼理論上前10條請求等待時間為0s(10個線程同時處理),第10到20個請求等待處理時間為20ms(每個線程處理耗時為20ms,處理完後再取新的請求),20到20個請求等待處理時間為40ms,以此類推。

QA:

問: 一般的Reactor網絡架構中都會有IO線程和Worker線程,IO線程recv資料時應該很快啊,為什麼Recv-Q(接受緩沖區)還會滿呢?

答: 需要了解Reactor本質及各種模式:在類似per thread one loop模式中IO線程同時也是Worker線程,當Worker處理阻塞時,自然沒法及時的IO;如果架構在實作時将IO線程獨立開來,IO線程負責recv并解析資料之後發送給消息隊列,并且在隊列滿時丢棄消息,這種方式可以避免Recv-Q阻塞導緻系統完全不可用,但是還沒有發現哪個架構采用這種方式。

【過載的根本原因】

除了上面講的因下遊子產品更新導緻過載外,還有其他可能引起過載的原因,比如:

  • 下遊子產品B大規模故障:類似于子產品A通路子產品B的延遲由10ms變成了“逾時時間”。
  • 上遊請求子產品A的流量劇增:比如因為cache擊穿或者秒殺活動等導緻流量劇增。
  • 同機其他子產品占用過多CPU或者網卡資源,導緻子產品A的處理性能降低。

以上所有原因都可以歸結為一點:請求量大于處理能力。

【Proxy過載保護設計】

目标: 

Proxy進行預防過載保護的目标是:在系統過載時,服務還能提供一個穩定的處理能力。如下圖所示,在發生過載時系統還能保持“處理成功QPS”的穩定性。

反向代理服務如何做好過載保護

思路:

各個層級首先要做好自我保護,然後再考慮對關聯系統的保護,主要思路是從三個方面入手:

  • 對Proxy子產品自身進行保護,避免在Proxy層出現“雪崩”: Proxy子產品需要做到在某個下遊出現大規模故障或整體不可用時,對其他子產品的轉發不影響。
  • 對Proxy的下遊子產品進行保護,盡量避免下遊出現過載: 鑒于Proxy的下遊子產品的複雜性,不能保證所有的下遊子產品都具備過載保護能力,是以需要在Proxy層進行保護,避免下遊子產品出現過載。
  • 當下遊子產品出現過載時,能保證下遊子產品及時恢複。

下面對以上三個方面展開讨論。

對Proxy子產品自身進行保護,避免在Proxy層出現“雪崩”

Proxy由多個下遊組成,有可能出現某個下遊子產品因為功能更新導緻平響升高,或者某個Bug引發服務不可用的情況。這樣會導緻Proxy整體的轉發性能降低,并引發過載或者雪崩,影響整個下遊的轉發。“資源隔離”可以防止這種情況的發生。資源隔離主要有兩種方法,一種是線程層面的隔離,一種是部署層面的隔離。

  • 線程層面隔離如下圖:
反向代理服務如何做好過載保護

它的主要思想是線上程層面,為下遊子產品配置設定好資源。假設Proxy一共開啟了100個處理線程,我們規定下遊子產品A隻能使用30個線程,子產品B和子產品C分别隻能使用20個線程。當子產品A使用的線程數達到門檻值後,主動拒絕新來的請求。這樣即使子產品A出現故障,也不會影響整個Proxy。

缺點: 很多系統都是使用的通用網絡架構,而多數通用網絡架構本身就使用了線程池,這種線程隔離的思路需要在網絡架構層之上再次封裝另一個線程池,很難做到高性能的實作。

  • 部署層面的隔離如下圖:
反向代理服務如何做好過載保護

這種隔離的主要思路是輕重分離,為某些流量大或者對系統SLA要求比較高的下遊單獨部署Proxy,做到實體層的隔離。跟線程層面隔離相比,這種隔離比較簡單,不需要改動代碼。

缺點:上遊還需要根據不同的請求分發到不同的Proxy中。

在實際生産環境中,部署層面隔離是一個比較好的選擇。

對Proxy的下遊子產品進行保護,盡量避免下遊出現過載

可以通過以下幾點避免下遊子產品出現過載:

  • 選擇好的負載均衡政策:線上上環境中,同一子產品的不同執行個體(或副本)所在機器的型号及負載各不相同,這就導緻不同執行個體的處理性能不同。好的負載均衡政策可以将上遊流量更多的分擔到性能更好的機器上,并在某個執行個體出現異常時,能很快的将其壓力分擔到其他執行個體中,最大化避免下遊故障。
  • 合理配置重試政策:重試最好隻在連接配接出錯時發起,防止系統讀寫逾時時頻繁重試導緻流量加劇。
  • 合理選擇并發控制政策:Proxy根據後端負載能力設定一個最大并發值,超過最大并發時降低向下轉發速率或者直接丢棄請求。設定最大并發的方法有兩種,一種是全局并發控制,另一種方式是單機并發控制。假設Proxy有2個執行個體proxy-1,proxy-2;對應的下遊A有三個執行個體A-1,A-2,A-3。A的每個執行個體可以處理的最大QPS為10,那麼子產品A可以處理的最大QPS為20。所謂單機并發控制,就是對每個Proxy執行個體進行并發設定,分别設定proxy-1和proxy-2往下遊轉發的最高QPS位30/2 = 15。所謂全局并發控制是在Proxy層引入一個并發的全局計數(比如使用redis或mmap共享記憶體),每次proxy往下遊轉發時都檢查此全局計數是否達到30。如果達到30,則認為超過最大并發。這兩種控制方式各有優缺點:
  • 單機并發控制:

    優點:并發配置簡單,無需與其他執行個體耦合。

    缺點:它的前提條件是上遊到達每個Proxy執行個體的流量是均勻的。當上遊采用帶有優先級的算法通路Proxy執行個體時,此方法會失效。

  • 全局并發控制:

    優點:無需限定上遊通路Proxy采用的負載均衡政策。

    缺點:需要各Proxy執行個體實時上報并發資訊到全局并發計數子產品,實作起來有一定複雜度(比如時效性因素、單點壓力因素等)。

在下遊子產品出現過載時,能保證下遊及時恢複

前面講過,過載恢複的條件隻有一個:請求量低于處理能力。當請求量低于處理能力時,“緩沖區”才會排空,系統才能恢複正常。

降低請求量的方法有兩種,一種是暴力的采用降級方式,按一定的比例丢棄流量;另一種是采用類似斷路器的方式,根據下遊系統的狀态自動進行調整。

下面詳細介紹斷路器方式。如下圖所示,是傳統的斷路器方式示意圖:

反向代理服務如何做好過載保護

它有三個狀态,分别為:斷路器關(正常狀态)、斷路器開、斷路器半開。

系統會實時對下遊的響應時間進行監控:

  • 當下遊響應時間正常時,斷路器處于“關閉狀态”,此時所有流量都可以通路下遊。
  • 當下遊平響異常到一定比例(比如50%請求逾時)時,斷路器打開,所有的請求直接拒絕,不再通路後端。
  • 當經過一段斷路時間間隔後,斷路器嘗試進入半開狀态,隻允許少量請求通路下遊,如果發現下遊恢複,則關閉斷路器,否則斷路器繼續打開。

但是這種斷路器的實作方式有個比較明顯的問題是當下遊出現過載時,斷路器會拒絕所有的流量,容易引發短暫的服務不可用。

可以基于此方法進行改進:

  • 當斷路器打開時,降低向下遊發送的速度,而不是不再通路下遊。
  • 經過一段時間後當系統監控發現平響時間變得正常時,嘗試将斷路器設定成半開狀态并根據響應時間的變化,灰階的恢複向下遊發送的速度。

這種改進的斷路器思路僅僅還停留在理論層面,還沒有在生産環境中驗證過,有心的讀者可以嘗試下。

【其他過載保護思路】

前面是從代碼層面闡述了如何進行過載保護,但是過載保護是一個系統性的工程,除了代碼層面,還需要從産品和運維層面下功夫。在産品層面,當系統過載時,需要給使用者一個良好的引導,防止使用者不停的人為重試導緻系統壓力加劇。在運維層面,需要對系統建立全方面的報警機制,當系統請求量達到容量的某些門檻值後能夠快速平滑的擴容。

【本文版權歸“百度地圖開放平台”所有,轉載請與百度地圖開放平台取得聯系】

反向代理服務如何做好過載保護

繼續閱讀