微言限流

在系統架構設計當中,限流是一個不得不說的話題,由於他太不起眼,可是也過重要了。這點有些像古代鎮守邊陲的將士,據守隘口,抵擋住外族的千軍萬馬,一旦隘口失守,各類饕餮涌入城內,勢必將咱們苦心經營的朝堂廟店洗劫一空,以前的全部努力都付之一炬。因此今天咱們點了這個話題,一方面是要對限流作下總結,另外一方面,拋磚引玉,看看你們各自的系統中,限流是怎麼作的。html

提到限流,映入腦海的確定是限制流量四個字,其重點在於如何限。並且這個限,還分爲單機限和分佈式限,單機限流,顧名思義,就是對部署了應用的docker機或者物理機,進行流量控制,以使得流量的涌入呈現可控的態勢,防止過大過快的流量涌入形成應用的性能問題,甚至於失去響應。分佈式限流,則是對集羣的流量限制,通常這類應用的流量限制集中在一個地方來進行,好比redis,zk或者其餘的可以支持分佈式限流的組件中。這樣當流量過大過快的時候,不至於由於集羣中的一臺機器被壓垮而帶來雪崩效應,形成集羣應用總體坍塌。redis

下面咱們來細數一下各類限流操做。算法

 

1. 基於計數器的單機限流docker

此類限流,通常是經過應用中的計數器來進行流量限制操做。計數器能夠用Integer類型的變量,也能夠用Java自帶的AtomicLong來實現。原理就是設置一個計數器的閾值,每當有流量進入的時候,將計數器遞增,當達到閾值的時候,後續的請求將會直接被拋棄。代碼實現以下:緩存

    //限流計數器
    private static AtomicLong counter = new AtomicLong();
    //限流閾值
    private static final long counterMax = 500;
    //業務處理方法
    public void invoke(Request request) {
        try {
            //請求過濾
            if (counter.incrementAndGet() > counterMax) {
                return;
            }
            //業務邏輯
            doBusiness(request);
        } catch (Exception e) {
            //錯誤處理
            doException(request,e);
        } finally {
            counter.decrementAndGet();
        }
    }

上面的代碼就是一個簡單的基於計數器實現的單機限流。代碼簡單易行,操做方便,並且能夠帶來不錯的效果。可是缺點也很明顯,那就是先來的流量通常都能打進來,後來的流量基本上都會被拒絕。因爲每一個請求被執行的機率其實不同,因此就沒有公平性可言。架構

因此總結一下此種限流優缺點:併發

優勢:代碼簡潔,操做方便less

缺點:先到先得,先到的請求可執行機率爲100%,後到的請求可執行機率小一些,每一個請求得到執行的機會是不平等的。dom

那麼,若是想讓每一個請求得到執行的機會是平等的話,該怎麼作呢?分佈式

 

2. 基於隨機數的單機限流

此種限流算法,使得請求可被執行的機率是一致的,因此相對於基於計數器實現的限流說來,對用戶更加的友好一些。代碼以下:

    //獲取隨機數
    private static ThreadLocalRandom ptgGenerator = ThreadLocalRandom.current();
    //限流百分比,容許多少流量經過此業務,這裏限定爲10%
    private static final long ptgGuarder = 10;
    //業務處理方法
    public void invoke(Request request) {
        try {
            //請求進入,獲取百分比
            int currentPercentage = ptgGenerator.nextInt(1, 100);
            if (currentPercentage <= ptgGuarder) {
                //業務處理
                doBusiness(request);
            } else {
                return;
            }
        } catch (Exception e) {
            //錯誤處理
            doException(request, e);
        }
    }

