在電商高并發(fā)場(chǎng)景下,我們經(jīng)常會(huì)使用一些常用方法,去應(yīng)對(duì)流量高峰,比如限流、熔斷、降級(jí),今天我們聊聊限流。 什么是限流呢?限流是限制到達(dá)系統(tǒng)的并發(fā)請(qǐng)求數(shù)量,保證系統(tǒng)能夠正常響應(yīng)部分用戶請(qǐng)求,而對(duì)于超過限制的流量,則通過拒絕服務(wù)的方式保證整體系統(tǒng)的可用性。 根據(jù)限流作用范圍,可以分為單機(jī)限流和分布式限流;根據(jù)限流方式,又分為計(jì)數(shù)器、滑動(dòng)窗口、漏桶限令牌桶限流,下面我們對(duì)這塊詳細(xì)進(jìn)行講解。 常用限流方式計(jì)數(shù)器計(jì)數(shù)器是一種最簡(jiǎn)單限流算法,其原理就是:在一段時(shí)間間隔內(nèi),對(duì)請(qǐng)求進(jìn)行計(jì)數(shù),與閥值進(jìn)行比較判斷是否需要限流,一旦到了時(shí)間臨界點(diǎn),將計(jì)數(shù)器清零。 這個(gè)就像你去坐車一樣,車廂規(guī)定了多少個(gè)位置,滿了就不讓上車了,不然就是超載了,被交警叔叔抓到了就要罰款的,如果我們的系統(tǒng)那就不是罰款的事情了,可能直接崩掉了。 程序執(zhí)行邏輯:
那么問題來了,如果有個(gè)需求對(duì)于某個(gè)接口 /query 每分鐘最多允許訪問 200 次,假設(shè)有個(gè)用戶在第 59 秒的最后幾毫秒瞬間發(fā)送 200 個(gè)請(qǐng)求,當(dāng) 59 秒結(jié)束后 Counter 清零了,他在下一秒的時(shí)候又發(fā)送 200 個(gè)請(qǐng)求。 那么在 1 秒鐘內(nèi)這個(gè)用戶發(fā)送了 2 倍的請(qǐng)求,這個(gè)是符合我們的設(shè)計(jì)邏輯的,這也是計(jì)數(shù)器方法的設(shè)計(jì)缺陷,系統(tǒng)可能會(huì)承受惡意用戶的大量請(qǐng)求,甚至擊穿系統(tǒng)。這種方法雖然簡(jiǎn)單,但也有個(gè)大問題就是沒有很好的處理單位時(shí)間的邊界。 不過說實(shí)話,這個(gè)計(jì)數(shù)引用了鎖,在高并發(fā)場(chǎng)景,這個(gè)方式可能不太實(shí)用,我建議將鎖去掉,然后將 l.count 的邏輯通過原子計(jì)數(shù)處理,這樣就可以保證 l.count 自增時(shí)不會(huì)被多個(gè)線程同時(shí)執(zhí)行,即通過原子計(jì)數(shù)的方式實(shí)現(xiàn)限流。
滑動(dòng)窗口滑動(dòng)窗口是針對(duì)計(jì)數(shù)器存在的臨界點(diǎn)缺陷,所謂滑動(dòng)窗口(Sliding window)是一種流量控制技術(shù),這個(gè)詞出現(xiàn)在 TCP 協(xié)議中?;瑒?dòng)窗口把固定時(shí)間片進(jìn)行劃分,并且隨著時(shí)間的流逝,進(jìn)行移動(dòng),固定數(shù)量的可以移動(dòng)的格子,進(jìn)行計(jì)數(shù)并判斷閥值。 上圖中我們用紅色的虛線代表一個(gè)時(shí)間窗口(一分鐘),每個(gè)時(shí)間窗口有 6 個(gè)格子,每個(gè)格子是 10 秒鐘。每過 10 秒鐘時(shí)間窗口向右移動(dòng)一格,可以看紅色箭頭的方向。我們?yōu)槊總€(gè)格子都設(shè)置一個(gè)獨(dú)立的計(jì)數(shù)器 Counter,假如一個(gè)請(qǐng)求在 0:45 訪問了那么我們將第五個(gè)格子的計(jì)數(shù)器 1(也是就是 0:40~0:50),在判斷限流的時(shí)候需要把所有格子的計(jì)數(shù)加起來和設(shè)定的頻次進(jìn)行比較即可。 那么滑動(dòng)窗口如何解決我們上面遇到的問題呢?來看下面的圖: 當(dāng)用戶在 0:59 秒鐘發(fā)送了 200 個(gè)請(qǐng)求就會(huì)被第六個(gè)格子的計(jì)數(shù)器記錄 200,當(dāng)下一秒的時(shí)候時(shí)間窗口向右移動(dòng)了一個(gè),此時(shí)計(jì)數(shù)器已經(jīng)記錄了該用戶發(fā)送的 200 個(gè)請(qǐng)求,所以再發(fā)送的話就會(huì)觸發(fā)限流,則拒絕新的請(qǐng)求。 其實(shí)計(jì)數(shù)器就是滑動(dòng)窗口啊,只不過只有一個(gè)格子而已,所以想讓限流做的更精確只需要?jiǎng)澐指嗟母褡泳涂梢粤?,為了更精確我們也不知道到底該設(shè)置多少個(gè)格子,格子的數(shù)量影響著滑動(dòng)窗口算法的精度,依然有時(shí)間片的概念,無法根本解決臨界點(diǎn)問題。
漏桶漏桶算法(Leaky Bucket),原理就是一個(gè)固定容量的漏桶,按照固定速率流出水滴。 用過水龍頭都知道,打開龍頭開關(guān)水就會(huì)流下滴到水桶里,而漏桶指的是水桶下面有個(gè)漏洞可以出水,如果水龍頭開的特別大那么水流速就會(huì)過大,這樣就可能導(dǎo)致水桶的水滿了然后溢出。
一個(gè)固定容量的桶,有水流進(jìn)來,也有水流出去。對(duì)于流進(jìn)來的水來說,我們無法預(yù)計(jì)一共有多少水會(huì)流進(jìn)來,也無法預(yù)計(jì)水流的速度。但是對(duì)于流出去的水來說,這個(gè)桶可以固定水流出的速率(處理速度),從而達(dá)到流量整形和流量控制的效果。 漏桶算法有以下特點(diǎn):
漏桶限制的是常量流出速率(即流出速率是一個(gè)固定常量值),所以最大的速率就是出水的速率,不能出現(xiàn)突發(fā)流量。
令牌桶令牌桶算法(Token Bucket)是網(wǎng)絡(luò)流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發(fā)送到網(wǎng)絡(luò)上的數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送。
我們有一個(gè)固定的桶,桶里存放著令牌(token)。一開始桶是空的,系統(tǒng)按固定的時(shí)間(rate)往桶里添加令牌,直到桶里的令牌數(shù)滿,多余的請(qǐng)求會(huì)被丟棄。當(dāng)請(qǐng)求來的時(shí)候,從桶里移除一個(gè)令牌,如果桶是空的則拒絕請(qǐng)求或者阻塞。 令牌桶有以下特點(diǎn):
令牌桶限制的是平均流入速率(允許突發(fā)請(qǐng)求,只要有令牌就可以處理,支持一次拿3個(gè)令牌,4個(gè)令牌...),并允許一定程度突發(fā)流量,所以也是非常常用的限流算法。
Redis Lua 分布式限流單機(jī)版限流僅能保護(hù)自身節(jié)點(diǎn),但無法保護(hù)應(yīng)用依賴的各種服務(wù),并且在進(jìn)行節(jié)點(diǎn)擴(kuò)容、縮容時(shí)也無法準(zhǔn)確控制整個(gè)服務(wù)的請(qǐng)求限制。 而分布式限流,以集群為維度,可以方便的控制這個(gè)集群的請(qǐng)求限制,從而保護(hù)下游依賴的各種服務(wù)資源。 分布式限流最關(guān)鍵的是要將限流服務(wù)做成原子化,我們可以借助 Redis 的計(jì)數(shù)器,Lua 執(zhí)行的原子性,進(jìn)行分布式限流,大致的 Lua 腳本代碼如下: local key = 'rate.limit:' .. KEYS[1] --限流KEYlocal limit = tonumber(ARGV[1]) --限流大小local current = tonumber(redis.call('get', key) or '0')if current 1 > limit then --如果超出限流大小 return 0else --請(qǐng)求數(shù) 1,并設(shè)置1秒過期 redis.call('INCRBY', key,'1') redis.call('expire', key,'1') return current 1end復(fù)制代碼 限流邏輯(Java 語言):
聊聊其它上面的限流方式,主要是針對(duì)服務(wù)器進(jìn)行限流,我們也可以對(duì)容器進(jìn)行限流,比如 Tomcat、Nginx 等限流手段。 Tomcat 可以設(shè)置最大線程數(shù)(maxThreads),當(dāng)并發(fā)超過最大線程數(shù)會(huì)排隊(duì)等待執(zhí)行;而 Nginx 提供了兩種限流手段:一是控制速率,二是控制并發(fā)連接數(shù)。 對(duì)于 Java 語言,我們其實(shí)有相關(guān)的限流組件,比如大家常用的 RateLimiter,其實(shí)就是基于令牌桶算法,大家知道為什么唯獨(dú)選用令牌桶么? 對(duì)于 Go 語言,也有該語言特定的限流方式,比如可以通過 channel 實(shí)現(xiàn)并發(fā)控制限流,也支持第三方庫(kù) httpserver 實(shí)現(xiàn)限流,詳見這篇 《Go 限流的常見方法》。 在實(shí)際的限流場(chǎng)景中,我們也可以控制單個(gè) IP、城市、渠道、設(shè)備 id、用戶 id 等在一定時(shí)間內(nèi)發(fā)送的請(qǐng)求數(shù);如果是開放平臺(tái),需要為每個(gè) appkey 設(shè)置獨(dú)立的訪問速率規(guī)則。 限流對(duì)比下面我們就對(duì)常用的線程策略,總結(jié)它們的優(yōu)缺點(diǎn),便于以后選型。 計(jì)數(shù)器:
滑動(dòng)窗口:
漏桶:
令牌桶:
Redis Lua 分布式限流:
|
|