溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux高并發(fā)系統(tǒng)之限流技術(shù)

發(fā)布時間:2020-07-30 05:07:46 來源:網(wǎng)絡 閱讀:775 作者:我不是九爺 欄目:云計算

?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??

在開發(fā)高并發(fā)系統(tǒng)時有三把利器用來保護系統(tǒng):緩存、降級和限流。緩存的目的是提升系統(tǒng)訪問速度和增大系統(tǒng)能處理的容量,可謂是抗高并發(fā)流量的銀彈;而降級是當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉,待高峰或者問題解決后再打開;而有些場景并不能用緩存和降級來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的復雜查詢(評論的最后幾頁),因此需有一種手段來限制這些場景的并發(fā)/請求量,即限流。

?

限流的目的是通過對并發(fā)訪問/請求進行限速或者一個時間窗口內(nèi)的的請求進行限速來保護系統(tǒng),一旦達到限制速率則可以拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊或等待(比如秒殺、評論、下單)、降級(返回兜底數(shù)據(jù)或默認數(shù)據(jù),如商品詳情頁庫存默認有貨)。

?

一般開發(fā)高并發(fā)系統(tǒng)常見的限流有:限制總并發(fā)數(shù)(比如數(shù)據(jù)庫連接池、線程池)、限制瞬時并發(fā)數(shù)(如nginx的limit_conn模塊,用來限制瞬時并發(fā)連接數(shù))、限制時間窗口內(nèi)的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率);其他還有如限制遠程接口調(diào)用速率、限制MQ的消費速率。另外還可以根據(jù)網(wǎng)絡連接數(shù)、網(wǎng)絡流量、CPU或內(nèi)存負載等來限流。

?

先有緩存這個銀彈,后有限流來應對618、雙十一高并發(fā)流量,在處理高并發(fā)問題上可以說是如虎添翼,不用擔心瞬間流量導致系統(tǒng)掛掉或雪崩,最終做到有損服務而不是不服務;限流需要評估好,不可亂用,否則會正常流量出現(xiàn)一些奇怪的問題而導致用戶抱怨。

?

在實際應用時也不要太糾結(jié)算法問題,因為一些限流算法實現(xiàn)是一樣的只是描述不一樣;具體使用哪種限流技術(shù)還是要根據(jù)實際場景來選擇,不要一味去找最佳模式,白貓黑貓能解決問題的就是好貓。

?

因在實際工作中遇到過許多人來問如何進行限流,因此本文會詳細介紹各種限流手段。那么接下來我們從限流算法、應用級限流、分布式限流、接入層限流來詳細學習下限流技術(shù)手段。

?

限流算法

常見的限流算法有:令牌桶、漏桶。計數(shù)器也可以進行粗暴限流實現(xiàn)。

?

令牌桶算法

令牌桶算法是一個存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

  • 假設(shè)限制2r/s,則按照500毫秒的固定速率往桶中添加令牌;

  • 桶中最多存放b個令牌,當桶滿時,新添加的令牌被丟棄或拒絕;

  • 當一個n個字節(jié)大小的數(shù)據(jù)包到達,將從桶中刪除n個令牌,接著數(shù)據(jù)包被發(fā)送到網(wǎng)絡上;

  • 如果桶中的令牌不足n個,則不會刪除令牌,且該數(shù)據(jù)包將被限流(要么丟棄,要么緩沖區(qū)等待)。

?


Linux高并發(fā)系統(tǒng)之限流技術(shù)
?

漏桶算法

