高可用之限流-07-token bucket 令牌桶演算法

老马啸西风發表於2024-10-15

限流系列

開源元件 rate-limit: 限流

高可用之限流-01-入門介紹

高可用之限流-02-如何設計限流框架

高可用之限流-03-Semaphore 訊號量做限流

高可用之限流-04-fixed window 固定視窗

高可用之限流-05-slide window 滑動視窗

高可用之限流-06-slide window 滑動視窗 sentinel 原始碼

高可用之限流-07-token bucket 令牌桶演算法

高可用之限流 08-leaky bucket漏桶演算法

高可用之限流 09-guava RateLimiter 入門使用簡介 & 原始碼分析

令牌桶演算法

令牌桶演算法是網路流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種演算法。

典型情況下,令牌桶演算法用來控制傳送到網路上的資料的數目,並允許突發資料的傳送。

令牌桶演算法的原理是系統會以一個恆定的速度往桶裡放入令牌,而如果請求需要被處理,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。

從原理上看,令牌桶演算法和漏桶演算法是相反的,一個“進水”,一個是“漏水”。

Google的Guava包中的RateLimiter類就是令牌桶演算法的解決方案。

漏桶演算法和令牌桶演算法的選擇

漏桶演算法與令牌桶演算法在表面看起來類似,很容易將兩者混淆。但事實上,這兩者具有截然不同的特性,且為不同的目的而使用。

漏桶演算法與令牌桶演算法的區別在於,漏桶演算法能夠強行限制資料的傳輸速率,令牌桶演算法能夠在限制資料的平均傳輸速率的同時還允許某種程度的突發傳輸。

需要注意的是,在某些情況下,漏桶演算法不能夠有效地使用網路資源,因為漏桶的漏出速率是固定的,所以即使網路中沒有發生擁塞,漏桶演算法也不能使某一個單獨的資料流達到埠速率。

因此,漏桶演算法對於存在突發特性的流量來說缺乏效率。而令牌桶演算法則能夠滿足這些具有突發特性的流量。

通常,漏桶演算法與令牌桶演算法結合起來為網路流量提供更高效的控制。

演算法描述與實現

描述

假如使用者配置的平均傳送速率為r,則每隔1/r秒一個令牌被加入到桶中(每秒會有r個令牌放入桶中);

假設桶中最多可以存放b個令牌。如果令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄;

當一個n個位元組的資料包到達時,就從令牌桶中刪除n個令牌(不同大小的資料包,消耗的令牌數量不一樣),並且資料包被髮送到網路;

如果令牌桶中少於n個令牌,那麼不會刪除令牌,並且認為這個資料包在流量限制之外(n個位元組,需要n個令牌。該資料包將被快取或丟棄);

演算法允許最長b個位元組的突發,但從長期執行結果看,資料包的速率被限制成常量r。

對於在流量限制外的資料包可以以不同的方式處理:

1)它們可以被丟棄;

2)它們可以排放在佇列中以便當令牌桶中累積了足夠多的令牌時再傳輸;

3)它們可以繼續傳送,但需要做特殊標記,網路過載的時候將這些特殊標記的包丟棄。

實現

新增令牌的時機

我們當然不用起一個定時任務不停的向桶中新增令牌,只要在訪問時記下訪問時間,下次訪問時計算出兩次訪問時間的間隔,然後向桶中補充令牌,補充的令牌數為

(long) (durationMs * rate * 1.0 / 1000)

其中rate是每秒補充令牌數。

這裡可以前傻傻的一個任務不停的執行對比起來,還是比較巧妙的。

初步實現

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.rate.limit.core.core.ILimitContext;
import com.github.houbb.rate.limit.core.util.ExecutorServiceUtil;
import org.apiguardian.api.API;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 令牌桶演算法
 *
 * @author houbinbin
 * Created by bbhou on 2017/9/20.
 * @since 0.0.6
 */
public class LimitTokenBucket extends LimitAdaptor {

    private static final Log LOG = LogFactory.getLog(LimitTokenBucket.class);

    /**
     * 令牌的發放速率
     * <p>
     * 每一秒發放多少。
     *
     * @since 0.0.6
     */
    private final long rate;

    /**
     * 容量
     * <p>
     * 後期暴露為可以配置
     *
     * @since 0.0.6
     */
    private final long capacity;

