golang rate令牌桶源碼分析實(shí)現(xiàn)

大家好,我是dandyhuang。高并發(fā)三板斧:限流、緩存、降級(jí)。 限流其實(shí)就是:當(dāng)高并發(fā)或者瞬時(shí)高并發(fā)時(shí),為了保證系統(tǒng)的穩(wěn)定性、可用性,系統(tǒng)以犧牲部分請(qǐng)求為代價(jià)或者延遲處理請(qǐng)求為代價(jià),保證系統(tǒng)整體服務(wù)可用。今天就給大家介紹golang 官方擴(kuò)展包 time(golang.org/x/time/rate) 中,一個(gè)基于令牌桶的限流器實(shí)現(xiàn)。它的實(shí)現(xiàn)java guava rate limiter中的實(shí)現(xiàn)思路是一樣的。按照該思路我也用c++實(shí)現(xiàn)了一份代碼,可以供大家參考代碼token_limiter.cpp實(shí)現(xiàn)。

令牌桶

令牌桶.png

令牌桶算法的原理:系統(tǒng)會(huì)以一個(gè)恒定的速度往桶里放入令牌,而如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒(méi)有令牌可取時(shí),則拒絕服務(wù)。自然我們可能會(huì)想到啟一個(gè)協(xié)程,定時(shí)的往桶里丟入令牌。但golang中沒(méi)有按照這個(gè)思路去實(shí)現(xiàn)。而是實(shí)時(shí)計(jì)算當(dāng)前產(chǎn)生的令牌數(shù)??赡軙?huì)有人覺(jué)得,把定時(shí)的時(shí)間片調(diào)低一些,比如1us就去計(jì)算產(chǎn)生的令牌數(shù)。這種效果也和實(shí)時(shí)計(jì)算效果也差不多。但這種處理,隨著精度越高,cpu親緣性的問(wèn)題就越嚴(yán)重。我們來(lái)看看golang是怎么實(shí)現(xiàn)的。思路其實(shí)也是比較巧妙。

time/rate實(shí)現(xiàn)

package limiter

import (
 "fmt"
 "testing"
 "time"

 "golang.org/x/time/rate"
)

func TestLimter(t *testing.T) {
    limiter := rate.NewLimiter(rate.Every(time.Millisecond*10), 2)
    for i := 0; i < 10; i++ {
        var ok bool
        if limiter.Allow() {
            ok = true
        }
        time.Sleep(time.Millisecond * 3)
        fmt.Println(ok, limiter.Burst())
    }
}

執(zhí)行結(jié)果:

=== RUN   TestLimter
true 2
true 2
false 2
true 2
false 2
false 2
true 2
false 2
false 2
true 2
--- PASS: TestLimter1 (0.03s)

因?yàn)槌跏蓟?個(gè)令牌在里頭,隨著后續(xù)執(zhí)行,每10ms產(chǎn)生一個(gè)令牌。所以后續(xù)10ms內(nèi),只有一個(gè)請(qǐng)求能獲取到令牌。

創(chuàng)建限流器

func NewLimiter(r Limit, b int) *Limiter {
    return &Limiter{
        limit: r,
        burst: b,
    }
}

type Limiter struct {
    mu     sync.Mutex
    limit  Limit
    burst  int
    tokens float64
    // last is the last time the limiter's tokens field was updated
    last time.Time
    // lastEvent is the latest time of a rate-limited event (past or future)
    lastEvent time.Time
}

Limiter參數(shù)中

  • mu:每次請(qǐng)求并發(fā)安全,加鎖處理

  • burst : 桶內(nèi)能存放令牌的個(gè)數(shù)

  • limit:生成令牌的速率

  • tokens: 剩余令牌個(gè)數(shù)

  • last: 上一次取走 token 的時(shí)間

  • lastEvent:上一次限流事件的時(shí)間

Allow 判斷是否運(yùn)行通過(guò)

// 判斷是否滿足條件
func (lim *Limiter) Allow() bool {
    return lim.AllowN(time.Now(), 1)
}

func (lim *Limiter) AllowN(now time.Time, n int) bool {
    return lim.reserveN(now, n, 0).ok
}