漏桶作為計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用于流量×××(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:

  • 一個固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,則不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。


Linux高并發(fā)系統(tǒng)之限流技術(shù)
?

令牌桶和漏桶對比:

  • 令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數(shù)減為零時則拒絕新的請求;

  • 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數(shù)累積到漏桶容量時,則新流入的請求被拒絕;

  • 令牌桶限制的是平均流入速率(允許突發(fā)請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌),并允許一定程度突發(fā)流量;

  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發(fā)流入速率;

  • 令牌桶允許一定程度的突發(fā),而漏桶主要目的是平滑流入速率;

  • 兩個算法實現(xiàn)可以一樣,但是方向是相反的,對于相同的參數(shù)得到的限流效果是一樣的。

?

另外有時候我們還使用計數(shù)器來進行限流,主要用來限制總并發(fā)數(shù),比如數(shù)據(jù)庫連接池、線程池、秒殺的并發(fā)數(shù);只要全局總請求數(shù)或者一定時間段的總請求數(shù)設(shè)定的閥值則進行限流,是簡單粗暴的總數(shù)量限流,而不是平均速率限流。

?

到此基本的算法就介紹完了,接下來我們首先看看應用級限流。

?

?

應用級限流

?

限流總并發(fā)/連接/請求數(shù)

對于一個應用系統(tǒng)來說一定會有極限并發(fā)/請求數(shù),即總有一個TPS/QPS閥值,如果超了閥值則系統(tǒng)就會不響應用戶請求或響應的非常慢,因此我們最好進行過載保護,防止大量請求涌入擊垮系統(tǒng)。

如果你使用過Tomcat,其Connector 其中一種配置有如下幾個參數(shù):

acceptCount:如果Tomcat的線程都忙于響應,新來的連接會進入隊列排隊,如果超出排隊大小,則拒絕連接;

maxConnections: 瞬時最大連接數(shù),超出的會排隊等待;

maxThreads:Tomcat能啟動用來處理請求的最大線程數(shù),如果請求處理量一直遠遠大于最大線程數(shù)則可能會僵死。

詳細的配置請參考官方文檔。另外如Mysql(如max_connections)、Redis(如tcp-backlog)都會有類似的限制連接數(shù)的配置。

?

限流總資源數(shù)

如果有的資源是稀缺資源(如數(shù)據(jù)庫連接、線程),而且可能有多個系統(tǒng)都會去使用它,那么需要限制應用;可以使用池化技術(shù)來限制總資源數(shù):連接池、線程池。比如分配給每個應用的數(shù)據(jù)庫連接是100,那么本應用最多可以使用100個資源,超出了可以等待或者拋異常。

?

限流某個接口的總并發(fā)/請求數(shù)

如果接口可能會有突發(fā)訪問情況,但又擔心訪問量太大造成崩潰,如搶購業(yè)務;這個時候就需要限制這個接口的總并發(fā)/請求數(shù)總請求數(shù)了;因為粒度比較細,可以為每個接口都設(shè)置相應的閥值??梢允褂肑ava中的AtomicLong進行限流:

{
(atomic.incrementAndGet()?>?限流數(shù))?{
}
}?{
????atomic.decrementAndGet();
}

適合對業(yè)務無損的服務或者需要過載保護的服務進行限流,如搶購業(yè)務,超出了大小要么讓用戶排隊,要么告訴用戶沒貨了,對用戶來說是可以接受的。而一些開放平臺也會限制用戶調(diào)用某個接口的試用請求量,也可以用這種計數(shù)器方式實現(xiàn)。這種方式也是簡單粗暴的限流,沒有平滑處理,需要根據(jù)實際情況選擇使用;

?

限流某個接口的時間窗請求數(shù)

即一個時間窗口內(nèi)的請求數(shù),如想限制某個接口/服務每秒/每分鐘/每天的請求數(shù)/調(diào)用量。如一些基礎(chǔ)服務會被很多其他系統(tǒng)調(diào)用,比如商品詳情頁服務會調(diào)用基礎(chǔ)商品服務調(diào)用,但是怕因為更新量比較大將基礎(chǔ)服務打掛,這時我們要對每秒/每分鐘的調(diào)用量進行限速;一種實現(xiàn)方式如下所示:

LoadingCache<Long,?AtomicLong>?counter?=
????????CacheBuilder.()
????????????????.expireAfterWrite(,?TimeUnit.)
????????????????.build(CacheLoader<Long,?AtomicLong>()?{
????????????????????AtomicLong?load(Long?seconds)?Exception?{
????????????????????????AtomicLong();
????????????????????}
????????????????});
limit?=?;
()?{
????currentSeconds?=?System.()?/?;
????(counter.get(currentSeconds).incrementAndGet()?>?limit)?{
????????System..println(+?currentSeconds);
????????;
????}
????
}

?我們使用Guava的Cache來存儲計數(shù)器,過期時間設(shè)置為2秒(保證1秒內(nèi)的計數(shù)器是有的),然后我們獲取當前時間戳然后取秒數(shù)來作為KEY進行計數(shù)統(tǒng)計和限流,這種方式也是簡單粗暴,剛才說的場景夠用了。

?

平滑限流某個接口的請求數(shù)

之前的限流方式都不能很好地應對突發(fā)請求,即瞬間請求可能都被允許從而導致一些問題;因此在一些場景中需要對突發(fā)請求進行×××,×××為平均速率請求處理(比如5r/s,則每隔200毫秒處理一個請求,平滑了速率)。這個時候有兩種算法滿足我們的場景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法實現(xiàn),可直接拿來使用。

Guava RateLimiter提供了令牌桶算法實現(xiàn):平滑突發(fā)限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現(xiàn)。

?

SmoothBursty

RateLimiter?limiter?=?RateLimiter.();
System..println(limiter.acquire());
System..println(limiter.acquire());
System..println(limiter.acquire());
System..println(limiter.acquire());
System..println(limiter.acquire());
System..println(limiter.acquire());

?? 將得到類似如下的輸出:

? 0.0

? 0.198239

? 0.196083

? 0.200609

??0.199599

??0.19961

1、RateLimiter.create(5) 表示桶容量為5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;

2、limiter.acquire()表示消費一個令牌,如果當前桶中有足夠令牌則成功(返回值為0),如果桶中沒有令牌則暫停一段時間,比如發(fā)令牌間隔是200毫秒,則等待200毫秒后再去消費令牌(如上測試用例返回的為0.198239,差不多等待了200毫秒桶中才有令牌可用),這種實現(xiàn)將突發(fā)請求速率平均為了固定請求速率。

?

再看一個突發(fā)示例:

RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(5));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));

