1. 程式人生 > >Guava——平滑限流

Guava——平滑限流

1.常用限流方法

對於一個應用系統來說一定會有極限併發/請求數,即總有一個TPS/QPS閥值,如果超了閥值則系統就會不響應使用者請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求湧入擊垮系統。

常見的限流方法有:

  • 容器限流

    常見的web容器其實也具備限流的功能。

    以Tomcat容器為例,其Connector 其中一種配置有如下幾個引數:

    acceptCount:如果Tomcat的執行緒都忙於響應,新來的連線會進入佇列排隊,如果超出排隊大小,則拒絕連線;

    maxConnections: 瞬時最大連線數,超出的會排隊等待;

    maxThreads:Tomcat能啟動用來處理請求的最大執行緒數,如果請求處理量一直遠遠大於最大執行緒數則可能會僵死。

  • 限流總資源數

    如果有的資源是稀缺資源(如資料庫連線、執行緒),而且可能有多個系統都會去使用它,那麼需要限制應用;可以使用池化技術來限制總資源數:連線池、執行緒池。比如分配給每個應用的資料庫連線是100,那麼本應用最多可以使用100個資源,超出了可以等待或者拋異常。

  • 限流某個介面的總併發/請求數

    如果介面可能會有突發訪問情況,但又擔心訪問量太大造成崩潰,如搶購業務;這個時候就需要限制這個介面的總併發請求數了;因為粒度比較細,可以為每個介面都設定相應的閥值。可以使用Java中的AtomicLong進行限流:

    try {
    if(atomic.incrementAndGet() > 限流數) {
    //拒絕請求
     

    }
    //處理請求
    } finally {
    atomic.decrementAndGet();
    }

  • 利用Netflix的hystrix限流

2.漏桶演算法 VS 令牌桶演算法

2.1 漏桶演算法(Leaky Bucket)

漏桶演算法(Leaky Bucket):水(請求)先進入到漏桶裡,漏桶以一定的速度出水(介面有響應速率),當水流入速度過大會直接溢位(訪問頻率超過介面響應速率),然後就拒絕請求,可以看出漏桶演算法能強行限制資料的傳輸速率.示意圖如下:


可見這裡有兩個變數,一個是桶的大小,支援流量突發增多時可以存多少的水(burst),另一個是水桶漏洞的大小(rate)。

2.2 令牌桶演算法(Token Bucket)

令牌桶演算法(Token Bucket)和 Leaky Bucket 效果一樣但方向相反的演算法,更加容易理解.隨著時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裡加入Token(想象和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了.新請求來臨時,會各自拿走一個Token,如果沒有Token可拿了就阻塞或者拒絕服務.


令牌桶的另外一個好處是可以方便的改變速度. 一旦需要提高速率,則按需提高放入桶中的令牌的速率. 一般會定時(比如100毫秒)往桶中增加一定數量的令牌, 有些變種演算法則實時的計算應該增加的令牌的數量.

2.3 漏桶演算法VS令牌桶演算法

  • 令牌桶是按照固定速率往桶中新增令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減為零時則拒絕新的請求;

  • 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;

  • 令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支援一次拿3個令牌,4個令牌),並允許一定程度突發流量;

  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;

  • 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;

  • 兩個演算法實現可以一樣,但是方向是相反的,對於相同的引數得到的限流效果是一樣的。

3.Guava實現平滑限流

3.1Guava是什麼

Guava是一個開源的Java庫,其中包含谷歌很多專案使用的核心庫。這個庫是為了方便編碼,並減少編碼錯誤。這個庫提供用於集合,快取,支援原語,併發性,常見註解,字串處理,I/O和驗證的實用方法。

3.2Guava RateLimiter實現平滑限流

Guava RateLimiter提供了令牌桶演算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

平滑突發限流:

/**
 * 平滑突發限流(SmoothBursty)
 */
public class SmoothBurstyRateLimitTest {
    public static void main(String[] args) {
        //QPS = 5,每秒允許5個請求
        RateLimiter limiter = RateLimiter.create(5);
        //limiter.acquire() 返回獲取token的耗時,以秒為單位
        System.out.println(limiter.acquire());
        System.out.println(limiter.acquire());
        System.out.println(limiter.acquire());
        System.out.println(limiter.acquire());
        System.out.println(limiter.acquire());
        System.out.println(limiter.acquire());
    }
}

0.0
0.19667
0.195784
0.194672
0.195658
0.195285
/**
 * 平滑突發限流(smoothbursty)
 */
public class SmoothBurstyRateLimitTest02 {

    public static void main(String[] args) {
        //每秒允許5個請求,表示桶容量為5且每秒新增5個令牌,即每隔0.2毫秒新增一個令牌
        RateLimiter limiter = RateLimiter.create(5);
        //一次性消費5個令牌
        System.out.println(limiter.acquire(5));
        //limiter.acquire(1)將等待差不多1秒桶中才能有令牌
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
    }
}
/**
 * 平滑突發限流(smoothbursty)
 */
public class SmoothBurstyRateLimitTest03 {

    public static void main(String[] args) {
        //每秒允許5個請求,表示桶容量為5且每秒新增5個令牌,即每隔0.2毫秒新增一個令牌
        RateLimiter limiter = RateLimiter.create(5);
        //第一秒突發了10個請求
        System.out.println(limiter.acquire(10));
        //limiter.acquire(1)將等待差不多2秒桶中才能有令牌
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
        //固定速率
        System.out.println(limiter.acquire(1));
    }
}

平滑預熱限流:

/**
 * 平滑預熱限流(SmoothWarmingUp)
 */
public class SmoothWarmingUp {
    public static void main(String[] args) {
        //permitsPerSecond:每秒新增的令牌數  warmupPeriod:從冷啟動速率過渡到平均速率的時間間隔
        //系統冷啟動後慢慢的趨於平均固定速率(即剛開始速率慢一些,然後慢慢趨於我們設定的固定速率)
        RateLimiter limiter = RateLimiter.create(10, 1000, TimeUnit.MILLISECONDS);
        for(int i = 0; i < 10;i++) {
            //獲取一個令牌
            System.out.println(limiter.acquire(1));
        }
    }
}

秒殺場景:

/**
 * 秒殺場景模擬
 */
public class MiaoShaTest {

    public static void main(String[] args) throws InterruptedException {

        //限流,每秒允許10個請求進入秒殺
        RateLimiter limiter = RateLimiter.create(10);

        for (int i = 0; i < 100; i++) {
            //100個執行緒同時搶購
            new Thread(() -> {
                //每個秒殺請求如果100ms以內得到令牌,就算是秒殺成功,否則就返回秒殺失敗
                if (limiter.tryAcquire(50, TimeUnit.MILLISECONDS)) {
                    System.out.println("恭喜您,秒殺成功");
                } else {
                    System.out.println("秒殺失敗,請繼續努力");
                }
            }).start();
            //等待新的令牌生成
            Thread.sleep(10);
        }
    }
}

測試demo地址:https://github.com/online-demo/yunxi-guava