咱們常說的N個9,就是對SLA的一個描述。html
SLA全稱是ServiceLevel Agreement,翻譯爲服務水平協議,也稱服務等級協議,它代表了公有云提供服務的等級以及質量。java
例如阿里雲對外承諾的就是一個服務週期內集羣服務可用性不低於99.99%,若是低於這個標準,雲服務公司就須要賠償客戶的損失。git
對互聯網公司來講,SLA就是網站或者API服務可用性的一個保證。github
9越多表明整年服務可用時間越長服務更可靠,4個9的服務可用性,聽起來已經很高了,但對於實際的業務場景,這個值可能並不夠。算法
咱們來作一個簡單的計算,假設一個核心鏈路依賴20個服務,強依賴同時沒有配置任何降級,而且這20個服務的可用性達到4個9,也就是99.99%,數據庫
那這個核心鏈路的可用性只有99.99的20次方 = 99.8%,api
若是有10億次請求則有3,000,000次的失敗請求緩存
理想情況下,每一年仍是有17小時服務不可用服務器
這是一個理想的估算,在實際的生產環境中,因爲服務發佈,宕機等各類各樣的緣由,狀況確定會比這個更差,網絡
對於一些業務比較敏感的業務,好比金融,或是對服務穩定要求較高的行業,好比訂單或者支付業務,這樣的狀況是不能接受的。
除了對服務可用性的追求,微服務架構一個繞不過去的問題就是服務雪崩。
在一個調用鏈路上,微服務架構各個服務之間組成了一個鬆散的總體,牽一髮而動全身,
服務雪崩是一個多級傳導的過程,首先是某個服務提供者不可用,因爲大量超時等待,繼而致使服務調用者不可用,而且在整個鏈路上傳導,繼而致使系統癱瘓。
如同上面咱們分析的,在大規模微服務架構的場景下,避免服務出現雪崩,要減小停機時間,要儘量的提升服務可用性。
提升服務可用性,能夠從不少方向入手,好比緩存、池化、異步化、負載均衡、隊列和降級熔斷等手段。
緩存以及隊列等手段,增長系統的容量
限流和降級則是關心在到達系統瓶頸時系統的響應,更看重穩定性
緩存和異步等提升系統的戰力,限流降級關注的是防護。
限流和降級,具體實施方法能夠概括爲八字箴言,分別是限流,降級,熔斷和隔離。
限流顧名思義,提早對各個類型的請求設置最高的QPS閾值,若高於設置的閾值則對該請求直接返回,再也不調用後續資源。
限流須要結合壓測等,瞭解系統的最高水位,也是在實際開發中應用最多的一種穩定性保障手段。
降級則是當服務器壓力劇增的狀況下,根據當前業務狀況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行。
從降級配置方式上,降級通常能夠分爲主動降級和自動降級。
主動降級是提早配置,自動降級則是系統發生故障時,如超時或者頻繁失敗,自動降級。
其中,自動降級,又能夠分爲如下策略:
超時降級
失敗次數降級
故障降級
在系統設計中,降級通常是結合系統配置中心,經過配置中心進行推送,下面是一個典型的降級通知設計
若是某個目標服務調用慢或者有大量超時,此時熔斷該服務的調用,對於後續調用請求,不在繼續調用目標服務,直接返回,快速釋放資源。
熔斷通常須要設置不一樣的恢復策略,若是目標服務狀況好轉則恢復調用。
服務隔離與前面的三個略有區別,咱們的系統一般提供了不止一個服務,可是這些服務在運行時是部署在一個實例,或者一臺物理機上面的,
若是不對服務資源作隔離,一旦一個服務出現了問題,整個系統的穩定性都會受到影響!
服務隔離的目的就是避免服務之間相互影響。
通常來講,隔離要關注兩方面,一個是在哪裏進行隔離,另一個是隔離哪些資源。
何處隔離
一次服務調用,涉及到的是服務提供方和調用方,咱們所指的資源,也是兩方的服務器等資源,服務隔離一般能夠從提供方和調用方兩個方面入手。
隔離什麼
廣義的服務隔離,不只包括服務器資源,還包括數據庫分庫,緩存,索引等,這裏咱們只關注服務層面的隔離。
服務降級和熔斷在概念上比較相近,經過兩個場景,談談我本身的理解。
熔斷,通常是中止服務
典型的就是股市的熔斷,若是大盤不受控制,直接休市,不提供服務,是保護大盤的一種方式。
降級,一般是有備用方案
從北京到濟南,下雨致使航班延誤,我能夠乘坐高鐵,若是高鐵票買不到,也能夠乘坐汽車或者開車過去。
二者的區別
降級通常是主動的,有預見性的,熔斷一般是被動的,
服務A降級之後,通常會有服務B來代替,而熔斷一般是針對核心鏈路的處理。
在實際開發中,熔斷的下一步一般就是降級。
剛纔講了限流的概念,那麼怎樣判斷系統到達設置的流量閾值了?
這就須要一些限流策略來支持,不一樣的限流算法有不一樣的特色,平滑程度也不一樣。
計數器法是限流算法裏最簡單也是最容易實現的一種算法。
假設一個接口限制一分鐘內的訪問次數不能超過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 的突發流量,可使用滑動窗口算法去優化,具體不展開。
假設咱們有一個固定容量的桶,桶底部能夠漏水(忽略氣壓等,不是物理問題),而且這個漏水的速率可控的,那麼咱們能夠經過這個桶來控制請求速度,也就是漏水的速度。
咱們不關心流進來的水,也就是外部請求有多少,桶滿了以後,多餘的水會溢出。
漏桶算法的示意圖以下:
將算法中的水換成實際應用中的請求,能夠看到漏桶算法從入口限制了請求的速度。使用漏桶算法,咱們能夠保證接口會以一個常速速率來處理請求,因此漏桶算法不會出現臨界問題。
這裏簡單實現一下,也可使用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; } } } }
漏桶是控制水流入的速度,令牌桶則是控制留出,經過控制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; } } } }
這兩種算法的主要區別在於漏桶算法可以強行限制數據的傳輸速率,而令牌桶算法在可以限制數據的平均傳輸速率外,還容許某種程度的突發傳輸。
在令牌桶算法中,只要令牌桶中存在令牌,那麼就容許突發地傳輸數據直到達到用戶配置的門限,所以它適合於具備突發特性的流量。
漏桶和令牌桶算法實現能夠同樣,可是方向是相反的,對於相同的參數獲得的限流效果是同樣的。
主要區別在於令牌桶容許必定程度的突發,漏桶主要目的是平滑流入速率,考慮一個臨界場景,令牌桶內積累了100個token,能夠在一瞬間經過,可是由於下一秒產生token的速度是固定的,
因此令牌桶容許出現瞬間出現permitsPerSecond的流量,可是不會出現2*permitsPerSecond的流量,漏桶的速度則始終是平滑的。
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法實現流量限制,使用方便。
RateLimiter使用的是令牌桶的流控算法,RateLimiter會按照必定的頻率往桶裏扔令牌,線程拿到令牌才能執行,好比你但願本身的應用程序QPS不要超過1000,那麼RateLimiter設置1000的速率後,就會每秒往桶裏扔1000個令牌,看下方法的說明:
修飾符和類型 |
方法和描述 |
---|---|
修飾符和類型 |
方法和描述 |
double |
acquire() |
double |
acquire(int permits) |
static RateLimiter |
create(double permitsPerSecond) |
static RateLimiter |
create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) |
double |
getRate() |
void |
setRate(double permitsPerSecond) |
boolean |
tryAcquire() |
boolean |
tryAcquire(int permits) |
boolean |
tryAcquire(int permits, long timeout, TimeUnit unit) |
boolean |
tryAcquire(long timeout, TimeUnit unit) |
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+"次請求拒絕"); } } } }
本文從服務可用性開始,分析了在業務高峯期經過限流降級保障服務高可用的重要性。
接下來分別探討了限流,降級,熔斷,隔離的概念和應用,而且介紹了經常使用的限流策略,圖片引用網絡和維基百科。