高併發架構下的系統限流保護策略

跟著Mic學架構發表於2022-04-11

當系統併發流量過大的時候,有可能會造成系統被壓垮導致整個服務不可用的問題。

針對這個場景,一般的解決方案是:如果超過這個流量,我們就拒絕提供服務,從而使得我們的服務不會掛掉。

當然,限流雖然能夠保護系統不被壓垮,但是對於被限流的使用者,就會很不開心。所以限流其實是一種有損的解決方案。但是相比於全部不可用,有損服務是最好的一種解決辦法

tmall

限流的作用

除了前面說的限流使用場景之外,限流的設計還能防止惡意請求流量、惡意攻擊

所以,限流的基本原理是通過對併發訪問/請求進行限速或者一個時間視窗內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務(定向到錯誤頁或者告知資源沒有了)、排隊或等待(秒殺、下單)、降級(返回兜底資料或預設資料或預設資料,如商品詳情頁庫存預設有貨)

一般網際網路企業常見的限流有:限制總併發數(如資料庫連線池、執行緒池)、限制瞬時併發數(nginx的limit_conn模組,用來限制瞬時併發連線數)、限制時間視窗內的平均速率(如Guava的RateLimiter、nginx的limit_req模組,限制每秒的平均速率);其他的還有限制遠端介面呼叫速率、限制MQ的消費速率。另外還可以根據網路連線數、網路流量、CPU或記憶體負載等來限流。

有了快取以後再加上限流,在處理高併發的時候就能夠從容應對,不用擔心瞬間流量導致系統掛掉或雪崩,最終做到有損服務而不是不服務;但是限流需要評估好,不能亂用,否則一些正常流量出現一些奇怪的問題而導致使用者體驗很差造成使用者流失。

常見的限流演算法

滑動視窗

傳送和接受方都會維護一個資料幀的序列,這個序列被稱作視窗。傳送方的視窗大小由接受方確定,目的在於控制傳送速度,以免接受方的快取不夠大,而導致溢位,同時控制流量也可以避免網路擁塞。下面圖中的4,5,6號資料幀已經被髮送出去,但是未收到關聯的ACK,7,8,9幀則是等待傳送。可以看出傳送端的視窗大小為6,這是由接受端告知的。此時如果傳送端收到4號ACK,則視窗的左邊緣向右收縮,視窗的右邊緣則向右擴充套件,此時視窗就向前“滑動了”,即資料幀10也可以被髮送。

1564909772103

滑動視窗演示地址

漏桶(控制傳輸速率Leaky bucket)

漏桶演算法思路是,不斷的往桶裡面注水,無論注水的速度是大還是小,水都是按固定的速率往外漏水;如果桶滿了,水會溢位;

桶本身具有一個恆定的速率往下漏水,而上方時快時慢的會有水進入桶內。當桶還未滿時,上方的水可以加入。一旦水滿,上方的水就無法加入。桶滿正是演算法中的一個關鍵的觸發條件(即流量異常判斷成立的條件)。而此條件下如何處理上方流下來的水,有兩種方式

在桶滿水之後,常見的兩種處理方式為:

  1. 暫時攔截住上方水的向下流動,等待桶中的一部分水漏走後,再放行上方水。
  2. 溢位的上方水直接拋棄。

特點

  1. 漏水的速率是固定的
  2. 即使存在注水burst(突然注水量變大)的情況,漏水的速率也是固定的

    image-20200917200938881

令牌桶(能夠解決突發流量)

令牌桶演算法是網路流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種演算法。典型情況下,令牌桶演算法用來控制傳送到網路上的資料的數目,並允許突發資料的傳送。

令牌桶是一個存放固定容量令牌(token)的桶,按照固定速率往桶裡新增令牌; 令牌桶演算法實際上由三部分組成:兩個流和一個桶,分別是令牌流、資料流和令牌桶

令牌流與令牌桶

系統會以一定的速度生成令牌,並將其放置到令牌桶中,可以將令牌桶想象成一個緩衝區(可以用佇列這種資料結構來實現),當緩衝區填滿的時候,新生成的令牌會被扔掉。這裡有兩個變數很重要:

第一個是生成令牌的速度,一般稱為 rate 。比如,我們設定 rate = 2 ,即每秒鐘生成 2 個令牌,也就是每 1/2 秒生成一個令牌;

