天天看點

分布式系統關注點——99%的人都能看懂的「熔斷」以及最佳實踐

當我們工作所在的系統處于分布式系統初期的時候,往往這時候每個服務都隻部署了一個節點。

那麼在這樣的背景下,如果某個服務A需要釋出一個新版本,往往會對正在運作的其它依賴服務A的程式産生影響。甚至,一旦服務A的啟動預熱過程耗時過長,問題會更嚴重,大量請求會阻塞,産生級聯影響,導緻整個系統卡慢。

舉個誇張的例子來形容:一幢樓的下水管是從最高樓直通到最低樓的,這個時候如果你家樓下的管道口堵住了,那麼所有樓上的污水就會倒灌到你家。如果這導緻你家的管道口也堵住了,之後又會倒灌到樓上一層,以此類推。

然而實際生活中一旦你發現了這個問題,必然會想辦法先避免影響到自己家,然後跑到樓下讓他們趕緊疏通管道。此時,避免影響自己家的辦法就可被稱之為「熔斷」。

一、熔斷是什麼

熔斷本質上是一個過載保護機制。這一概念來源于電子工程中的斷路器,可能你曾經被這個東西的“跳閘”保護過。

在網際網路系統中的熔斷機制是指:當下遊服務因通路壓力過大而響應變慢或失敗,上遊服務為了保護自己以及系統整體的可用性,可以暫時切斷對下遊服務的調用。

做熔斷的思路大體上就是:一個中心思想,分四步走。

二、熔斷怎麼做

首先,需秉持的一個中心思想是:量力而行。因為軟體和人不同,沒有奇迹會發生,什麼樣的性能撐多少流量是固定的。這是根本。

然後,這四步走分别是:

定義一個識别是否處于“不可用”狀态的政策

切斷聯系

定義一個識别是否處于“可用”狀态的政策,并嘗試探測

重新恢複正常

定義一個識别是否處于“不正常”狀态的政策

相信軟體開發經驗豐富的你也知道,識别一個系統是否正常,無非是兩個點。

是不是能調通

如果能調通,耗時是不是超過預期的長

但是,由于分布式系統被建立在一個并不是100%可靠的網絡上,是以上述的情況總有發生,是以我們不能将偶發的瞬時異常等同于系統“不可用”(避免以偏概全)。由此我們需要引入一個「時間視窗」的概念,這個時間視窗用來“放寬”判定“不可用”的區間,也意味着多給了系統幾次證明自己“可用”機會。但是,如果系統還是在這個時間視窗内達到了你定義“不可用”标準,那麼我們就要“斷臂求生”了。

這個标準可以有兩種方式來指定。

門檻值。比如,在10秒内出現100次“無法連接配接”或者出現100次大于5秒的請求。

百分比。比如,在10秒内有30%請求“無法連接配接”或者30%的請求大于5秒。

最終會形成這樣這樣的一段代碼。

全局變量 errorcount = 0; //有個獨立的線程每隔10秒(時間視窗)重置為0。

全局變量 isOpenCircuitBreaker = false;

//do some thing...

if(success){

return success;           

}