將得到類似如下的輸出:

0.0

0.98745

0.183553

0.199909

limiter.acquire(5)表示桶的容量為5且每秒新增5個令牌,令牌桶算法允許一定程度的突發(fā),所以可以一次性消費5個令牌,但接下來的limiter.acquire(1)將等待差不多1秒桶中才能有令牌,且接下來的請求也×××為固定速率了。

RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(10));
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(1));

將得到類似如下的輸出:

0.0

1.997428

0.192273

0.200616

同上邊的例子類似,第一秒突發(fā)了10個請求,令牌桶算法也允許了這種突發(fā)(允許消費未來的令牌),但接下來的limiter.acquire(1)將等待差不多2秒桶中才能有令牌,且接下來的請求也×××為固定速率了。


接下來再看一個突發(fā)的例子:

RateLimiter limiter = RateLimiter.create(2);
System.out.println(limiter.acquire());
Thread.sleep(2000L);
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

將得到類似如下的輸出:

0.0

0.0

0.0

0.0

0.499876

0.495799

1、創(chuàng)建了一個桶容量為2且每秒新增2個令牌;

2、首先調(diào)用limiter.acquire()消費一個令牌,此時令牌桶可以滿足(返回值為0);

3、然后線程暫停2秒,接下來的兩個limiter.acquire()都能消費到令牌,第三個limiter.acquire()也同樣消費到了令牌,到第四個時就需要等待500毫秒了。

此處可以看到我們設(shè)置的桶容量為2(即允許的突發(fā)量),這是因為SmoothBursty中有一個參數(shù):最大突發(fā)秒數(shù)(maxBurstSeconds)默認值是1s,突發(fā)量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突發(fā)量為2,例子中前兩個是消費了之前積攢的突發(fā)量,而第三個開始就是正常計算的了。令牌桶算法允許將一段時間內(nèi)沒有消費的令牌暫存到令牌桶中,留待未來使用,并允許未來請求的這種突發(fā)。

?

SmoothBursty通過平均速率和最后一次新增令牌的時間計算出下次新增令牌的時間的,另外需要一個桶暫存一段時間內(nèi)沒有使用的令牌(即可以突發(fā)的令牌數(shù))。另外RateLimiter還提供了tryAcquire方法來進行無阻塞或可超時的令牌消費。

?

因為SmoothBursty允許一定程度的突發(fā),會有人擔心如果允許這種突發(fā),假設(shè)突然間來了很大的流量,那么系統(tǒng)很可能扛不住這種突發(fā)。因此需要一種平滑速率的限流工具,從而系統(tǒng)冷啟動后慢慢的趨于平均固定速率(即剛開始速率小一些,然后慢慢趨于我們設(shè)置的固定速率)。Guava也提供了SmoothWarmingUp來實現(xiàn)這種需求,其可以認為是漏桶算法,但是在某些特殊場景又不太一樣。

?

SmoothWarmingUp創(chuàng)建方式:RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)

permitsPerSecond表示每秒新增的令牌數(shù),warmupPeriod表示在從冷啟動速率過渡到平均速率的時間間隔。

?

示例如下:

RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
for(int i = 1; i < 5;i++) {
? ? System.out.println(limiter.acquire());
}
Thread.sleep(1000L);
for(int i = 1; i < 5;i++) {
? ? System.out.println(limiter.acquire());
}

將得到類似如下的輸出:

0.0

0.51767

0.357814

0.21×××

0.199984

0.0

0.360826

0.220166

0.199723

0.199555

速率是梯形上升速率的,也就是說冷啟動時會以一個比較大的速率慢慢到平均速率;然后趨于平均速率(梯形下降到平均速率)??梢酝ㄟ^調(diào)節(jié)warmupPeriod參數(shù)實現(xiàn)一開始就是平滑固定速率。

?

到此應用級限流的一些方法就介紹完了。假設(shè)將應用部署到多臺機器,應用級限流方式只是單應用內(nèi)的請求限流,不能進行全局限流。因此我們需要分布式限流和接入層限流來解決這個問題。

?

分布式限流

分布式限流最關(guān)鍵的是要將限流服務做成原子化,而解決方案可以使使用redis+lua或者nginx+lua技術(shù)進行實現(xiàn),通過這兩種技術(shù)可以實現(xiàn)的高并發(fā)和高性能。

首先我們來使用redis+lua實現(xiàn)時間窗內(nèi)某個接口的請求數(shù)限流,實現(xiàn)了該功能后可以改造為限流總并發(fā)/請求數(shù)和限制總資源數(shù)。Lua本身就是一種編程語言,也可以使用它實現(xiàn)復雜的令牌桶或漏桶算法。

?

redis+lua實現(xiàn)中的lua腳本:

local key = KEYS[1] --限流KEY(一秒一個)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call("INCRBY", key, "1")) --請求數(shù)+1
if current > limit then --如果超出限流大小
? ? return 0
elseif current == 1 then --只有第一次訪問需要設(shè)置2秒的過期時間
? ? redis.call("expire", key,"2")
end
return 1

如上操作因是在一個lua腳本中,又因Redis是單線程模型,因此是線程安全的。如上方式有一個缺點就是當達到限流大小后還是會遞增的,可以改造成如下方式實現(xiàn):

local key = KEYS[1] --限流KEY(一秒一個)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
? ? return 0
else --請求數(shù)+1,并設(shè)置2秒過期
? ? redis.call("INCRBY", key,"1")
? ? redis.call("expire", key,"2")
? ? return 1
end

如下是Java中判斷是否需要限流的代碼:

public static boolean acquire() throws Exception {
String luaScript = Files.toString(new File("limit.lua"), Charset.defaultCharset());
Jedis jedis = new Jedis("192.168.147.52", 6379);
String key = "ip:" + System.currentTimeMillis()/ 1000; //此處將當前時間戳取秒數(shù)
Stringlimit = "3"; //限流大小
return (Long)jedis.eval(luaScript,Lists.newArrayList(key), Lists.newArrayList(limit)) == 1;
}

因為Redis的限制(Lua中有寫操作不能使用帶隨機性質(zhì)的讀操作,如TIME)不能在Redis Lua中使用TIME獲取時間戳,因此只好從應用獲取然后傳入,在某些極端情況下(機器時鐘不準的情況下),限流會存在一些小問題。

?

使用Nginx+Lua實現(xiàn)的Lua腳本:

local locks = require "resty.lock"
local function acquire()
? ? local lock =locks:new("locks")
? ? local elapsed, err =lock:lock("limit_key") --互斥鎖
? ? local limit_counter =ngx.shared.limit_counter --計數(shù)器
? ? local key = "ip:" ..os.time()
? ? local limit = 5 --限流大小
? ? local current =limit_counter:get(key)

? ? if current ~= nil and current + 1> limit then --如果超出限流大小
? ? ? ? lock:unlock()
? ? ? ? return 0
? ? end
? ? if current == nil then
? ? ? ? limit_counter:set(key, 1, 1) --第一次需要設(shè)置過期時間,設(shè)置key的值為1,過期時間為1秒
? ? else
? ? ? ? limit_counter:incr(key, 1) --第二次開始加1即可
? ? end
? ? lock:unlock()
? ? return 1
end
ngx.print(acquire())

實現(xiàn)中我們需要使用lua-resty-lock互斥鎖模塊來解決原子性問題(在實際工程中使用時請考慮獲取鎖的超時問題),并使用ngx.shared.DICT共享字典來實現(xiàn)計數(shù)器。如果需要限流則返回0,否則返回1。使用時需要先定義兩個共享字典(分別用來存放鎖和計數(shù)器數(shù)據(jù)):

?

Java代碼??Linux高并發(fā)系統(tǒng)之限流技術(shù)

  1. http?{??

  2. ????……??

  3. ????lua_shared_dict?locks?10m;??

  4. ????lua_shared_dict?limit_counter?10m;??

  5. }??

有人會糾結(jié)如果應用并發(fā)量非常大那么redis或者nginx是不是能抗得??;不過這個問題要從多方面考慮:你的流量是不是真的有這么大,是不是可以通過一致性哈希將分布式限流進行分片,是不是可以當并發(fā)量太大降級為應用級限流;對策非常多,可以根據(jù)實際情況調(diào)節(jié);像在京東使用Redis+Lua來限流搶購流量,一般流量是沒有問題的。

?

對于分布式限流目前遇到的場景是業(yè)務上的限流,而不是流量入口的限流;流量入口限流應該在接入層完成,而接入層筆者一般使用Nginx。

?

接入層限流

接入層通常指請求流量的入口,該層的主要目的有:負載均衡、非法請求過濾、請求聚合、緩存、降級、限流、A/B測試、服務質(zhì)量監(jiān)控等等,可以參考筆者寫的《使用Nginx+Lua(OpenResty)開發(fā)高性能Web應用》。

?

對于Nginx接入層限流可以使用Nginx自帶了兩個模塊:連接數(shù)限流模塊ngx_http_limit_conn_module和漏桶算法實現(xiàn)的請求限流模塊ngx_http_limit_req_module。還可以使用OpenResty提供的Lua限流模塊lua-resty-limit-traffic進行更復雜的限流場景。

?

limit_conn用來對某個KEY對應的總的網(wǎng)絡連接數(shù)進行限流,可以按照如IP、域名維度進行限流。limit_req用來對某個KEY對應的請求的平均速率進行限流,并有兩種用法:平滑模式(delay)和允許突發(fā)模式(nodelay)。

?

ngx_http_limit_conn_module