從上面代碼能夠看出來,針對每一個請求,都會先獲取一個隨機的1~100的執行率,而後和當前限流閾值(好比當前接口只容許10%的流量經過)相比,若是小於此限流閾值,則放行;若是大於此限流閾值,則直接返回,不作任何處理。和以前的計數器限流比起來,每一個請求得到執行的機率是一致的。固然,在真正的業務場景中,用戶能夠經過動態配置化閾值參數,來控制每分鐘經過的流量百分比,或者是每小時經過的流量百分比。可是若是對於突增的高流量,此種方法則有點問題,由於高併發下,每一個請求之間進入的時間很短暫,致使nextInt生成的值,大機率是重複的,因此這裏須要作的一個優化點,就是爲其尋找合適的seed,用於優化nextInt生成的值。

優勢:代碼簡潔,操做簡便,每一個請求可執行的機會是平等的。

缺點:不適合應用突增的流量。

 

3. 基於時間段的單機限流

有時候,咱們的應用只想在單位時間內放固定的流量進來,好比一秒鐘內只容許放進來100個請求,其餘的請求拋棄。那麼這裏的作法有不少,能夠基於計數器限流實現,而後判斷時間,可是此種作法稍顯複雜,可控性不是特別好。

那麼這裏咱們就要用到緩存組件來實現了。原理是這樣的,首先請求進來,在guava中設置一個key,此key就是當前的秒數,秒數的值就是放進來的請求累加數,若是此累加數到100了,則拒絕後續請求便可。代碼以下:

   //獲取guava實例
    private static LoadingCache<Long, AtomicLong> guava = CacheBuilder.newBuilder()
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build(new CacheLoader<Long, AtomicLong>() {
                        @Override
                        public AtomicLong load(Long seconds) throws Exception {
                            return null;
                        }
                    });
    //每秒容許經過的請求數
    private static final long requestsPerSecond = 100;
    //業務處理方法
    public void invoke(Request request) {
        try {
            //guava key
            long guavaKey = System.currentTimeMillis() / 1000;
            //請求累加數
            long guavaVal = guava.get(guavaKey).incrementAndGet();
            if (guavaVal <= requestsPerSecond) {
                //業務處理
                doBusiness(request);
            } else {
                return;
            }
        } catch (Exception e) {
            //錯誤處理
            doException(request, e);
        }
    }

從上面的代碼中能夠看到,咱們巧妙的利用了緩存組件的特性來實現。每當有請求進來,緩存組件中的key值累加,到達閾值則拒絕後續請求,這樣很方便的實現了時間段限流的效果。雖然例子中給的是按照秒來限流的實現,咱們能夠在此基礎上更改成按照分鐘或者按照小時來實現的方案。

優勢:操做簡單,可靠性強

缺點:突增的流量,會致使每一個請求都會訪問guava,因爲guava是堆內內存實現,勢必會對性能有一點點影響。其實若是怕限流影響到其餘內存計算,咱們能夠將此限流操做用堆外內存組件來實現,好比利用OHC或者mapdb等。也是比較好的備選方案。

 

4. 基於漏桶算法的單機限流

所謂漏桶(Leaky bucket),則是指,有一個盛水的池子,而後有一個進水口,有一個出水口,進水口的水流可大可小,可是出水口的水流是恆定的。下圖圖示能夠顯示的更加清晰:

image

從圖中咱們能夠看到,水龍頭至關於各端的流量,進入到漏桶中,當流量很小的時候,漏桶能夠承載這種流量,出水口按照恆定的速度出水,水不會溢出來。當流量開始增大的時候,漏桶中的出水速度趕不上進水速度,那麼漏桶中的水位一直在上漲。當流量再大,則漏桶中的水過滿則溢。

因爲目前不少MQ,好比rabbitmq等,都屬於漏桶算法原理的具體實現,請求過來先入queue隊列,隊列滿了拋棄多餘請求,以後consumer端勻速消費隊列裏面的數據。因此這裏再也不貼多餘的代碼。

優勢:流量控制效果不錯

缺點:不可以很好的應付突增的流量。適合保護性能較弱的系統,可是不適合性能較強的系統。若是性能較強的系統可以應對這種突增的流量的話,那麼漏桶算法是不合適的。

 

5. 基於令牌桶算法的單機限流