    /**
     * 令牌數量
     *
     * @since 0.0.6
     */
    private volatile long tokenNum;

    /**
     * 上一次的更新時間
     *
     * @since 0.0.6
     */
    private volatile long lastUpdateTime;

    /**
     * 構造器
     *
     * @param context 上下文
     * @since 0.0.4
     */
    public LimitTokenBucket(final ILimitContext context) {
        // 暫不考慮特殊輸入,比如 1s 令牌少於1 的場景
        long intervalSeconds = context.timeUnit().toSeconds(context.interval());
        this.rate = context.count() / intervalSeconds;

        // 8 的資料
        this.capacity = this.rate * 8;
        // 這裡可以慢慢的加,初始化設定為0
        // 這樣就有一個 warmUp 的過程
        this.tokenNum = 0;
        this.lastUpdateTime = System.currentTimeMillis();
    }

    /**
     * 獲取鎖
     *
     * @since 0.0.5
     */
    @Override
    public synchronized boolean acquire() {

        if (tokenNum < 1) {
            // 加入令牌
            long now = System.currentTimeMillis();
            long durationMs = now - lastUpdateTime;
            long newTokenNum = (long) (durationMs * 1.0 * rate / 1000);

            LOG.debug("[Limit] new token is " + newTokenNum);
            if (newTokenNum > 0) {
                // 超過的部分將捨棄
                this.tokenNum = Math.min(newTokenNum + this.tokenNum, capacity);
                lastUpdateTime = now;
            } else {
                // 時間不夠
                return false;
            }
        }

        this.tokenNum--;
        return true;
    }

}

測試

  • Test.java
import com.github.houbb.heaven.util.util.TimeUtil;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.rate.limit.core.bs.LimitBs;
import com.github.houbb.rate.limit.core.core.ILimit;
import com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket;

/**
 * <p> project: rate-limit-LimitTokenBucketTest </p>
 * <p> create on 2020/6/22 22:38 </p>
 *
 * @author binbin.hou
 * @since 0.0.6
 */
public class LimitTokenBucketTest {

    private static final Log LOG = LogFactory.getLog(LimitTokenBucket.class);

    /**
     * 2S 內最多執行 5 次
     * @since 0.0.5
     */
    private static final ILimit LIMIT = LimitBs.newInstance()
            .interval(1)
            .count(10)
            .limit(LimitTokenBucket.class)
            .build();