limit_conn是對某個KEY對應的總的網(wǎng)絡連接數(shù)進行限流??梢园凑誌P來限制IP維度的總連接數(shù),或者按照服務域名來限制某個域名的總連接數(shù)。但是記住不是每一個請求連接都會被計數(shù)器統(tǒng)計,只有那些被Nginx處理的且已經(jīng)讀取了整個請求頭的請求連接才會被計數(shù)器統(tǒng)計。

?

配置示例:

http {
? ? limit_conn_zone$binary_remote_addr zone=addr:10m;?
? ? limit_conn_log_level error;?
? ? limit_conn_status 503;
? ? ...
? ? server {
? ? ...
? ? location /limit {
? ? ? ? limit_conn addr 1;
? ? }

limit_conn:要配置存放KEY和計數(shù)器的共享內(nèi)存區(qū)域和指定KEY的最大連接數(shù);此處指定的最大連接數(shù)是1,表示Nginx最多同時并發(fā)處理1個連接;

limit_conn_zone:用來配置限流KEY、及存放KEY對應信息的共享內(nèi)存區(qū)域大?。淮颂幍腒EY是“$binary_remote_addr”其表示IP地址,也可以使用如$server_name作為KEY來限制域名級別的最大連接數(shù);

limit_conn_status:配置被限流后返回的狀態(tài)碼,默認返回503;

limit_conn_log_level:配置記錄被限流后的日志級別,默認error級別。

?

limit_conn的主要執(zhí)行過程如下所示:

1、請求進入后首先判斷當前l(fā)imit_conn_zone中相應KEY的連接數(shù)是否超出了配置的最大連接數(shù);

2.1、如果超過了配置的最大大小,則被限流,返回limit_conn_status定義的錯誤狀態(tài)碼;

2.2、否則相應KEY的連接數(shù)加1,并注冊請求處理完成的回調(diào)函數(shù);

3、進行請求處理;

4、在結(jié)束請求階段會調(diào)用注冊的回調(diào)函數(shù)對相應KEY的連接數(shù)減1。

?

limt_conn可以限流某個KEY的總并發(fā)/請求數(shù),KEY可以根據(jù)需要變化。

?

按照IP限制并發(fā)連接數(shù)配置示例:

首先定義IP維度的限流區(qū)域:

limit_conn_zone $binary_remote_addrzone=perip:10m;

?

接著在要限流的location中添加限流邏輯:

location /limit {
? ? limit_conn perip 2;
? ? echo "123";
}

即允許每個IP最大并發(fā)連接數(shù)為2。

?

使用AB測試工具進行測試,并發(fā)數(shù)為5個,總的請求數(shù)為5個:

ab -n 5 -c 5 http://localhost/limit

??

將得到如下access.log輸出:

[08/Jun/2016:20:10:51+0800] [1465373451.802] 200

[08/Jun/2016:20:10:51+0800] [1465373451.803] 200

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

[08/Jun/2016:20:10:51 +0800][1465373451.803] 503

?

此處我們把access log格式設(shè)置為log_format main? '[$time_local] [$msec] $status';分別是“日期 日期秒/毫秒值 響應狀態(tài)碼”。

?

如果被限流了,則在error.log中會看到類似如下的內(nèi)容:

2016/06/08 20:10:51 [error] 5662#0: *5limiting connections by zone "perip", client: 127.0.0.1, server: _,request: "GET /limit HTTP/1.0", host: "localhost"

?

按照域名限制并發(fā)連接數(shù)配置示例:

首先定義域名維度的限流區(qū)域:

limit_conn_zone $ server_name zone=perserver:10m;

?

接著在要限流的location中添加限流邏輯:

location /limit {
? ? limit_conn perserver 2;
? ? echo "123";
}

即允許每個域名最大并發(fā)請求連接數(shù)為2;這樣配置可以實現(xiàn)服務器最大連接數(shù)限制。

?

ngx_http_limit_req_module

limit_req是漏桶算法實現(xiàn),用于對指定KEY對應的請求進行限流,比如按照IP維度限制請求速率。

?

配置示例:

http {
? ? limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
? ? limit_conn_log_level error;
? ? limit_conn_status 503;
? ? ...
? ? server {
? ? ...
? ? location /limit {
? ? ? ? limit_req zone=one burst=5 nodelay;
? ? }

limit_req:配置限流區(qū)域、桶容量(突發(fā)容量,默認0)、是否延遲模式(默認延遲);

limit_req_zone:配置限流KEY、及存放KEY對應信息的共享內(nèi)存區(qū)域大小、固定請求速率;此處指定的KEY是“$binary_remote_addr”表示IP地址;固定請求速率使用rate參數(shù)配置,支持10r/s和60r/m,即每秒10個請求和每分鐘60個請求,不過最終都會轉(zhuǎn)換為每秒的固定請求速率(10r/s為每100毫秒處理一個請求;60r/m,即每1000毫秒處理一個請求)。

limit_conn_status:配置被限流后返回的狀態(tài)碼,默認返回503;

limit_conn_log_level:配置記錄被限流后的日志級別,默認error級別。

?

limit_req的主要執(zhí)行過程如下所示:

1、請求進入后首先判斷最后一次請求時間相對于當前時間(第一次是0)是否需要限流,如果需要限流則執(zhí)行步驟2,否則執(zhí)行步驟3;

2.1、如果沒有配置桶容量(burst),則桶容量為0;按照固定速率處理請求;如果請求被限流,則直接返回相應的錯誤碼(默認503);

2.2、如果配置了桶容量(burst>0)且延遲模式(沒有配置nodelay);如果桶滿了,則新進入的請求被限流;如果沒有滿則請求會以固定平均速率被處理(按照固定速率并根據(jù)需要延遲處理請求,延遲使用休眠實現(xiàn));

2.3、如果配置了桶容量(burst>0)且非延遲模式(配置了nodelay);不會按照固定速率處理請求,而是允許突發(fā)處理請求;如果桶滿了,則請求被限流,直接返回相應的錯誤碼;

3、如果沒有被限流,則正常處理請求;

4、Nginx會在相應時機進行選擇一些(3個節(jié)點)限流KEY進行過期處理,進行內(nèi)存回收。

?

場景2.1測試

首先定義IP維度的限流區(qū)域:

limit_req_zone $binary_remote_addrzone=test:10m rate=500r/s;

限制為每秒500個請求,固定平均速率為2毫秒一個請求。

?

接著在要限流的location中添加限流邏輯:

location /limit {
? ? limit_req zone=test;
? ? echo "123";
}

即桶容量為0(burst默認為0),且延遲模式。

?

使用AB測試工具進行測試,并發(fā)數(shù)為2個,總的請求數(shù)為10個:

ab -n 10 -c 2 http://localhost/limit

?????????????????

將得到如下access.log輸出:

[08/Jun/2016:20:25:56+0800] [1465381556.410] 200

[08/Jun/2016:20:25:56 +0800][1465381556.410] 503

[08/Jun/2016:20:25:56 +0800][1465381556.411] 503

[08/Jun/2016:20:25:56+0800] [1465381556.411] 200

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

[08/Jun/2016:20:25:56 +0800][1465381556.412] 503

?

雖然每秒允許500個請求,但是因為桶容量為0,所以流入的請求要么被處理要么被限流,無法延遲處理;另外平均速率在2毫秒左右,比如1465381556.410和1465381556.411被處理了;有朋友會說這固定平均速率不是1毫秒嘛,其實這是因為實現(xiàn)算法沒那么精準造成的。

?

如果被限流在error.log中會看到如下內(nèi)容:

2016/06/08 20:25:56 [error] 6130#0: *1962limiting requests, excess: 1.000 by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

?

如果被延遲了在error.log(日志級別要INFO級別)中會看到如下內(nèi)容:

2016/06/10 09:05:23 [warn] 9766#0: *97021delaying request, excess: 0.368, by zone "test", client: 127.0.0.1,server: _, request: "GET /limit HTTP/1.0", host:"localhost"

?

場景2.2測試

首先定義IP維度的限流區(qū)域:

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

為了方便測試設(shè)置速率為每秒2個請求,即固定平均速率是500毫秒一個請求。

?

接著在要限流的location中添加限流邏輯:

location /limit {
? ? limit_req zone=test burst=3;
? ? echo "123";
}

固定平均速率為500毫秒一個請求,通容量為3,如果桶滿了新的請求被限流,否則可以進入桶中排隊并等待(實現(xiàn)延遲模式)。

?

為了看出限流效果我們寫了一個req.sh腳本:

ab -c 6 -n 6 http://localhost/limit
sleep 0.3
ab -c 6 -n 6 http://localhost/limit

首先進行6個并發(fā)請求6次URL,然后休眠300毫秒,然后再進行6個并發(fā)請求6次URL;中間休眠目的是為了能跨越2秒看到效果,如果看不到如下的效果可以調(diào)節(jié)休眠時間。

?

將得到如下access.log輸出:

[09/Jun/2016:08:46:43+0800] [1465433203.959] 200

[09/Jun/2016:08:46:43 +0800][1465433203.959] 503

[09/Jun/2016:08:46:43 +0800][1465433203.960] 503

[09/Jun/2016:08:46:44+0800] [1465433204.450] 200

[09/Jun/2016:08:46:44+0800] [1465433204.950] 200

[09/Jun/2016:08:46:45 +0800][1465433205.453] 200

?

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.766] 503

[09/Jun/2016:08:46:45 +0800][1465433205.767] 503

[09/Jun/2016:08:46:45+0800] [1465433205.950] 200

[09/Jun/2016:08:46:46+0800] [1465433206.451] 200

[09/Jun/2016:08:46:46+0800] [1465433206.952] 200


Linux高并發(fā)系統(tǒng)之限流技術(shù)
?

桶容量為3,即桶中在時間窗口內(nèi)最多流入3個請求,且按照2r/s的固定速率處理請求(即每隔500毫秒處理一個請求);桶計算時間窗口(1.5秒)=速率(2r/s)/桶容量(3),也就是說在這個時間窗口內(nèi)桶最多暫存3個請求。因此我們要以當前時間往前推1.5秒和1秒來計算時間窗口內(nèi)的總請求數(shù);另外因為默認是延遲模式,所以時間窗內(nèi)的請求要被暫存到桶中,并以固定平均速率處理請求:

第一輪:有4個請求處理成功了,按照漏桶桶容量應該最多3個才對;這是因為計算算法的問題,第一次計算因沒有參考值,所以第一次計算后,后續(xù)的計算才能有參考值,因此第一次成功可以忽略;這個問題影響很小可以忽略;而且按照固定500毫秒的速率處理請求。

第二輪:因為第一輪請求是突發(fā)來的,差不多都在1465433203.959時間點,只是因為漏桶將速率進行了平滑變成了固定平均速率(每500毫秒一個請求);而第二輪計算時間應基于1465433203.959;而第二輪突發(fā)請求差不多都在1465433205.766時間點,因此計算桶容量的時間窗口應基于1465433203.959和1465433205.766來計算,計算結(jié)果為1465433205.766這個時間點漏桶為空了,可以流入桶中3個請求,其他請求被拒絕;又因為第一輪最后一次處理時間是1465433205.453,所以第二輪第一個請求被延遲到了1465433205.950。這里也要注意固定平均速率只是在配置的速率左右,存在計算精度問題,會有一些偏差。

?

如果桶容量改為1(burst=1),執(zhí)行req.sh腳本可以看到如下輸出:

09/Jun/2016:09:04:30+0800] [1465434270.362] 200

[09/Jun/2016:09:04:30 +0800][1465434270.371] 503

[09/Jun/2016:09:04:30 +0800] [1465434270.372]503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30 +0800][1465434270.372] 503

[09/Jun/2016:09:04:30+0800] [1465434270.864] 200

?

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.178] 503

