高可用之限流降級
1、前言
在大規模微服務架構的場景下,為了避免服務出現雪崩,要減少停機時間,盡可能的提高服務可用性。提高服務可用性,可以從很多方向入手,比如緩存、池化、異步化、負載均衡、隊列和降級熔斷等手段。
- 緩存以及隊列等手段,增加系統的容量
- 限流和降級則是關心在到達系統瓶頸時系統的響應,更看重穩定性
緩存和異步等關注提高系統戰力,而限流降級則關注增強系統防禦,具體實施方法可以歸納為八字箴言,限流、降級、熔斷、隔離。
2、限流&降級
2.1、限流
- 限流,顧名思義,即提前對各個類型的請求設定最高的QPS門檻值,若高于設定的門檻值則對該請求直接傳回,不再調用後續資源。
- 限流需要結合壓測等,了解系統的最高水位,也是在實際開發中應用最多的一種穩定性保障手段。
2.2、降級
- 降級,則是指在系統調用高峰時,優先保證核心服務,對于非核心服務可以選擇将其關閉以保證核心服務的可用。 例如在淘寶雙11時,支付功能是核心,其他諸如使用者中心等非核心功能可以選擇降級,優先保證交易。
- 從降級配置方式上,降級一般可以分為主動降級和自動降級
- 主動降級是提前配置,自動降級則是系統發生故障時,如逾時或者頻繁失敗,自動降級。
- 其中自動降級,又可以分為以下政策:
- 逾時降級
- 失敗次數降級
- 故障降級
2.3、降級通知設計
在系統設計中,降級一般是結合系統配置中心,通過配置中心進行推送,下面是一個典型的降級通知設計:

