使用golang實(shí)現(xiàn)令牌桶限流和時(shí)間窗口控制

這篇文章不是講令牌桶算法原理,關(guān)于原理,請(qǐng)參考 https://blog.csdn.net/lzw_2006/article/details/51768935

我這里只是使用golang語(yǔ)言來(lái)實(shí)現(xiàn)令牌桶算法,以及時(shí)間窗口限流。

針對(duì)接口進(jìn)行并發(fā)控制

如果擔(dān)心接口某個(gè)時(shí)刻并發(fā)量過(guò)大了,可以細(xì)粒度地限制每個(gè)接口的 總并發(fā)/請(qǐng)求數(shù)

以下代碼golang實(shí)現(xiàn)

package main

import (
    "fmt"
    "net"
    "os"
    "sync/atomic"
    "time"
)

var (
   limiting int32 = 1 // 這就是我的令牌桶
)

func main() {
    tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9090") //獲取一個(gè)tcpAddr
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr) //監(jiān)聽(tīng)一個(gè)端口
    checkError(err)
    defer listener.Close()
    for {
        conn, err := listener.Accept() // 在此處阻塞,每次來(lái)一個(gè)請(qǐng)求才往下運(yùn)行handle函數(shù)
        if err != nil {
            fmt.Println(err)
            continue
        }
        go handle(&conn) // 起一個(gè)單獨(dú)的協(xié)程處理,有多少個(gè)請(qǐng)求,就起多少個(gè)協(xié)程,協(xié)程之間共享同一個(gè)全局變量limiting,對(duì)其進(jìn)行原子操作。
    }
}

func handle(conn *net.Conn) {
    defer (*conn).Close()
    n := atomic.AddInt32(&limiting, -1) // dcr 1 by atomic,獲取一個(gè)令牌,總數(shù)減1。這是一個(gè)原子性的操作,并發(fā)情況下,數(shù)據(jù)不會(huì)寫錯(cuò)。
    if n < 0 {
        // 令牌不夠用了,限流,拋棄此次請(qǐng)求。
        (*conn).Write([]byte("HTTP/1.1 404 NOT FOUND\r\n\r\nError, too many request, please try again."))
    } else {
        // 還有剩余令牌可用
        time.Sleep(1 * time.Second) // 假設(shè)我們的應(yīng)用處理業(yè)務(wù)用了1s的時(shí)間
        (*conn).Write([]byte("HTTP/1.1 200 OK\r\n\r\nI can change the world!")) // 業(yè)務(wù)處理結(jié)束后,回復(fù)200成功。
    }
    atomic.AddInt32(&limiting, 1) // add 1 by atomic,業(yè)務(wù)處理完畢,放回令牌
}

// 異常報(bào)錯(cuò)的處理
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

limiting這個(gè)變量就是我用來(lái)限流的,把它看做令牌桶的池子吧。初始池中只有1個(gè)令牌,每一條處理請(qǐng)求,sleep了1秒??纯床l(fā)的效果。在一個(gè)終端中啟動(dòng)

go run example1.go

另外起一個(gè)終端,用golang的boom來(lái)做壓測(cè)。要提前安裝boom工具

go get github.com/rakyll/hey
go install github.com/rakyll/hey

然后壓測(cè)

$ hey -c 10 -n 50 http://localhost:9090
Summary:
  Total:    5.0246 secs
  Slowest:  1.0066 secs
  Fastest:  0.0008 secs
  Average:  0.1023 secs
  Requests/sec: 9.9510


Response time histogram:
  0.001 [1] |■
  0.101 [44]    |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.202 [0] |
  0.303 [0] |
  0.403 [0] |
  0.504 [0] |
  0.604 [0] |
  0.705 [0] |
  0.805 [0] |
  0.906 [0] |
  1.007 [5] |■■■■■


Latency distribution:
  10% in 0.0011 secs
  25% in 0.0013 secs
  50% in 0.0014 secs
  75% in 0.0044 secs
  90% in 1.0021 secs
  95% in 1.0061 secs
  0% in 0.0000 secs

