本篇文章承接上一篇go-zero 如何扛住流量沖擊(一),
上一篇介紹的是 go-zero 中滑動視窗限流,本篇介紹另外一個 tokenlimit ,令牌桶限流,
使用
const (
burst = 100
rate = 100
seconds = 5
)
store := redis.NewRedis("localhost:6379", "node", "")
fmt.Println(store.Ping())
// New tokenLimiter
limiter := limit.NewTokenLimiter(rate, burst, store, "rate-test")
timer := time.NewTimer(time.Second * seconds)
quit := make(chan struct{})
defer timer.Stop()
go func() {
<-timer.C
close(quit)
}()
var allowed, denied int32
var wait sync.WaitGroup
for i := 0; i < runtime.NumCPU(); i++ {
wait.Add(1)
go func() {
for {
select {
case <-quit:
wait.Done()
return
default:
if limiter.Allow() {
atomic.AddInt32(&allowed, 1)
} else {
atomic.AddInt32(&denied, 1)
}
}
}
}()
}
wait.Wait()
fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds)
tokenlimit
從整體上令牌桶生產token邏輯如下:
- 用戶配置的平均發送速率為r,則每隔1/r秒一個令牌被加入到桶中;
- 假設桶中最多可以存放b個令牌,如果令牌到達時令牌桶已經滿了,那么這個令牌會被丟棄;
- 當流量以速率v進入,從桶中以速率v取令牌,拿到令牌的流量通過,拿不到令牌流量不通過,執行熔斷邏輯;
go-zero 在兩類限流器下都采取 lua script 的方式,依賴redis可以做到分布式限流,lua script同時可以做到對 token 生產讀取操作的原子性,
下面來看看 lua script 控制的幾個關鍵屬性:
| argument | mean |
|---|---|
| ARGV[1] | rate 「每秒生成幾個令牌」 |
| ARGV[2] | burst 「令牌桶最大值」 |
| ARGV[3] | now_time「當前時間戳」 |
| ARGV[4] | get token nums 「開發者需要獲取的token數」 |
| KEYS[1] | 表示資源的tokenkey |
| KEYS[2] | 表示重繪時間的key |
-- 回傳是否可以活獲得預期的token
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
-- fill_time:需要填滿 token_bucket 需要多久
local fill_time = capacity/rate
-- 將填充時間向下取整
local ttl = math.floor(fill_time*2)
-- 獲取目前 token_bucket 中剩余 token 數
-- 如果是第一次進入,則設定 token_bucket 數量為 令牌桶最大值
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
-- 上一次更新 token_bucket 的時間
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
-- 通過當前時間與上一次更新時間的跨度,以及生產token的速率,計算出新的token數
-- 如果超過 max_burst,多余生產的token會被丟棄
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
-- 更新新的token數,以及更新時間
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return allowed
上述可以看出 lua script :只涉及對 token 操作,保證 token 生產合理和讀取合理,
函式分析

從上述流程中看出:
- 有多重保障機制,保證限流一定會完成,
- 如果
redis limiter失效,至少在行程內rate limiter兜底, - 重試
redis limiter機制保證盡可能地正常運行,
總結
go-zero 中的 tokenlimit 限流方案適用于瞬時流量沖擊,現實請求場景并不以恒定的速率,令牌桶相當預請求,當真實的請求到達不至于瞬間被打垮,當流量沖擊到一定程度,則才會按照預定速率進行消費,
但是生產token上,不能按照當時的流量情況作出動態調整,不夠靈活,還可以進行進一步優化,此外可以參考Token bucket WIKI中提到分層令牌桶,根據不同的流量帶寬,分至不同排隊中,
參考
- go-zero tokenlimit
- Go-Redis 提供的分布式限流庫
如果覺得文章不錯,歡迎 github 點個star ??
同時歡迎大家使用 go-zero ,https://github.com/tal-tech/go-zero
專案地址:
https://github.com/tal-tech/go-zero
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/227638.html
標籤:Go
上一篇:08 選擇排序
下一篇:Java 列舉 enum 詳解