    static class LimitRunnable implements Runnable {
        @Override
        public void run() {
            for(int i = 0; i < 6; i++) {
                while (!LIMIT.acquire()) {
                    // 等待令牌
                    TimeUtil.sleep(100);
                }

                LOG.info("{}-{}", Thread.currentThread().getName(), i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new LimitRunnable()).start();
    }

}
  • 日誌
22:47:19.084 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-0
22:47:19.186 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-1
22:47:19.286 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-2
22:47:19.386 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-3
22:47:19.486 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-4
22:47:19.586 [Thread-1] INFO  com.github.houbb.rate.limit.core.core.impl.LimitTokenBucket - Thread-1-5

相對來說令牌桶還是比較平滑的。

小結

令牌桶演算法是網路流量整形和速率限制中最常使用的一種演算法。

典型情況下,令牌桶演算法用來控制傳送到網路上的資料的數目,並允許突發資料的傳送。

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

傳送到令牌桶的資料包需要消耗令牌。不同大小的資料包,消耗的令牌數量不一樣。令牌桶這種控制機制基於令牌桶中是否存在令牌來指示什麼時候可以傳送流量。令牌桶中的每一個令牌都代表一個位元組。如果令牌桶中存在令牌,則允許傳送流量;而如果令牌桶中不存在令牌,則不允許傳送流量。

因此,如果突發門限被合理地配置並且令牌桶中有足夠的令牌,那麼流量就可以以峰值速率傳送。

原理

ps: 原理相對枯燥,理解即可。

令牌桶是網路裝置的內部儲存池,而令牌則是以給定速率填充令牌桶的虛擬資訊包。每個到達的令牌都會從資料佇列領出相應的資料包進行傳送,傳送完資料後令牌被刪除。

請求註解(RFC)中定義了兩種令牌桶演算法——單速率三色標記演算法和雙速率三色標記演算法,其評估結果都是為報文打上紅、黃、綠三色標記。

QoS會根據報文的顏色,設定報文的丟棄優先順序,其中單速率三色標記比較關心報文尺寸的突發,而雙速率三色標記則關注速率上的突發,兩種演算法都可工作於色盲模式和非色盲模式。

以下結合這兩種工作模式介紹一下RFC中所描述的這兩種演算法。

1)單速率三色標記演算法

網路工程師任務小組(IETF)的RFC檔案定義了單速率三色標記演算法,評估依據以下3個引數:承諾訪問速率(CIR),即向令牌桶中填充令牌的速率;承諾突發尺寸(CBS),即令牌桶的容量,每次突發所允許的最大流量尺寸(注:設定的突發尺寸必須大於最大報文長度);超額突發尺寸(EBS)。

一般採用雙桶結構:C桶和E桶。Tc表示C桶中的令牌數,Te表示E桶中令牌數,兩桶的總容量分別為CBS和EBS。初始狀態時兩桶是滿的,即Tc和Te初始值分別等於CBS和EBS。令牌的產生速率是CIR,通常是先往C桶中新增令牌,等C桶滿了,再往E桶中新增令牌,當兩桶都被填滿時,新產生的令牌將會被丟棄。

色盲模式下,假設到達的報文長度為B。若報文長度B小於C桶中的令牌數Tc,則報文被標記為綠色,且C桶中的令牌數減少B;若 Tc < B < Te,則標記為黃色,E和C桶中的令牌數均減少B;若 B > Te,標記為紅色,兩桶總令牌數都不減少。

在非色盲模式下,若報文已被標記為綠色或 B < Tc,則報文被標記為綠色,Tc減少B;若報文已被標記為黃色或 Tc < B < Te,則標記為黃色,且Te減少B;若報文已被標記為紅色或 B > Te,則標記為紅色,Tc和Te都不減少。

2)雙速率三色標記演算法

IETF的RFC檔案定義了雙速率三色演算法,主要是根據4種流量引數來評估:CIR、CBS、峰值資訊速率(PIR),峰值突發尺寸(PBS)。前兩種引數與單速率三色演算法中的含義相同,PIR這個引數只在交換機上才有,路由器沒有這個引數。該值必須不小於CIR的設定值,如果大於CIR,則速率限制在CIR於PRI之間的一個值。

與單速率三色標記演算法不同,雙速率三色標記演算法的兩個令牌桶C桶和P桶填充令牌的速率不同,C桶填充速率為CIR,P桶為PIR;兩桶的容量分別為CBS和PBS。用Tc和Tp表示兩桶中的令牌數目,初始狀態時兩桶是滿的,即Tc和Tp初始值分別等於CBS和PBS。

色盲模式下,如果到達的報文速率大於PIR,超過Tp+Tc部分無法得到令牌,報文被標記為紅色,未超過Tp+Tc而從P桶中獲取令牌的報文標記為黃色,從C桶中獲取令牌的報文被標記為綠色;當報文速率小於PIR,大於CIR時,報文不會得不到令牌,但超過Tp部分報文將從P桶中獲取令牌,被標記為黃色報文,從C桶中獲取令牌的報文被標記為綠色;當報文速率小於CIR時,報文所需令牌數不會超過Tc,只從C桶中獲取令牌,所以只會被標記為綠色報文。

在非色盲模式下,如果報文已被標記為紅色或者超過Tp+Tc部分無法得到令牌的報文,被標記為紅色;如果標記為黃色或者超過Tc未超過Tp部分報文記為黃色;如果報文被標記為綠或未超過Tc部分報文,被標記為綠色。

擴充閱讀

guava 原始碼解析

漏桶演算法實現

參考資料

漏桶演算法&令牌桶演算法理解及常用的演算法

流量控制演算法——漏桶演算法和令牌桶演算法

Token Bucket 令牌桶演算法

華為-令牌桶演算法

簡單分析Guava中RateLimiter中的令牌桶演算法的實現

網路擁塞及其對策

令牌桶演算法限流

程式設計師必會 | 限流限速RateLimiter

令牌桶(TokenBucket)限流 - Java實現

相關文章