[09/Jun/2016:09:04:31 +0800][1465434271.179] 503

[09/Jun/2016:09:04:31+0800] [1465434271.366] 200

桶容量為1,按照每1000毫秒一個請求的固定平均速率處理請求。

?

場景2.3測試

首先定義IP維度的限流區(qū)域:

limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;

為了方便測試配置為每秒2個請求,固定平均速率是500毫秒一個請求。

?

接著在要限流的location中添加限流邏輯:

location /limit {
? ? limit_req zone=test burst=3 nodelay;
? ? echo "123";
}

桶容量為3,如果桶滿了直接拒絕新請求,且每秒2最多兩個請求,桶按照固定500毫秒的速率以nodelay模式處理請求。

?

為了看到限流效果我們寫了一個req.sh腳本:

ab -c 6 -n 6 http://localhost/limit
sleep 1
ab -c 6 -n 6 http://localhost/limit
sleep 0.3
ab -c 6 -n 6 http://localhost/limit
sleep 0.3
ab -c 6 -n 6 http://localhost/limit
sleep 0.3
ab -c 6 -n 6 http://localhost/limit
sleep 2
ab -c 6 -n 6 http://localhost/limit

?

將得到類似如下access.log輸出:

[09/Jun/2016:14:30:11+0800] [1465453811.754] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.755] 200