所謂令牌桶(Token Bucket),則是指,請求過來的時候,先去令牌桶裏面申請令牌,申請到令牌以後,才能去進行業務處理。若是沒有申請到令牌,則操做終止。具體說明以下圖:

image

因爲生成令牌的流量是恆定的,面對突增流量的時候,桶裏有足夠令牌的狀況下,突增流量能夠快速的獲取到令牌,而後進行處理。從這裏能夠看出令牌桶對於突增流量的處理是允許的。

因爲目前guava組件中已經有了對令牌桶的具體實現類:RateLimiter, 因此咱們能夠藉助此類來實現咱們的令牌桶限流。代碼以下:

    //指定每秒放1個令牌
    private static RateLimiter limiter = RateLimiter.create(1);
    //令牌獲取超時時間
    private static final long acquireTimeout = 1000;
    //業務處理方法
    public void invoke(Request request) {
        try {
            //拿到令牌則進行業務處理
            if (limiter.tryAcquire(acquireTimeout, TimeUnit.MILLISECONDS)) {
                //業務處理
                doBusiness(request);
            }
            //拿不到令牌則退出
            else {
                return;
            }
        } catch (Exception e) {
            //錯誤處理
            doException(request, e);
        }
    }

從上面代碼咱們能夠看到,一秒生成一個令牌,那麼咱們的接口限定爲一秒處理一個請求,若是感受接口性能能夠達到1000tps單機,那麼咱們能夠適當的放大令牌桶中的令牌數量,好比800,那麼當突增流量過來,會直接拿到令牌而後進行業務處理。可是當令牌桶中的令牌消費完畢以後,那麼請求就會被阻塞,直到下一秒另外一批800個令牌生成出來,請求才開始繼續進行處理。

因此利用令牌桶的優缺點就很明顯了:

有點:使用簡單,有成熟組件

缺點:適合單機限流,不適合分佈式限流。

 

6. 基於redis lua的分佈式限流

因爲上面5中限流方式都是單機限流,可是在實際應用中,不少時候咱們不只要作單機限流,還要作分佈式限流操做。因爲目前作分佈式限流的方法很是多,我就再也不一一贅述了。咱們今天用到的分佈式限流方法,是redis+lua來實現的。

爲何用redis+lua來實現呢?緣由有兩個:

其一:redis的性能很好,處理能力強,且容災能力也不錯。

其二:一個lua腳本在redis中就是一個原子性操做,能夠保證數據的正確性。

因爲要作限流,那麼確定有key來記錄限流的累加數,此key能夠隨着時間進行任意變更。並且key須要設置過時參數,防止無效數據過多而致使redis性能問題。

來看看lua代碼:

            --限流的key
            local key = 'limitkey'..KEYS[1]

            --累加請求數
            local val = tonumber(redis.call('get', key) or 0)

            --限流閾值
            local threshold = tonumber(ARGV[1])

            if  val>threshold then
                --請求被限
                return 0
            else
                --遞增請求數
                redis.call('INCRBY', key, "1")
                --5秒後過時
                redis.call('expire', key, 5)
                --請求經過
                return 1
            end

以後就是直接調用使用,而後根據返回內容爲0仍是1來斷定業務邏輯能不能走下去就好了。這樣能夠經過此代碼段來控制整個集羣的流量,從而避免出現雪崩效應。固然此方案的解決方式也能夠利用zk來進行,因爲zk的強一致性保證,不失爲另外一種好的解決方案,可是因爲zk的性能沒有redis好,因此若是在乎性能的話,仍是用redis吧。

優勢:集羣總體流量控制,防止雪崩效應

缺點:須要引入額外的redis組件,且要求redis支持lua腳本。

 

總結

經過以上6種限流方式的講解,主要是想起到拋磚引玉的做用,期待你們更好更優的解決方法。

以上代碼都是僞代碼,使用的時候請進行線上驗證,不然帶來了反作用的話,就得不償失了。

相關文章
相關標籤/搜索