本篇文章承接上一篇go-zero 如何扛住流量衝擊(一)。node
上一篇介紹的是 go-zero
中滑動窗口限流,本篇介紹另一個 tokenlimit
,令牌桶限流。git
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)
從總體上令牌桶生產token邏輯以下:github
go-zero
在兩類限流器下都採起 lua script
的方式,依賴redis能夠作到分佈式限流,lua script
同時能夠作到對 token 生產讀取操做的原子性。redis
下面來看看 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
限流方案適用於瞬時流量衝擊,現實請求場景並不以恆定的速率。令牌桶至關預請求,當真實的請求到達不至於瞬間被打垮。當流量衝擊到必定程度,則纔會按照預約速率進行消費。ui
可是生產token
上,不能按照當時的流量狀況做出動態調整,不夠靈活,還能夠進行進一步優化。此外能夠參考Token bucket WIKI中提到分層令牌桶,根據不一樣的流量帶寬,分至不一樣排隊中。atom
若是以爲文章不錯,歡迎 github 點個star 🤝lua
同時歡迎你們使用 go-zero
,https://github.com/tal-tech/go-zero