3、 熔斷&隔離
3.1、熔斷
- 如果某個目标服務調用慢或者有大量逾時,此時熔斷該服務的調用,對于後續調用請求,不在繼續調用目标服務,直接傳回,快速釋放資源。
- 熔斷一般需要設定不同的恢複政策,如果目标服務情況好轉則恢複調用。
3.2、隔離
服務隔離與前面的三個略有差別,我們的系統通常提供了不止一個服務,但是這些服務在運作時是部署在一個執行個體,或者一台實體機上面的,如果不對服務資源做隔離,一旦一個服務出現了問題,整個系統的穩定性都會受到影響!
- 服務隔離的目的就是避免服務之間互相影響。一般來說,隔離要關注兩方面,一個是在哪裡進行隔離,另外一個是隔離哪些資源。
- 何處隔離?
- 一次服務調用,涉及到的是服務提供方和調用方,我們所指的資源,也是兩方的伺服器等資源,服務隔離通常可以從提供方和調用方兩個方面入手。
- 隔離什麼?
- 廣義的服務隔離,不僅包括伺服器資源,還包括資料庫分庫,緩存,索引等,這裡我們隻關注服務層面的隔離。
3.3、降級和熔斷的差別
服務降級和熔斷在概念上比較相近,通過兩個場景,談談我自己的了解。
- 熔斷,一般是停止服務
典型的就是股市的熔斷,如果大盤不受控制,直接休市,不提供服務,是保護大盤的一種方式。
- 降級,通常是有備用方案
從深圳到鄭州,下雨導緻航班延誤,我可以乘坐高鐵,如果高鐵票買不到,也可以乘坐汽車或者開車過去。
- 兩者的差別
降級一般是主動的,有預見性的,熔斷通常是被動的,服務A降級以後,一般會有服務B來代替,而熔斷通常是針對核心鍊路的處理。在實際開發中,熔斷的下一步通常就是降級。
4、常用限流算法設計
4.1、計數器法
計數器法是限流算法裡最簡單也是最容易實作的一種算法。
- 假設一個接口限制一分鐘内的通路次數不能超過100個,維護一個計數器,每次有新的請求過來,計數器加一,這時候判斷,如果計數器的值小于限流值,并且與上一次請求的時間間隔還在一分鐘内,允許請求通過,否則拒絕請求,如果超出了時間間隔,要将計數器清零。
- 計數器限流可以比較容易的應用在分布式環境中,用一個單點的存儲來儲存計數值,比如用Redis,并且設定自動過期時間,這時候就可以統計整個叢集的流量,并且進行限流。
- 計數器方式的缺點是不能處理臨界問題,或者說限流政策不夠平滑。假設在限流臨界點的前後,分别發送100個請求,實際上在計數器置0前後的極短時間裡,處理了200個請求,這是一個瞬時的高峰,可能會超過系統的限制。
- 計數器限流允許出現
的突發流量,可以使用滑動視窗算法去優化2*permitsPerSecond
4.2 漏桶算法
- 假設我們有一個固定容量的桶,桶底部可以漏水(忽略氣壓等,不是實體問題),并且這個漏水的速率可控的,那麼我們可以通過這個桶來控制請求速度,也就是漏水的速度。
- 我們不關心流進來的水,也就是外部請求有多少,桶滿了之後,多餘的水會溢出。
漏桶算法的示意圖如下:
- 将算法中的水換成實際應用中的請求,可以看到漏桶算法從入口限制了請求的速度。
- 使用漏桶算法,我們可以保證接口會以一個常速速率來處理請求,是以漏桶算法不會出現臨界問題。漏桶算法無法解決系統突發流量的情況
- 可以使用
的Guava
類,可以更好的控制漏桶算法,SmoothWarmingUp
4.3 令牌桶算法
- 假設一個大小恒定的桶,桶裡存放着令牌(token)。桶一開始是空的,現在以一個固定的速率往桶裡填充,直到達到桶的容量,多餘的令牌将會被丢棄。
- 如果令牌不被消耗,或者被消耗的速度小于産生的速度,令牌就會不斷地增多,直到把桶填滿。後面再産生的令牌就會從桶中溢出。最後桶中可以儲存的最大令牌數永遠不會超過桶的大小,
- 每當一個請求過來時,就會嘗試從桶裡移除一個令牌,如果沒有令牌的話,請求無法通過
- 在令牌桶算法中,隻要令牌桶中存在令牌,那麼就允許突發地傳輸資料直到達到使用者配置的門限,是以它适合于具有突發特性的流量。
4.4、令牌桶和漏桶差別
- 漏桶是控制水流入的速度,令牌桶則是控制流出,通過控制token,調節流量。
- 令牌桶算法相對漏桶算法的優勢在于可以處理系統的突發流量
- 這兩種算法的主要差別在于漏桶算法能夠強行限制資料的傳輸速率,而令牌桶算法在能夠限制資料的平均傳輸速率外,還允許某種程度的突發傳輸。
5、使用RateLimiter實作限流
-
開源工具包Google
提供了限流工具類Guava
,該類基于令牌桶算法實作流量限制,使用友善。RateLimiter
-
使用的是令牌桶的流控算法,RateLimiter
會按照一定的頻率往桶裡扔令牌,線程拿到令牌才能執行,比如你希望自己的應用程式QPS不要超過1000,那麼RateLimiter
設定1000的速率後,就會每秒往桶裡扔1000個令牌,方法說明如下:RateLimiter
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RateLimiterExample {
public static void main(String[] args) throws InterruptedException {
// qps設定為5,代表一秒鐘隻允許處理五個并發請求
RateLimiter rateLimiter = RateLimiter.create(5);
ExecutorService executorService = Executors.newFixedThreadPool(5);
int nTasks = 10;
CountDownLatch countDownLatch = new CountDownLatch(nTasks);
long start = System.currentTimeMillis();
for (int i = 0; i < nTasks; i++) {
final int j = i;
executorService.submit(() -> {
rateLimiter.acquire(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " gets job " + j + " done");
countDownLatch.countDown();
});
}
executorService.shutdown();
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("10 jobs gets done by 5 threads concurrently in " + (end - start) + " milliseconds");
}
}
5.1、算法原理
RateLimiter
基于令牌桶算法,它的核心思想主要有:
- 響應本次請求之後,動态計算下一次可以服務的時間,如果下一次請求在這個時間之前則需要進行等待。
類中的SmoothRateLimiter
屬性表示下一次可以響應的時間。例如,如果我們設定QPS為1,本次請求處理完之後,那麼下一次最早的能夠響應請求的時間一秒鐘之後。nextFreeTicketMicros
-
的子類RateLimiter
支援處理突發流量請求,例如,我們設定QPS為1,在十秒鐘之内沒有請求,那麼令牌桶中會有10個(假設設定的最大令牌數大于10)空閑令牌,如果下一次請求是SmoothBursty
,則不需要等待20秒鐘,因為令牌桶中已經有10個空閑的令牌。acquire(20)
類中的SmoothRateLimiter
就是用來表示目前令牌桶中的空閑令牌數。storedPermits
-
方法沒有等待逾時的概念,會一直阻塞直到滿足本次請求。acquire
-
子類RateLimiter
不同于SmoothWarmingUp
,它存在一個“熱身”的概念。它将SmoothBursty
分成兩個區間值:[0, thresholdPermits) 和 [thresholdPermits, maxPermits]。當請求進來時,如果目前系統處于"cold"的狀态,從 [thresholdPermits, maxPermits] 區間去拿令牌,所需要等待的時間會長于從區間 [0, thresholdPermits) 拿相同令牌所需要等待的時間。當請求增多,storedPermits
減少到storedPermits
以下時,此時拿令牌所需要等待的時間趨于穩定。這也就是所謂“熱身”的過程。這個過程後面會詳細分析。thresholdPermits
RateLimiter
主要的類的類圖如下所示:
RateLimiter
是一個抽象類,
SmoothRateLimiter
繼承自
RateLimiter
,不過
SmoothRateLimiter
仍然是一個抽象類,
SmoothBursty
和
SmoothWarmingUp
才是具體的實作類。
5.2、SmoothRateLimiter主要屬性
storedPermits
表明目前令牌桶中有多少令牌。
maxPermits
表示令牌桶最大令牌數目,
storedPermits
的取值範圍為:[0, maxPermits]。
stableIntervalMicros
等于
1/qps
,它代表系統在穩定期間,兩次請求之間間隔的微秒數。例如:如果我們設定的 qps 為5,則
stableIntervalMicros
為200ms。
nextFreeTicketMicros
表示系統處理完目前請求後,下一次請求被許可的最短微秒數,如果在這之前有請求進來,則必須等待。
當我們設定了 qps 之後,需要計算某一段時間系統能夠生成的令牌數目,那麼怎麼計算呢?一種方式是開啟一個背景任務去做,但是這樣代價未免有點大。
RateLimiter
中采取的是惰性計算方式:在每次請求進來的時候先去計算上次請求和本次請求之間應該生成多少個令牌。
末
【面試系列】會持續更新,歡迎關注公衆号“任冬學程式設計”,一起學習與進步!