Details (average, fastest, slowest):
  DNS+dialup:   0.0016 secs, 0.0008 secs, 1.0066 secs
  DNS-lookup:   0.0010 secs, 0.0003 secs, 0.0022 secs
  req write:    0.0002 secs, 0.0000 secs, 0.0008 secs
  resp wait:    0.1022 secs, 0.0000 secs, 1.0050 secs
  resp read:    0.0001 secs, 0.0000 secs, 0.0002 secs

Status code distribution:
  [200] 5 responses
  [404] 45 responses

hey命令-c表示并發(fā)數(shù),我設(shè)為10,-n表示總共發(fā)送多少條,我發(fā)50條。

結(jié)果是只有5條返回http成功的狀態(tài)碼200,其他45條都失敗了。這說(shuō)明有得線程能競(jìng)爭(zhēng)資源成功,有的線程競(jìng)爭(zhēng)資源失敗,這里只有5個(gè)競(jìng)爭(zhēng)成功的??偣灿脮r(shí)也就5.0246秒,平均速率1r/s。這種結(jié)果這和代碼中令牌池只有1個(gè)令牌,而每個(gè)請(qǐng)求要花1s的時(shí)間的要求相吻合。說(shuō)明我們現(xiàn)在將請(qǐng)求限流在1r/s,超過(guò)這個(gè)速度涌進(jìn)來(lái)的請(qǐng)求都會(huì)被拋棄404。

注意:這里使用的是golang的協(xié)程,和線程還是有區(qū)別的,不過(guò)在這里不影響我們做測(cè)試,只要把它理解為并發(fā)就行了,協(xié)程的原理可以去搜下看看。

修改一下結(jié)果,把limiting改成10,再測(cè)試

......
Status code distribution:
  [200] 50 responses

這回是恰到好處啊,剛好滿足10r/s的QPS,所有的請(qǐng)求都成功了。

當(dāng)然,這種并發(fā)控制方式簡(jiǎn)單粗暴,沒(méi)有平滑處理,慎用。

針對(duì)時(shí)間窗口進(jìn)行并發(fā)控制

如果某個(gè)基礎(chǔ)服務(wù)調(diào)用量很大,我們害怕它被突然的大流量打掛,所以需要限制一個(gè)窗口期內(nèi)接口的請(qǐng)求量。下面是一種實(shí)現(xiàn)窗口時(shí)間并發(fā)控制的方法

我們使用緩存來(lái)存儲(chǔ)計(jì)數(shù)器,秒數(shù)作為Key,Value代表這一秒有多少個(gè)請(qǐng)求。這樣就限制了一秒內(nèi)的并發(fā)數(shù),過(guò)期時(shí)間設(shè)置長(zhǎng)一些,比如兩秒,保證一秒內(nèi)的數(shù)據(jù)是存在的。

package main

import (
    "fmt"
    "net"
    "os"
    "time"
    cache "github.com/UncleBig/goCache"
)

var (
    limit int = 10
    c *cache.Cache
)

func main() {
    c = cache.New(10*time.Minute, 30*time.Second)
    tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9090") //獲取一個(gè)tcpAddr
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr) //監(jiān)聽(tīng)一個(gè)端口
    checkError(err)
    defer listener.Close()
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        go handle(&conn)
    }
}

