golang限流庫(kù)兩個(gè)大bug(半年之久無(wú)人提起)
uber-go/ratelimit 庫(kù)
我先前都是使用juju/ratelimit[2]這個(gè)限流庫(kù)的,不過(guò)我不太喜歡這個(gè)庫(kù)的復(fù)雜的“構(gòu)造函數(shù)”,后來(lái)嘗試了uber-go/ratelimit[3]這個(gè)庫(kù)后,感覺(jué) SDK 設(shè)計(jì)比較簡(jiǎn)單,而且使用起來(lái)也不錯(cuò),就一直使用了。當(dāng)時(shí)的版本是v0.2.0
,而且我也不會(huì)設(shè)置它的slack
參數(shù),所以也相安無(wú)事。
最近我同事在做項(xiàng)目的時(shí)候,把這個(gè)庫(kù)更新到最新的v0.3.0
,發(fā)現(xiàn)在發(fā)包一段時(shí)間后,突然限流不起作用了,發(fā)包頻率狂飆導(dǎo)致程序 panic。
通過(guò)單元測(cè)試復(fù)現(xiàn)
很容易通過(guò)下面一個(gè)單元測(cè)試復(fù)現(xiàn)這個(gè)問(wèn)題:
func TestLimiter(t *testing.T) { limiter := ratelimit.New(1, ratelimit.Per(time.Second), ratelimit.WithSlack(1)) for i := 0; i < 25; i++ { if i == 1 { time.Sleep(2 * time.Second) } limiter.Take() fmt.Println(time.Now().Unix(), i) // burst } }
slack 的判斷邏輯出現(xiàn)問(wèn)題
這個(gè)單元測(cè)試嘗試在第二個(gè)周期中不調(diào)用限流器,讓它有機(jī)會(huì)進(jìn)入 slack 判斷的邏輯。這個(gè)庫(kù)的 slack 設(shè)計(jì)的本意是在 rate 的基礎(chǔ)上留一點(diǎn)余地,不那么嚴(yán)格按照 rate 進(jìn)行限流,不過(guò)因?yàn)?code>v0.3.0代碼的問(wèn)題,導(dǎo)致 slack 的判斷邏輯出現(xiàn)了問(wèn)題:
func (t *atomicInt64Limiter) Take() time.Time { var ( newTimeOfNextPermissionIssue int64 now int64 ) for { now = t.clock.Now().UnixNano() timeOfNextPermissionIssue := atomic.LoadInt64(&t.state) switch { case timeOfNextPermissionIssue == 0 || (t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest)): // if this is our first call or t.maxSlack == 0 we need to shrink issue time to now newTimeOfNextPermissionIssue = now case t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack): // a lot of nanoseconds passed since the last Take call // we will limit max accumulated time to maxSlack newTimeOfNextPermissionIssue = now - int64(t.maxSlack) default: // calculate the time at which our permission was issued newTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest) } if atomic.CompareAndSwapInt64(&t.state, timeOfNextPermissionIssue, newTimeOfNextPermissionIssue) { break } } sleepDuration := time.Duration(newTimeOfNextPermissionIssue - now) if sleepDuration > 0 { t.clock.Sleep(sleepDuration) return time.Unix(0, newTimeOfNextPermissionIssue) } // return now if we don't sleep as atomicLimiter does return time.Unix(0, now) }
原理分析
一旦進(jìn)入case t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack):
這個(gè)分支,你會(huì)發(fā)現(xiàn)后續(xù)調(diào)用Take
基本都會(huì)進(jìn)入這個(gè)分支,程序不會(huì)阻塞,只要調(diào)用Take
都不會(huì)阻塞??梢钥吹疆?dāng)設(shè)置 slack>0 的時(shí)候才會(huì)進(jìn)入這個(gè)分支,正好默認(rèn) slack=10。這個(gè) bug 也可以推算出來(lái)。假設(shè)當(dāng)前進(jìn)入這個(gè)分支,當(dāng)前時(shí)間是 now1,那么這次 Take 就會(huì)把newTimeOfNextPermissionIssue
設(shè)置為 now1-int64(t.maxSlack)
。
接下來(lái)再調(diào)用 Take,當(dāng)前時(shí)間是 now2,now2 總是會(huì)比 now1 大一點(diǎn),至少大幾納秒吧。這個(gè)時(shí)候我們計(jì)算分支的條件now-timeOfNextPermissionIssue > int64(t.maxSlack)
,這個(gè)條件肯定是成立的,因?yàn)?code>now2-(now1-int64(t.maxSlack)) = (now2-now1) + int64(t.maxSlack)
> int64(t.maxSlack)
。導(dǎo)致后續(xù)的每次 Take 都會(huì)進(jìn)入這個(gè)分支,不會(huì)阻塞,導(dǎo)致程序瘋狂發(fā)包,最終導(dǎo)致 panic。
周末的時(shí)候我給這個(gè)項(xiàng)目提了一個(gè) bug, 它的一個(gè)維護(hù)者進(jìn)行了修復(fù),不過(guò)這個(gè)項(xiàng)目主要開發(fā)者已經(jīng)對(duì)這個(gè)v0.3.0
的實(shí)現(xiàn)喪失了信心,因?yàn)檫@個(gè)實(shí)現(xiàn)已經(jīng)出現(xiàn)過(guò)一次類似的 bug,被他回滾后了,后來(lái)有被修復(fù)才合進(jìn)來(lái),現(xiàn)在有出現(xiàn) bug 了。
不管作者修不修復(fù),你一定要注意,使用這個(gè)庫(kù)的v0.3.0
一定小心,有可能踩到這個(gè)雷。
這個(gè)其中的一個(gè)大 bug。
其實(shí)我們對(duì) slack 的有無(wú)不是那么關(guān)心的,那么我們使用ratelimit.WithoutSlack
這個(gè)選項(xiàng),把 slack 設(shè)置為 0,是不是就沒(méi)問(wèn)題了呢?
嗯,是的,不會(huì)再出現(xiàn)上面的 bug,而且在我的 mac 筆記本上跑的單元測(cè)試也每問(wèn)題,但是!但是!但是!又出現(xiàn)了另外一個(gè) bug。
我們把限流的速率修改為5000
,結(jié)果在 Linux 測(cè)試機(jī)器上跑只能跑到接近2000
,遠(yuǎn)遠(yuǎn)小于預(yù)期,那這還咋限流,流根本打不上去。
我的同事說(shuō)把ratelimit
版本降到v0.2.0
,同時(shí)不要設(shè)置slack=0
可以解決這個(gè)問(wèn)題。
這就很奇怪了,經(jīng)過(guò)一番排查,發(fā)現(xiàn)問(wèn)題可能出在 Go 標(biāo)準(zhǔn)庫(kù)的time.Sleep
上。
我們使用time.Sleep
休眠 50 微秒的話,在 Go 1.16 之前,Linux 機(jī)器上基本上實(shí)際會(huì)休眠 80、90 微秒,但是在 Go 1.16 之后,Linux 機(jī)器上 1 毫秒,差距巨大,在 Windows 機(jī)器上,Go 1.16 之前是 1 毫秒,之后是 14 毫秒,差距也是巨大的。我在蘋果的 MacPro M1 的機(jī)器測(cè)試,就沒(méi)有這個(gè)問(wèn)題。
這個(gè) bug 記錄在issues#44343[4], 自 2021 年 2 月提出來(lái)來(lái),已經(jīng)快三年了,這個(gè) bug 還一直沒(méi)有關(guān)閉,問(wèn)題還一直存在著,看樣子這個(gè) bug 也不是那么容易找到根因和徹底解決。
所以如果你要使用time.Sleep
,請(qǐng)記得在 Linux 環(huán)境下,它的精度也就在1ms左右。所以ratelimit
庫(kù)如果依賴它做 5000 的限流,如果不好好設(shè)計(jì)的話,達(dá)不到限流的效果。
總結(jié)一下
如果你使用uber-go/ratelimit[5],一定記得:
使用較老的版本
v0.2.0
不要設(shè)置
slack=0
, 默認(rèn)或者設(shè)置一個(gè)非零的值
其實(shí)我從juju/ratelimit
切換到uber-go/ratelimit
還有一個(gè)根本的原因。juju/ratelimit
是基于令牌桶的限流,而uber-go/ratelimit
基于漏桶的限流,或者說(shuō)uber-go/ratelimit
更像是整形(shaping),更符合我們使用的場(chǎng)景,我們想勻速的發(fā)送數(shù)據(jù)包,不希望有 Burst 或者突然的速率變化,我們的場(chǎng)景更看中的是勻速。
當(dāng)然你也可以使用juju/ratelimit[6],這是 Canonical 公司貢獻(xiàn)的一個(gè)限流庫(kù),版權(quán)是 LGPL 3.0 + 對(duì) Go 更合適的條款,這也是 Canonical 公司統(tǒng)一對(duì)它們的 Go 項(xiàng)目的授權(quán)。它是一個(gè)基于令牌的限流庫(kù),其實(shí)用起來(lái)也可以,不過(guò)已經(jīng) 4 年沒(méi)有代碼更新了。有一點(diǎn)我覺(jué)得不太爽的地方是它初始化就把桶填滿了,導(dǎo)致的結(jié)果就是可能一開始使用這個(gè)桶獲取令牌的速度超出你的預(yù)期,有可能導(dǎo)致一開始就發(fā)包速度很快,然后慢慢的才勻速,這個(gè)不是我想要的效果,但是我又每辦法修改,所以我 fork 了這個(gè)項(xiàng)目smallnest/ratelimit[7],可以在初始化限流器的時(shí)候,可以設(shè)置初始的令牌,比如將初始的令牌設(shè)置為零。
當(dāng)前 Go 官方也提供了一個(gè)擴(kuò)展庫(kù)golang.org/x/time/rate[8], 功能更強(qiáng)大,強(qiáng)大帶來(lái)的負(fù)面效果就是使用起來(lái)比較復(fù)雜,復(fù)雜帶來(lái)的效果就是可能帶來(lái)一些的潛在的錯(cuò)誤,不過(guò)在認(rèn)真評(píng)估和測(cè)試后也是可以使用的。
參考資料
[1]
uber-go/ratelimit: https://github.com/uber-go/ratelimit
[2]
juju/ratelimit: https://github.com/juju/ratelimit
[3]
uber-go/ratelimit: https://github.com/uber-go/ratelimithttps://github.com/uber-go/ratelimit
[4]
issues#44343: https://github.com/golang/go/issues/44343
[5]
uber-go/ratelimit: https://github.com/uber-go/ratelimit
[6]
juju/ratelimit: https://github.com/juju/ratelimit
[7]
smallnest/ratelimit: https://github.com/smallnest/ratelimit
[8]
golang.org/x/time/rate: https://pkg.go.dev/golang.org/x/time/rate
還有一些關(guān)注度不是那么高的第三庫(kù),還包括一些使用滑動(dòng)窗口實(shí)現(xiàn)的限流庫(kù),還有分布式的限流庫(kù),如果你想了解更多請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解如何使用Go語(yǔ)言進(jìn)行文件監(jiān)控和通知
在Go語(yǔ)言中,文件監(jiān)控通常涉及到文件系統(tǒng)事件的監(jiān)聽,文件或目錄的狀態(tài)發(fā)生變化(如創(chuàng)建、刪除、修改等)時(shí),你的程序需要得到通知,所以本文給大家介紹了如何使用Go語(yǔ)言進(jìn)行文件監(jiān)控和通知,需要的朋友可以參考下2024-06-06gtoken替換jwt實(shí)現(xiàn)sso登錄的問(wèn)題小結(jié)
這篇文章主要介紹了gtoken替換jwt實(shí)現(xiàn)sso登錄,主要介紹了替換jwt的原因分析及gtoken的優(yōu)勢(shì),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05深入解析Go語(yǔ)言中上下文超時(shí)與子進(jìn)程管理
這篇文章小編將通過(guò)一個(gè)實(shí)際問(wèn)題的案例,和大家深入探討一下Go語(yǔ)言中的上下文超時(shí)和子進(jìn)程管理,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-10-10Golang基礎(chǔ)教程之字符串string實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Golang基礎(chǔ)教程之字符串string的相關(guān)資料,需要的朋友可以參考下2022-07-07