else{

errorcount++;
if(errorcount == 不可用門檻值){
    isOpenCircuitBreaker = true;
}           

切斷聯系要盡可能的“果斷”,既然已經認定了對方“不可用”,那麼索性就預設“失敗”,避免做無用功,也順帶能緩解對方的壓力。

分布式系統中的程式間調用,一般都會通過一些RPC架構進行。

那麼,這個時候作為用戶端一方,在自己程序内通過代理發起調用之前就可以直接傳回失敗,不走網絡。

這就是常說的「fail fast」機制。就是在前面提到的代碼段之前增加下面的這段代碼。

if(isOpenCircuitBreaker == true){

return fail;           

切斷聯系後,功能的完整性必然會受影響,是以還是需要盡快恢複回來,以提供完整的服務能力。這事肯定不能人為去幹預,及時性必然會受到影響。那麼如何能夠自動的識别依賴系統是否“可用”呢?這也需要你來定義一個政策。

一般來說這個政策與識别“不可用”的政策類似,隻是這裡是一個反向名額。

門檻值。比如,在10秒内出現100次“調用成功”并且耗時都小于1秒。

百分比。比如,在10秒内有95%請求“調用成功”并且98%的請求小于1秒。

同樣包含「時間視窗」、「門檻值」以及「百分比」。

稍微不同的地方在于,大多數情況下,一個系統“不可用”的狀态往往會持續一段時間,不會那麼快就恢複過來。是以我們不需要像第一步中識别“不可用”那樣,無時無刻的記錄請求狀況,而隻需要在每隔一段時間之後去進行探測即可。是以,這裡多了一個「間隔時間」的概念。這個間隔幅度可以是固定的,比如30秒。也可以是動态增加的,通過線性增長或者指數增長等方式。

這個用代碼表述大緻是這樣。

全局變量 successCount = 0;

//有個獨立的線程每隔10秒(時間視窗)重置為0。

//并且将下面的isHalfOpen設為false。

全局變量 isHalfOpen = true;

//有個獨立的線程每隔30秒(間隔時間)重置為true。

if(isHalfOpen){
    successCount ++;
    if(successCount = 可用門檻值){
        isOpenCircuitBreaker = false;
    }
}

return success;           
errorcount++;
if(errorcount == 不可用門檻值){
    isOpenCircuitBreaker = true;
}           

另外,嘗試探測本質上是一個“試錯”,要控制下“試錯成本”。是以我們不可能拿100%的流量去驗證,一般會有以下兩種方式:

放行一定比例的流量去驗證。

如果在整個通信架構都是統一的情況下,還可以統一給每個系統增加一個專門用于驗證程式健康狀态檢測的獨立接口。這個接口額外可以多傳回一些系統負載資訊用于判斷健康狀态,如CPU、I/O的情況等。

一旦通過了衡量是否“可用”的驗證,整個系統就恢複到了“正常”狀态,此時需要重新開啟識别“不可用”的政策。就這樣,系統會形成一個循環。

這就是一個完整的熔斷機制的面貌。了解了這些核心思想,用什麼架構去實施就變得不是那麼重要了,因為大部分都是換湯不換藥。

上面聊到的這些可以說是主幹部分,還有一些最佳實踐可以讓你在實施熔斷的時候拿捏的更到位。

三、做熔斷的最佳實踐

什麼場景最适合做熔斷

一個事物在不同的場景裡會發揮出不同的效果。以下是我能想到最适合熔斷發揮更大優勢的幾個場景:

所依賴的系統本身是一個共享系統,目前用戶端隻是其中的一個用戶端。這是因為,如果其它用戶端進行胡亂調用也會影響到你的調用。

是以依賴的系統被部署在一個共享環境中(資源未做隔離),并不獨占使用。比如,和某個高負荷的資料庫在同一台伺服器上。

所依賴的系統是一個經常會疊代更新的服務。這點也意味着,越“靈活”的系統越需要“熔斷”。

目前所在的系統流量大小是不确定的。比如,一個電商網站的流量波動會很大,你能抗住突增的流量不代表所依賴的後端系統也能抗住。這點也反映出了我們在軟體設計中帶着“面向懷疑”的心态的重要性。

做熔斷時還要注意的一些地方

與所有事物一樣,熔斷也不是一個完美的事物,我們特别需要注意2個問題。

首先,如果所依賴的系統是多副本或者做了分區的,那麼要注意其中個别節點的異常并不等于所有節點都存在異常,是以需要差別對待。

其次,熔斷往往應作為最後的選擇,我們應優先使用一些「降級」或者「限流」方案。因為“部分勝于無”,雖然無法提供完整的服務,但盡可能的降低影響是要持續去努力的。比如,抛棄非核心業務、給出友好提示等等,這部分内容我們會在後續的文章中展開。

四、總結

本文主要聊了熔斷的作用以及做法,并且總結了一些我自己的最佳實踐。

上面的這些代碼示例中也可以看到,熔斷代碼所在的位置要麼在實際方法之前,要麼在實際方法之後。它非常适合AOP程式設計思想的發揮,是以我們平常用到的熔斷架構都會基于AOP去做。

熔斷隻是一個保護殼,在周圍出現異常的時候保全自身。但是從長遠來看平時定期做好壓力測試才能更好的防範于未然,降低觸發熔斷的次數。如果清楚的知道每個系統有幾斤幾兩,在這個基礎上再把「限流」和「降級」做好,這基本就将“高壓”下觸發熔斷的機率降到最低了。