這篇文章不是講令牌桶算法原理,關(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)同步