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

hxl發表於2020-11-16

不管是在單體服務中還是在微服務中,開發者為前端提供的 API 介面都是有訪問上限的,當訪問頻率或者併發量超過其承受範圍時候,我們就必須考慮限流來保證介面的可用性或者降級可用性。即介面也需要安裝上保險絲,以防止非預期的請求對系統壓力過大而引起的系統癱瘓。

go-zero 整合了開箱即用的 限流器 。其中內建了兩種限流器,也對應兩類使用場景:

種類 原理 場景
periodlimit 單位時間限制訪問次數 需要強行限制資料的傳輸速率
tokenlimit 令牌桶限流 限制資料的平均傳輸速率,同時允許某種程度的突發傳輸

本文就來介紹一下 periodlimit

使用

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 藉助 redisincrby 做資源訪問計數
  • 採用 lua script 做整個視窗計算,保證計算的原子性

下面來看看 lua script 控制的幾個關鍵屬性:

argument mean
key[1] 訪問資源的標示
ARGV[1] limit => 請求總數,超過則限速。可設定為 QPS
ARGV[2] window 大小 => 滑動視窗,用 ttl 模擬出滑動的效果
-- 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 時後續發生的情況:

後續處理

如果在服務某個時間點,請求大批量打進來,periodlimit 短期時間內達到 limit 閾值,而且設定的時間範圍還遠遠沒有到達。後續請求的處理就成為問題。

periodlimit 中並沒有處理,而是返回 code 。把後續請求的處理交給了開發者自己處理。

  1. 如果不做處理,那就是簡單的將請求拒絕
  2. 如果需要處理這些請求,開發者可以藉助 mq 將請求緩衝,減緩請求的壓力
  3. 採用 tokenlimit,允許暫時的流量衝擊

所以下一篇我們就來聊聊 tokenlimit

總結

go-zero 中的 periodlimit 限流方案是基於 redis 計數器,通過呼叫 redis lua script ,保證計數過程的原子性,同時保證在分散式的情況下計數是正常的。

但是這種方案也存在缺點,因為它要記錄時間視窗內的所有行為記錄,如果這個量特別大的時候,記憶體消耗會變得非常嚴重。

參考

同時歡迎大家使用 go-zero 並加入我們,https://github.com/tal-tech/go-zero

更多原創文章乾貨分享,請關注公眾號
  • go-zero 如何扛住流量衝擊(一)
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章