[09/Jun/2016:14:30:11+0800] [1465453811.759] 200

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

[09/Jun/2016:14:30:11 +0800][1465453811.759] 503

?

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12+0800] [1465453812.776] 200

[09/Jun/2016:14:30:12 +0800][1465453812.776] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

[09/Jun/2016:14:30:12 +0800][1465453812.777] 503

?

[09/Jun/2016:14:30:13 +0800] [1465453813.095]503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.097] 503

[09/Jun/2016:14:30:13 +0800][1465453813.098] 503

?

[09/Jun/2016:14:30:13+0800] [1465453813.425] 200

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.425] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

[09/Jun/2016:14:30:13 +0800][1465453813.426] 503

?

[09/Jun/2016:14:30:13+0800] [1465453813.754] 200

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.755] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

[09/Jun/2016:14:30:13 +0800][1465453813.756] 503

?

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15+0800] [1465453815.278] 200

[09/Jun/2016:14:30:15 +0800][1465453815.278] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

[09/Jun/2016:14:30:15 +0800][1465453815.279] 503

?

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.300] 200

[09/Jun/2016:14:30:17+0800] [1465453817.301] 200

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503

[09/Jun/2016:14:30:17 +0800][1465453817.301] 503


Linux高并發(fā)系統(tǒng)之限流技術(shù)
?