第二個是令牌桶的大小,一般稱為 burst 。比如,我們設定 burst = 10 ,即令牌桶最大隻能容納 10 個令牌。

資料流

資料流是真正的進入系統的流量,對於http介面來說,如果平均每秒鐘會呼叫2次,則認為速率為 2次/s。

有以下三種情形可能發生:

資料流的速率 等於 令牌流的速率。這種情況下,每個到來的資料包或者請求都能對應一個令牌,然後無延遲地通過佇列;

資料流的速率 小於 令牌流的速率。通過佇列的資料包或者請求只消耗了一部分令牌,剩下的令牌會在令牌桶裡積累下來,直到桶被裝滿。剩下的令牌可以在突發請求的時候消耗掉。

資料流的速率 大於 令牌流的速率。這意味著桶裡的令牌很快就會被耗盡。導致服務中斷一段時間,如果資料包或者請求持續到來,將發生丟包或者拒絕響應。

比如前面舉的例子,生成令牌的速率和令牌桶的大小分別為 rate = 2, burst = 10 ,則系統能承受的突發請求速率為 10次/s ,平均請求速率為 2次/s 。 三種情形中的最後一種情景是這個演算法的核心所在,這個演算法非常精確,實現非常簡單並且對伺服器的壓力可以忽略不計,因此應用得相當廣泛,值得學習和利用

image-20200917201022871

特點

  1. 令牌可以積累:桶中最大的令牌數是b,表示可以積累的最大令牌數
  2. 允許突發流量:桶中token可以積累到n(b<=n<=0),此時如果有n個突發請求同時到達,這n個請求是可以同時允許處理的

限流演算法實戰

Semaphore

Semaphore比較常見的就是用來做限流操作了。比如下面的場景中,模擬20個客戶端請求過來,我們為了減少訪問的壓力,通過Semaphore來限制請求的流量。

public class SemaphoreTest {

    public static void main(String[] args) {  
        // 執行緒池 
        ExecutorService exec = Executors.newCachedThreadPool();  
        // 只能5個執行緒同時訪問 
        final Semaphore semp = new Semaphore(5);  
        // 模擬20個客戶端訪問 
        for (int index = 0; index < 20; index++) {
            final int NO = index;  
            Runnable run = new Runnable() {  
                public void run() {  
                    try {  
                        // 獲取許可 
                        semp.acquire();  
                        System.out.println("Accessing: " \+ NO);  
                        Thread.sleep((long) (Math.random() * 10000));  
                        // 訪問完後,釋放 
                        semp.release();  
                    } catch (InterruptedException e) {  
                    }  
                }  
            };  
            exec.execute(run);  
        }  
        // 退出執行緒池 
        exec.shutdown();  
    }  
}

Guava的RateLimiter實現

在Guava中RateLimiter的實現有兩種: Bursty和WarmUp

bursty是基於token bucket的演算法實現,比如

RateLimiter rateLimiter=RateLimiter.create(permitPerSecond); //建立一個bursty例項

rateLimiter.acquire(); //獲取1個permit,當令牌數量不夠時會阻塞直到獲取為止

  1. 引入jar包

    <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>23.0</version>
    </dependency>
  2. 編寫測試程式碼

    public class PayService {
    
        RateLimiter rateLimiter=RateLimiter.create(10);//qps=10
    
        public void doRequest(String threadName){
            if(rateLimiter.tryAcquire()){
                System.out.println(threadName+": 支付成功");
            }else{
                System.out.println(threadName+": 當前支付人數過多,請稍候再試");
            }
        }
    
        public static void main(String[] args) throws IOException {
            PayService payService=new PayService();
            CountDownLatch latch=new CountDownLatch(1);
            Random random=new Random(10);
            for (int i = 0; i < 20; i++) {
                int finalI = i;
                new Thread(()->{
                    try {
                        latch.await();
                        int sleepTime = random.nextInt(1000);
                        Thread.sleep(sleepTime);
                        payService.doRequest("t-"+ finalI);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }).start();
            }
            latch.countDown();
            System.in.read();
        }
    }
下一篇文章來分析Alibaba開源的限流框架Sentinel!
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注同名微信公眾號獲取更多技術乾貨!

相關文章