96秒破百億,如何抗住雙11高併發流量

2019年雙十一,1 分 36 秒,交易額衝到 100 億 !這個速度再次刷新天貓雙 11 成交總額破 100 億的紀錄。

那麼如何抗住雙 11 高併發流量?接下來讓我們一起來聊聊高可用的“大殺器”限流降級技術。

一、服務等級協議

 我們常說的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個,維護一個計數器,每次有新的請求過來,計數器加一,這時候判斷,如果計數器的值小於限流值,並且與上一次請求的時間間隔還在一分鐘內,

允許請求通過,否則拒絕請求,如果超出了時間間隔,要將計數器清零。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

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類,可以更好的控制漏桶算法,

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

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)。桶一開始是空的,現在以一個固定的速率往桶裏填充,直到達到桶的容量,多餘的令牌將會被丟棄。

如果令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢出。最後桶中可以保存的最大令牌數永遠不會超過桶的大小,

每當一個請求過來時,就會嘗試從桶裏移除一個令牌,如果沒有令牌的話,請求無法通過。

 

 

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

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獲取一個許可,該方法會被阻塞直到獲取到請求

double

acquire(int permits)
從RateLimiter獲取指定許可數,該方法會被阻塞直到獲取到請求

static RateLimiter

create(double permitsPerSecond)
根據指定的穩定吞吐率創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少查詢)

static RateLimiter

create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
根據指定的穩定吞吐率和預熱期來創建RateLimiter,這裏的吞吐率是指每秒多少許可數(通常是指QPS,每秒多少個請求量),在這段預熱時間內,RateLimiter每秒分配的許可數會平穩地增長直到預熱期結束時達到其最大速率。(只要存在足夠請求數來使其飽和)

double

getRate()
返回RateLimiter 配置中的穩定速率,該速率單位是每秒多少許可數

void

setRate(double permitsPerSecond)
更新RateLimite的穩定速率,參數permitsPerSecond 由構造RateLimiter的工廠方法提供。

boolean

tryAcquire()
從RateLimiter 獲取許可,如果該許可可以在無延遲下的情況下立即獲取得到的話

boolean

tryAcquire(int permits)
從RateLimiter 獲取許可數,如果該許可數可以在無延遲下的情況下立即獲取得到的話

boolean

tryAcquire(int permits, long timeout, TimeUnit unit)
從RateLimiter 獲取指定許可數如果該許可數可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可數的話,那麼立即返回false (無需等待)

boolean

tryAcquire(long timeout, TimeUnit unit)
從RateLimiter 獲取許可如果該許可可以在不超過timeout的時間內獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可的話,那麼立即返回false(無需等待)

  

RateLimter提供的API可以直接應用,其中acquire會阻塞,類似JUC的信號量Semphore,tryAcquire方法則是非阻塞的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章