func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
  // 加鎖防止并發(fā)
    lim.mu.Lock()
    defer lim.mu.Unlock()
    // 邊界處理,都是一些異常設(shè)置,設(shè)置最大值,或沒(méi)有設(shè)置的場(chǎng)景
    if lim.limit == Inf {
        return Reservation{
            ok:        true,
            lim:       lim,
            tokens:    n,
            timeToAct: now,
        }
    } else if lim.limit == 0 {
        var ok bool
        if lim.burst >= n {
            ok = true
            lim.burst -= n
        }
        return Reservation{
            ok:        ok,
            lim:       lim,
            tokens:    lim.burst,
            timeToAct: now,
        }
    }
     // 從上次獲取令牌到當(dāng)前時(shí)間,共產(chǎn)生的令牌數(shù)tokens
    now, last, tokens := lim.advance(now)

    // 減去需要的令牌數(shù)據(jù)
    tokens -= float64(n)

    var waitDuration time.Duration
  // 如果需要的令牌數(shù)tokens 為負(fù)數(shù),則計(jì)算需要等待的 WaitDuration時(shí)間
    if tokens < 0 {
        waitDuration = lim.limit.durationFromTokens(-tokens)
    }

    // 獲取需要的令牌數(shù)如果大于桶的容量  或者 等待時(shí)間大于設(shè)置的maxFutureReserve
    ok := n <= lim.burst && waitDuration <= maxFutureReserve

    // 構(gòu)造一個(gè)Reservation對(duì)象,后續(xù)結(jié)果返回該對(duì)象
    r := Reservation{
        ok:    ok,
        lim:   lim,
        limit: lim.limit,
    }
  // 成功才更新該對(duì)象
    if ok {
        r.tokens = n
        r.timeToAct = now.Add(waitDuration)
    }

    // 成功了,才把上次時(shí)間和tokens數(shù)更新
    if ok {
        lim.last = now
        lim.tokens = tokens
        lim.lastEvent = r.timeToAct
    } else {
    // 這里因?yàn)殒i并發(fā)的問(wèn)題,所以才回導(dǎo)致last會(huì)有更新
        lim.last = last
    }

    return r
}

reserveN中記錄了上次訪問(wèn)的時(shí)間和當(dāng)前桶中令牌的數(shù)量。當(dāng)再次訪問(wèn)時(shí),通過(guò)上次訪問(wèn)記錄的時(shí)間實(shí)時(shí)計(jì)算當(dāng)前令牌的數(shù)量,決定是否可以放行。

advance計(jì)算產(chǎn)生的令牌數(shù)

func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
    last := lim.last
  // 因?yàn)榧渔i的原因,所以出現(xiàn)這種情況
    if now.Before(last) {
        last = now
    }

    // (當(dāng)前時(shí)間-上次時(shí)間)* 速率 = 產(chǎn)生的令牌數(shù)
    elapsed := now.Sub(last)
    delta := lim.limit.tokensFromDuration(elapsed)
    tokens := lim.tokens + delta
    if burst := float64(lim.burst); tokens > burst {
        tokens = burst
    }
    return now, last, tokens
}

這里AllowN()請(qǐng)求中,我們是在Allow()的時(shí)候調(diào)用lim.AllowN(time.Now(), 1),并把當(dāng)前時(shí)間傳入。這時(shí)候,如果

reserveN處理的比較慢,并且請(qǐng)求成功。如線程t1請(qǐng)求的時(shí)間為10:10:12秒,線程t2請(qǐng)求時(shí)間為10:10:10秒。 而線程t1先搶到了鎖。并處理請(qǐng)求成功。接下去t2才進(jìn)行處理。這時(shí)last為10:10:12秒,now為10:10:10秒的場(chǎng)景

總結(jié)

golang/rate包中,犧牲一點(diǎn)加鎖的性能,實(shí)時(shí)計(jì)算產(chǎn)生的令牌數(shù)。這種實(shí)現(xiàn)的好處: 對(duì)令牌的計(jì)算可以非常精確。而對(duì)比于定時(shí)往桶里添加令牌的實(shí)現(xiàn),雖然在請(qǐng)求可以使用原子計(jì)算,不上鎖實(shí)現(xiàn)。但對(duì)于令牌的計(jì)算來(lái)說(shuō),是比較不準(zhǔn)確的,需要根據(jù)定時(shí)器的精度來(lái)保證。而精度越小,cpu親緣性問(wèn)題就越明顯。個(gè)人覺(jué)得雖然加鎖的實(shí)現(xiàn),對(duì)性能有一部分影響,但是令牌桶都是在計(jì)算,所以性能不會(huì)有很大的問(wèn)題,加鎖時(shí)間不長(zhǎng)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容