1. 程式人生 > >go-zero 如何扛住流量衝擊(一)

go-zero 如何扛住流量衝擊(一)

不管是在單體服務中還是在微服務中,開發者為前端提供的API介面都是有訪問上限的,當訪問頻率或者併發量超過其承受範圍時候,我們就必須考慮限流來保證介面的可用性或者降級可用性。即介面也需要安裝上保險絲,以防止非預期的請求對系統壓力過大而引起的系統癱瘓。 `go-zero` 集成了開箱即用的 **限流器** 。其中內建了兩種限流器,也對應兩類使用場景: | **種類** | **原理** | **場景** | | ------------- | -------------------- | -------------------------------------------------- | | `periodlimit` | 單位時間限制訪問次數 | 需要強行限制資料的傳輸速率 | | `tokenlimit` | 令牌桶限流 | 限制資料的平均傳輸速率,同時允許某種程度的突發傳輸 | 本文就來介紹一下 `periodlimit` 。 ## 使用 ```go const ( seconds = 1 total = 100 quota = 5 ) // New limiter l := NewPeriodLimit(seconds, quota, redis.NewRedis(s.Addr(), redis.NodeType), "periodlimit") // take source code, err := l.Take("first") if err != nil { logx.Error(err) return true } // switch val => process request switch code { case limit.OverQuota: logx.Errorf("OverQuota key: %v", key) return false case limit.Allowed: logx.Infof("AllowedQuota key: %v", key) return true case limit.HitQuota: logx.Errorf("HitQuota key: %v", key) // todo: maybe we need to let users know they hit the quota return false default: logx.Errorf("DefaultQuota key: %v", key) // unknown response, we just let the sms go return true } ``` ## periodlimit `go-zero` 採取 **滑動視窗** 計數的方式,計算一段時間內對同一個資源的訪問次數,如果超過指定的 `limit` ,則拒絕訪問。當然如果你是在一段時間內訪問不同的資源,每一個資源訪問量都不超過 `limit` ,此種情況是允許大量請求進來的。 而在一個分散式系統中,存在多個微服務提供服務。所以當瞬間的流量同時訪問同一個資源,如何讓計數器在分散式系統中正常計數? 同時在計算資源訪問時,可能會涉及多個計算,如何保證計算的原子性? - `go-zero` 藉助 `redis` 的 `incrby` 做資源訪問計數 - 採用 `lua script` 做整個視窗計算,保證計算的原子性 下面來看看 `lua script` 控制的幾個關鍵屬性: | **argument** | **mean** | | ------------ | ----------------------------------------------- | | key[1] | 訪問資源的標示 | | ARGV[1] | limit => 請求總數,超過則限速。可設定為 QPS | | ARGV[2] | window大小 => 滑動視窗,用 ttl 模擬出滑動的效果 | ```lua -- to be compatible with aliyun redis, -- we cannot use `local key = KEYS[1]` to reuse thekey local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) -- incrbt key 1 => key visis++ local current = redis.call("INCRBY", KEYS[1], 1) -- 如果是第一次訪問,設定過期時間 => TTL = window size -- 因為是隻限制一段時間的訪問次數 if current == 1 then redis.call("expire", KEYS[1], window) return 1 elseif current < limit then return 1 elseif current == limit then return 2 else return 0 end ``` 至於上述的 `return code` ,返回給呼叫方。由呼叫方來決定請求後續的操作: | **return code** | **tag** | call code | **mean** | | --------------- | --------- | --------- | -------------- | | 0 | OverQuota | 3 | **over limit** | | 1 | Allowed | 1 | **in limit** | | 2 | HitQuota | 2 | **hit limit** | 下面這張圖描述了請求進入的過程,以及請求觸發 `limit` 時後續發生的情況: ![](https://img2020.cnblogs.com/other/14470/202011/14470-20201117120014665-1691005333.png) ![](https://img2020.cnblogs.com/other/14470/202011/14470-20201117120014859-992882879.png) ## 後續處理 如果在服務某個時間點,請求大批量打進來,`periodlimit` 短期時間內達到 `limit` 閾值,而且設定的時間範圍還遠遠沒有到達。後續請求的處理就成為問題。 `periodlimit` 中並沒有處理,而是返回 `code` 。把後續請求的處理交給了開發者自己處理。 1. 如果不做處理,那就是簡單的將請求拒絕 2. 如果需要處理這些請求,開發者可以藉助 `mq` 將請求緩衝,減緩請求的壓力 3. 採用 `tokenlimit`,允許暫時的流量衝擊 所以下一篇我們就來聊聊 `tokenlimit` ## 總結 `go-zero` 中的 `periodlimit` 限流方案是基於 `redis` 計數器,通過呼叫 `redis lua script` ,保證計數過程的原子性,同時保證在分散式的情況下計數是正常的。 但是這種方案也存在缺點,因為它要記錄時間視窗內的所有行為記錄,如果這個量特別大的時候,記憶體消耗會變得非常嚴重。 ## 參考 - [go-zero periodlimit](https://github.com/tal-tech/go-zero/blob/master/core/limit/periodlimit.go) - [分散式服務限流實戰,已經為你排好坑了](https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673) 同時歡迎大家使用 `go-zero` 並加入我們,https://github.com/tal-tech/go-zero 如果覺得文章不錯,歡迎github點個star