func handle(conn *net.Conn) {
    defer (*conn).Close()
    t := time.Now().Unix()
    key := fmt.Sprintf("%d", t)
    if n, found := c.Get(key); found {
        num := n.(int)
        fmt.Printf("key:%d num:%d\n", t, num)
        if num >= limit {
            (*conn).Write([]byte("HTTP/1.1 404 NOT FOUND\r\n\r\nError, too many request, please try again."))
        } else {
            (*conn).Write([]byte("HTTP/1.1 200 OK\r\n\r\nI can change the world!"))
            c.Increment(key, 1)
        }
    } else {
        (*conn).Write([]byte("HTTP/1.1 200 OK\r\n\r\nI can change the world!"))
        c.Set(key, 1, 2 * time.Second)
    }
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

這段代碼用了緩存,所以要先下載庫(kù)

go get -u github.com/UncleBig/goCache

同樣的方式啟動(dòng)測(cè)試,先來(lái)個(gè)小測(cè)試,服務(wù)端打印日志

[root@VM_195_216_centos ~]# go run example2.go
key:1510229724 num:1 success
key:1510229724 num:2 success
key:1510229724 num:3 success
key:1510229724 num:4 success
key:1510229724 num:5 success
key:1510229724 num:6 success
key:1510229724 num:7 success
key:1510229724 num:8 success
key:1510229724 num:9 success
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed
key:1510229724 num:10 failed

再看看我們測(cè)試用的命令

$ hey -c 10 -n 30 http://localhost:9090
......
Status code distribution:
  [200] 10 responses
  [404] 20 responses

結(jié)果是10條成功20條失敗。看服務(wù)端 的日志發(fā)現(xiàn),所有的日志都是打印的同一秒(1510229724)內(nèi)的請(qǐng)求。當(dāng)累計(jì)處理完10條限流要求的請(qǐng)求之后(num從1打印到10),再往后在這一秒內(nèi)的請(qǐng)求都直接返回失敗了,在這一秒內(nèi)的限流取得了成功。

接下來(lái)再看看,大量持續(xù)請(qǐng)求的情況下,限流效果。

[root@VM_195_216_centos ~]# go run example2.go 
key:1510229933 num:1 success
key:1510229933 num:2 success
key:1510229933 num:3 success
key:1510229933 num:4 success
key:1510229933 num:5 success
key:1510229933 num:6 success
key:1510229933 num:7 success
key:1510229933 num:8 success
key:1510229933 num:9 success
key:1510229933 num:10 failed
key:1510229933 num:10 failed
......
key:1510229933 num:10 failed
key:1510229933 num:10 failed
key:1510229934 num:1 success
key:1510229934 num:2 success
key:1510229934 num:3 success
key:1510229934 num:4 success
key:1510229934 num:5 success
key:1510229934 num:6 success
key:1510229934 num:7 success
key:1510229934 num:8 success
key:1510229934 num:9 success
key:1510229934 num:10 failed
key:1510229934 num:10 failed
......
key:1510229934 num:10 failed
key:1510229934 num:10 failed
key:1510229935 num:1 success
key:1510229935 num:2 success
key:1510229935 num:3 success
key:1510229935 num:4 success
key:1510229935 num:5 success
key:1510229935 num:6 success
key:1510229935 num:7 success
key:1510229935 num:8 success
key:1510229935 num:9 success
key:1510229935 num:10 failed
key:1510229935 num:10 failed
......
key:1510229935 num:10 failed
key:1510229935 num:10 failed
key:1510229936 num:1 success
key:1510229936 num:2 success
key:1510229936 num:3 success
key:1510229936 num:4 success
key:1510229936 num:5 success
key:1510229936 num:6 success
key:1510229936 num:7 success
key:1510229936 num:8 success
key:1510229936 num:9 success
key:1510229936 num:10 failed
key:1510229936 num:10 failed
......

測(cè)試命令

$ hey -c 10 -n 10000 http://localhost:9090
Summary:
  Total:        2.9792 secs
......
Status code distribution:
  [200] 40 responses
  [404] 9937 responses

這次總共花了近3秒時(shí)間,發(fā)了1w條請(qǐng)求,由于日志打印太多了,截取部分有代表性的??梢钥吹浇?jīng)歷了3秒,每1秒內(nèi)都只成功10條,接下來(lái)到下一秒之前的請(qǐng)求都是失敗的。3秒總共成功了40條,按理說(shuō)應(yīng)該30條,可能邊界值那幾毫秒控制的不是很精準(zhǔn),這個(gè)誤差可以容忍,還是能達(dá)到限流的理想效果。


創(chuàng)建于 2018-09-08 北京,更新于 2019-05-23 北京

該文章在以下平臺(tái)同步

最后編輯于
?著作權(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ù)。

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