桶容量為3(,即桶中在時間窗口內(nèi)最多流入3個請求,且按照2r/s的固定速率處理請求(即每隔500毫秒處理一個請求);桶計算時間窗口(1.5秒)=速率(2r/s)/桶容量(3),也就是說在這個時間窗口內(nèi)桶最多暫存3個請求。因此我們要以當前時間往前推1.5秒和1秒來計算時間窗口內(nèi)的總請求數(shù);另外因為配置了nodelay,是非延遲模式,所以允許時間窗內(nèi)突發(fā)請求的;另外從本示例會看出兩個問題:

第一輪和第七輪:有4個請求處理成功了;這是因為計算算法的問題,本示例是如果2秒內(nèi)沒有請求,然后接著突然來了很多請求,第一次計算的結(jié)果將是不正確的;這個問題影響很小可以忽略;

第五輪:1.0秒計算出來是3個請求;此處也是因計算精度的問題,也就是說limit_req實現(xiàn)的算法不是非常精準的,假設(shè)此處看成相對于2.75的話,1.0秒內(nèi)只有1次請求,所以還是允許1次請求的。

?

如果限流出錯了,可以配置錯誤頁面:

proxy_intercept_errors on;
recursive_error_pages on;
error_page 503 //www.jd.com/error.aspx;

limit_conn_zone/limit_req_zone定義的內(nèi)存不足,則后續(xù)的請求將一直被限流,所以需要根據(jù)需求設(shè)置好相應的內(nèi)存大小。

?

此處的限流都是單Nginx的,假設(shè)我們接入層有多個nginx,此處就存在和應用級限流相同的問題;那如何處理呢?一種解決辦法:建立一個負載均衡層將按照限流KEY進行一致性哈希算法將請求哈希到接入層Nginx上,從而相同KEY的將打到同一臺接入層Nginx上;另一種解決方案就是使用Nginx+Lua(OpenResty)調(diào)用分布式限流邏輯實現(xiàn)。

?

lua-resty-limit-traffic

之前介紹的兩個模塊使用上比較簡單,指定KEY、指定限流速率等就可以了,如果我們想根據(jù)實際情況變化KEY、變化速率、變化桶大小等這種動態(tài)特性,使用標準模塊就很難去實現(xiàn)了,因此我們需要一種可編程來解決我們問題;而OpenResty提供了lua限流模塊lua-resty-limit-traffic,通過它可以按照更復雜的業(yè)務邏輯進行動態(tài)限流處理了。其提供了limit.conn和limit.req實現(xiàn),算法與nginx limit_conn和limit_req是一樣的。

?

此處我們來實現(xiàn)ngx_http_limit_req_module中的【場景2.2測試】,不要忘記下載lua-resty-limit-traffic模塊并添加到OpenResty的lualib中。

?

配置用來存放限流用的共享字典:

lua_shared_dict limit_req_store 100m;

?

以下是實現(xiàn)【場景2.2測試】的限流代碼limit_req.lua:

local limit_req = require "resty.limit.req"
local rate = 2 --固定平均速率 2r/s
local burst = 3 --桶容量
local error_status = 503
local nodelay = false --是否需要不延遲處理
local lim, err = limit_req.new("limit_req_store", rate, burst)
if not lim then --沒定義共享字典
? ? ngx.exit(error_status)
end
local key = ngx.var.binary_remote_addr --IP維度的限流
--流入請求,如果請求需要被延遲則delay > 0
local delay, err = lim:incoming(key, true)
if not delay and err == "rejected" then --超出桶大小了
? ? ngx.exit(error_status)
end
if delay > 0 then --根據(jù)需要決定是延遲或者不延遲處理
? ? if nodelay then
? ? ? ? --直接突發(fā)處理了
? ? else
? ? ? ? ngx.sleep(delay) --延遲處理
? ? end
end

即限流邏輯再nginx access階段被訪問,如果不被限流繼續(xù)后續(xù)流程;如果需要被限流要么sleep一段時間繼續(xù)后續(xù)流程,要么返回相應的狀態(tài)碼拒絕請求。

?

在分布式限流中我們使用了簡單的Nginx+Lua進行分布式限流,有了這個模塊也可以使用這個模塊來實現(xiàn)分布式限流。

?

另外在使用Nginx+Lua時也可以獲取ngx.var.connections_active進行過載保護,即如果當前活躍連接數(shù)超過閾值進行限流保護。

if tonumber(ngx.var.connections_active) >= tonumber(limit) then
? ? //限流
end

?

nginx也提供了limit_rate用來對流量限速,如limit_rate 50k,表示限制下載速度為50k。

?

到此筆者在工作中涉及的限流用法就介紹完,這些算法中有些允許突發(fā),有些會×××為平滑,有些計算算法簡單粗暴;其中令牌桶算法和漏桶算法實現(xiàn)上是類似的,只是表述的方向不太一樣,對于業(yè)務來說不必刻意去區(qū)分它們;因此需要根據(jù)實際場景來決定如何限流,最好的算法不一定是最適用的。

?

參考資料

https://en.wikipedia.org/wiki/Token_bucket

https://en.wikipedia.org/wiki/Leaky_bucket

http://redis.io/commands/incr

http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html

https://github.com/openresty/lua-resty-limit-traffic

http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI