一、服務等級協定
我們常說的N個9,就是對SLA的一個描述。
SLA全稱是ServiceLevel Agreement,翻譯為服務水準協定,也稱服務等級協定,它表明了公有雲提供服務的等級以及品質。
例如阿裡雲對外承諾的就是一個服務周期内叢集服務可用性不低于99.99%,如果低于這個标準,雲服務公司就需要賠償客戶的損失。
1.1 做到4個9夠好了嗎
對網際網路公司來說,SLA就是網站或者API服務可用性的一個保證。
9越多代表全年服務可用時間越長服務更可靠,4個9的服務可用性,聽起來已經很高了,但對于實際的業務場景,這個值可能并不夠。
我們來做一個簡單的計算,假設一個核心鍊路依賴20個服務,強依賴同時沒有配置任何降級,并且這20個服務的可用性達到4個9,也就是99.99%,
那這個核心鍊路的可用性隻有99.99的20次方 = 99.8%,
- 如果有10億次請求則有3,000,000次的失敗請求
- 理想狀況下,每年還是有17小時服務不可用
高可用架構之限流降級實作
這是一個理想的估算,在實際的生産環境中,由于服務釋出,當機等各種各樣的原因,情況肯定會比這個更差,
對于一些業務比較敏感的業務,比如金融,或是對服務穩定要求較高的行業,比如訂單或者支付業務,這樣的情況是不能接受的。
1.2 微服務的雪崩效應
除了對服務可用性的追求,微服務架構一個繞不過去的問題就是服務雪崩。
在一個調用鍊路上,微服務架構各個服務之間組成了一個松散的整體,牽一發而動全身,
服務雪崩是一個多級傳導的過程,首先是某個服務提供者不可用,由于大量逾時等待,繼而導緻服務調用者不可用,并且在整個鍊路上傳導,繼而導緻系統癱瘓。
二、限流降級怎麼做
如同上面我們分析的,在大規模微服務架構的場景下,避免服務出現雪崩,要減少停機時間,要盡可能的提高服務可用性。
提高服務可用性,可以從很多方向入手,比如緩存、池化、異步化、負載均衡、隊列和降級熔斷等手段。
- 緩存以及隊列等手段,增加系統的容量
- 限流和降級則是關心在到達系統瓶頸時系統的響應,更看重穩定性
緩存和異步等提高系統的戰力,限流降級關注的是防禦。
限流和降級,具體實施方法可以歸納為八字箴言,分别是限流,降級,熔斷和隔離。
2.1 限流和降級
限流顧名思義,提前對各個類型的請求設定最高的QPS門檻值,若高于設定的門檻值則對該請求直接傳回,不再調用後續資源。
限流需要結合壓測等,了解系統的最高水位,也是在實際開發中應用最多的一種穩定性保障手段。
降級則是當伺服器壓力劇增的情況下,根據目前業務情況及流量對一些服務和頁面有政策的降級,以此釋放伺服器資源以保證核心任務的正常運作。
從降級配置方式上,降級一般可以分為主動降級和自動降級。
主動降級是提前配置,自動降級則是系統發生故障時,如逾時或者頻繁失敗,自動降級。
其中,自動降級,又可以分為以下政策:
- 逾時降級
- 失敗次數降級
- 故障降級
在系統設計中,降級一般是結合系統配置中心,通過配置中心進行推送,下面是一個典型的降級通知設計
2.2 熔斷隔離
如果某個目标服務調用慢或者有大量逾時,此時熔斷該服務的調用,對于後續調用請求,不在繼續調用目标服務,直接傳回,快速釋放資源。
熔斷一般需要設定不同的恢複政策,如果目标服務情況好轉則恢複調用。
服務隔離與前面的三個略有差別,我們的系統通常提供了不止一個服務,但是這些服務在運作時是部署在一個執行個體,或者一台實體機上面的,
如果不對服務資源做隔離,一旦一個服務出現了問題,整個系統的穩定性都會受到影響!
服務隔離的目的就是避免服務之間互相影響。
一般來說,隔離要關注兩方面,一個是在哪裡進行隔離,另外一個是隔離哪些資源。
- 何處隔離
一次服務調用,涉及到的是服務提供方和調用方,我們所指的資源,也是兩方的伺服器等資源,服務隔離通常可以從提供方和調用方兩個方面入手。
- 隔離什麼
廣義的服務隔離,不僅包括伺服器資源,還包括資料庫分庫,緩存,索引等,這裡我們隻關注服務層面的隔離。
2.3 降級和熔斷的差別
服務降級和熔斷在概念上比較相近,通過兩個場景,談談我自己的了解。
- 熔斷,一般是停止服務
典型的就是股市的熔斷,如果大盤不受控制,直接休市,不提供服務,是保護大盤的一種方式。
- 降級,通常是有備用方案
從北京到濟南,下雨導緻航班延誤,我可以乘坐高鐵,如果高鐵票買不到,也可以乘坐汽車或者開車過去。
- 兩者的差別
降級一般是主動的,有預見性的,熔斷通常是被動的,
服務A降級以後,一般會有服務B來代替,而熔斷通常是針對核心鍊路的處理。
在實際開發中,熔斷的下一步通常就是降級。
三、常用限流算法設計
剛才講了限流的概念,那麼怎樣判斷系統到達設定的流量門檻值了?
這就需要一些限流政策來支援,不同的限流算法有不同的特點,平滑程度也不同。
3.1 計數器法
計數器法是限流算法裡最簡單也是最容易實作的一種算法。
假設一個接口限制一分鐘内的通路次數不能超過100個,維護一個計數器,每次有新的請求過來,計數器加一,這時候判斷,如果計數器的值小于限流值,并且與上一次請求的時間間隔還在一分鐘内,
允許請求通過,否則拒絕請求,如果超出了時間間隔,要将計數器清零。
public class CounterLimiter {
//初始時間
private static long startTime = System.currentTimeMillis();
//初始計數值
private static final AtomicInteger ZERO = new AtomicInteger(0);
//時間視窗限制
private static final long interval = 10000;
//限制通過請求
private static int limit = 100;
//請求計數
private AtomicInteger requestCount = ZERO;
//擷取限流
public boolean tryAcquire() {
long now = System.currentTimeMillis();
//在時間視窗内
if (now < startTime + interval) {
//判斷是否超過最大請求
if (requestCount.get() < limit) {
requestCount.incrementAndGet();
return true;
}
return false;
} else {
//逾時重置
startTime = now;
requestCount = ZERO;
return true;
}
}
}
計數器限流可以比較容易的應用在分布式環境中,用一個單點的存儲來儲存計數值,比如用Redis,并且設定自動過期時間,這時候就可以統計整個叢集的流量,并且進行限流。
計數器方式的缺點是不能處理臨界問題,或者說限流政策不夠平滑。
假設在限流臨界點的前後,分别發送100個請求,實際上在計數器置0前後的極短時間裡,處理了200個請求,這是一個瞬時的高峰,可能會超過系統的限制。
計數器限流允許出現 2*permitsPerSecond 的突發流量,可以使用滑動視窗算法去優化,具體不展開。
3.2 漏桶算法
假設我們有一個固定容量的桶,桶底部可以漏水(忽略氣壓等,不是實體問題),并且這個漏水的速率可控的,那麼我們可以通過這個桶來控制請求速度,也就是漏水的速度。
我們不關心流進來的水,也就是外部請求有多少,桶滿了之後,多餘的水會溢出。
漏桶算法的示意圖如下:
将算法中的水換成實際應用中的請求,可以看到漏桶算法從入口限制了請求的速度。使用漏桶算法,我們可以保證接口會以一個常速速率來處理請求,是以漏桶算法不會出現臨界問題。
這裡簡單實作一下,也可以使用Guava的SmoothWarmingUp類,可以更好的控制漏桶算法,
public class LeakyLimiter {
//桶的容量
private int capacity;
//漏水速度
private int ratePerMillSecond;
//水量
private double water;
//上次漏水時間
private long lastLeakTime;
public LeakyLimiter(int capacity, int ratePerMillSecond) {
this.capacity = capacity;
this.ratePerMillSecond = ratePerMillSecond;
this.water = 0;
}
//擷取限流
public boolean tryAcquire() {
//執行漏水,更新剩餘水量
refresh();
//嘗試加水,水滿則拒絕
if (water + 1 > capacity) {
return false;
}
water = water + 1;
return true;
}
private void refresh() {
//目前時間
long currentTime = System.currentTimeMillis();
if (currentTime > lastLeakTime) {
//距上次漏水的時間間隔
long millisSinceLastLeak = currentTime - lastLeakTime;
long leaks = millisSinceLastLeak * ratePerMillSecond;
//允許漏水
if (leaks > 0) {
//已經漏光
if (water <= leaks) {
water = 0;
} else {
water = water - leaks;
}
this.lastLeakTime = currentTime;
}
}
}
}
3.3 令牌桶算法
漏桶是控制水流入的速度,令牌桶則是控制留出,通過控制token,調節流量。
假設一個大小恒定的桶,桶裡存放着令牌(token)。桶一開始是空的,現在以一個固定的速率往桶裡填充,直到達到桶的容量,多餘的令牌将會被丢棄。
如果令牌不被消耗,或者被消耗的速度小于産生的速度,令牌就會不斷地增多,直到把桶填滿。後面再産生的令牌就會從桶中溢出。最後桶中可以儲存的最大令牌數永遠不會超過桶的大小,
每當一個請求過來時,就會嘗試從桶裡移除一個令牌,如果沒有令牌的話,請求無法通過。
public class TokenBucketLimiter {
private long capacity;
private long windowTimeInSeconds;
long lastRefillTimeStamp;
long refillCountPerSecond;
long availableTokens;
public TokenBucketLimiter(long capacity, long windowTimeInSeconds) {
this.capacity = capacity;
this.windowTimeInSeconds = windowTimeInSeconds;
lastRefillTimeStamp = System.currentTimeMillis();
refillCountPerSecond = capacity / windowTimeInSeconds;
availableTokens = 0;
}
public long getAvailableTokens() {
return this.availableTokens;
}
public boolean tryAcquire() {
//更新令牌桶
refill();
if (availableTokens > 0) {
--availableTokens;
return true;
} else {
return false;
}
}
private void refill() {
long now = System.currentTimeMillis();
if (now > lastRefillTimeStamp) {
long elapsedTime = now - lastRefillTimeStamp;
int tokensToBeAdded = (int) ((elapsedTime / 1000) * refillCountPerSecond);
if (tokensToBeAdded > 0) {
availableTokens = Math.min(capacity, availableTokens + tokensToBeAdded);
lastRefillTimeStamp = now;
}
}
}
}
這兩種算法的主要差別在于漏桶算法能夠強行限制資料的傳輸速率,而令牌桶算法在能夠限制資料的平均傳輸速率外,還允許某種程度的突發傳輸。
在令牌桶算法中,隻要令牌桶中存在令牌,那麼就允許突發地傳輸資料直到達到使用者配置的門限,是以它适合于具有突發特性的流量。
3.4 漏桶和令牌桶的比較
漏桶和令牌桶算法實作可以一樣,但是方向是相反的,對于相同的參數得到的限流效果是一樣的。
主要差別在于令牌桶允許一定程度的突發,漏桶主要目的是平滑流入速率,考慮一個臨界場景,令牌桶内積累了100個token,可以在一瞬間通過,但是因為下一秒産生token的速度是固定的,
是以令牌桶允許出現瞬間出現permitsPerSecond的流量,但是不會出現2*permitsPerSecond的流量,漏桶的速度則始終是平滑的。
3.5 使用RateLimiter實作限流
Google開源工具包Guava提供了限流工具類RateLimiter,該類基于令牌桶算法實作流量限制,使用友善。
RateLimiter使用的是令牌桶的流控算法,RateLimiter會按照一定的頻率往桶裡扔令牌,線程拿到令牌才能執行,比如你希望自己的應用程式QPS不要超過1000,那麼RateLimiter設定1000的速率後,就會每秒往桶裡扔1000個令牌,看下方法的說明:
修飾符和類型 | 方法和描述 |
---|---|
double | acquire() 從RateLimiter擷取一個許可,該方法會被阻塞直到擷取到請求 |
acquire(int permits) 從RateLimiter擷取指定許可數,該方法會被阻塞直到擷取到請求 | |
static RateLimiter | create(double permitsPerSecond) 根據指定的穩定吞吐率建立RateLimiter,這裡的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少查詢) |
create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 根據指定的穩定吞吐率和預熱期來建立RateLimiter,這裡的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少個請求量),在這段預熱時間内,RateLimiter每秒配置設定的許可數會平穩地增長直到預熱期結束時達到其最大速率。(隻要存在足夠請求數來使其飽和) | |
getRate() 傳回RateLimiter 配置中的穩定速率,該速率機關是每秒多少許可數 | |
void | setRate(double permitsPerSecond) 更新RateLimite的穩定速率,參數permitsPerSecond 由構造RateLimiter的工廠方法提供。 |
boolean | tryAcquire() 從RateLimiter 擷取許可,如果該許可可以在無延遲下的情況下立即擷取得到的話 |
tryAcquire(int permits) 從RateLimiter 擷取許可數,如果該許可數可以在無延遲下的情況下立即擷取得到的話 | |
tryAcquire(int permits, long timeout, TimeUnit unit) 從RateLimiter 擷取指定許可數如果該許可數可以在不超過timeout的時間内擷取得到的話,或者如果無法在timeout 過期之前擷取得到許可數的話,那麼立即傳回false (無需等待) | |
tryAcquire(long timeout, TimeUnit unit) 從RateLimiter 擷取許可如果該許可可以在不超過timeout的時間内擷取得到的話,或者如果無法在timeout 過期之前擷取得到許可的話,那麼立即傳回false(無需等待) |
RateLimter提供的API可以直接應用,其中acquire會阻塞,類似JUC的信号量Semphore,tryAcquire方法則是非阻塞的:
public class RateLimiterTest {
public static void main(String[] args) throws InterruptedException {
//允許10個,permitsPerSecond
RateLimiter limiter = RateLimiter.create(10);
for(int i=1;i<20;i++){
if (limiter.tryAcquire(1)){
System.out.println("第"+i+"次請求成功");
}else{
System.out.println("第"+i+"次請求拒絕");
}
}
}
}
四、總結
本文從服務可用性開始,分析了在業務高峰期通過限流降級保障服務高可用的重要性。
接下來分别探讨了限流,降級,熔斷,隔離的概念和應用,并且介紹了常用的限流政策,圖檔引用網絡和維基百科。
參考資料
阿裡雲伺服器 ECS服務等級協定
接口限流算法總結
Guava Docs
How-it-Works
https://en.wikipedia.org/wiki/Token_bucket
作者:邴越
掃碼關注公衆号:架構進化論,獲得第一手的技術資訊和原創文章
如果文章對您有幫助,可以點選文章右下角【推薦】一下,您的鼓勵是作者堅持原創和持續寫作的最大動力!