GO 流量限制之令牌桶的實現

0x00 流量限制的手段

流量限制的手段有很多,最常見的:漏桶、令牌桶兩種:

  1. 漏桶是指我們有一個一直裝滿了水的桶,每過固定的一段時間即向外漏一滴水。如果你接到了這滴水,那麼你就可以繼續服務請求,如果沒有接到,那麼就需要等待下一滴水。
  2. 令牌桶則是指勻速向桶中添加令牌,服務請求時需要從桶中獲取令牌,令牌的數目可以按照需要消耗的資源進行相應的調整。如果沒有令牌,可以選擇等待,或者放棄。

這兩種方法看起來很像,不過還是有區別的。漏桶流出的速率固定,而令牌桶只要在桶中有令牌,那就可以拿。也就是說令牌桶是允許一定程度的併發的,比如同一個時刻,有100個用戶請求,只要令牌桶中有100個令牌,那麼這100個請求全都會放過去。令牌桶在桶中沒有令牌的情況下也會退化爲漏桶模型。

0x01 簡單令牌桶的實現

簡單令牌桶的實現原理是使用定時器,每隔一定時間間隔向桶內放入令牌,實現如下:


// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
	var bucketToken = make(chan struct{}, maxCapacity)
	timeD := time.Second / time.Duration(capacityPs)
	ticker := time.NewTicker(timeD)
	go func() {
		for {
			select {
			case <-ticker.C:
				select {
				case bucketToken <- struct{}{}:
				default:
				}
			}
			fmt.Println(len(bucketToken), time.Now())
		}
	}()
	return bucketToken
}

有新的請求時需要獲取token,獲取token時可以選擇阻塞或者非阻塞兩種方式,獲取token的實現方式如下:

func GetToken(block bool, bucket *chan struct{}) bool {
	if block {
		select {
		case <-*bucket:
			return true
		}
	} else {
		select {
		case <-*bucket:
			return true
		default:
			return false
		}
	}
}

整體方法的使用方式如下:

package main

import (
	"fmt"
	"time"
)

// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
	var bucketToken = make(chan struct{}, maxCapacity)
	timeD := time.Second / time.Duration(capacityPs)
	ticker := time.NewTicker(timeD)
	go func() {
		for {
			select {
			case <-ticker.C:
				select {
				case bucketToken <- struct{}{}:
				default:
				}
			}
			fmt.Println(len(bucketToken), time.Now())
		}
	}()
	return bucketToken
}

func GetToken(block bool, bucket *chan struct{}) bool {
	if block {
		select {
		case <-*bucket:
			return true
		}
	} else {
		select {
		case <-*bucket:
			return true
		default:
			return false
		}
	}
}

func main() {

	capacityPs := 50
	maxCapacity := 100
	bucket := GetBucket(capacityPs, maxCapacity)
	for {
		time.Sleep(100 * time.Millisecond)
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
	}

}

0x02 github.com/juju/ratelimit

取消定時填充token邏輯,改爲記錄上次填充時間,通過計算距今時間差值來計算該時間段內應填充的數量,一次性進行填充。

記:

  • 桶中剩餘的token數量爲:cnt
  • token數量上限爲: cap
  • 每次填充的時間間隔爲:d
  • 每次填充的token數爲: q
  • 上次填充的時間爲:t1
  • 當前時間爲: t2

則在 t2 時刻桶中的token數量應該爲:

min( cnt + ((t2 - t1) / d) * q , cap )

0x03 github.com/uber-go/ratelimit

  • TODO

沒太看懂,有時間再看

0x04 參考文檔

本文參考了《Go語言高級編程》,作者:柴樹杉、曹春暉。

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