對於高併發的系統,有三把利器用來保護系統:緩存、降級 和 限流。限流常見的應用場景是秒殺、下單和評論等 突發性 併發問題。java
緩存 的目的是提高 系統訪問速度 和 系統吞吐量。node
降級 是當服務 出問題 或者影響到核心流程的性能,則須要 暫時屏蔽掉,待 高峯 或者 問題解決後 再打開。nginx
有些場景並不能用 緩存 和 降級 來解決,好比稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(最新的評論)。所以需有一種手段來限制這些場景的 併發/請求量,即 限流。redis
限流的目的是經過對 併發訪問/請求進行 限速,或者一個 時間窗口 內的的請求進行限速來 保護系統,一旦達到限制速率則能夠 拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊 或 等待(好比秒殺、評論、下單)、降級(返回託底數據或默認數據,如商品詳情頁庫存默認有貨)。算法
限制 總併發數(好比 數據庫鏈接池、線程池)數據庫
限制 瞬時併發數(如 nginx
的 limit_conn
模塊,用來限制 瞬時併發鏈接數)編程
限制 時間窗口內的平均速率(如 Guava
的 RateLimiter
、nginx
的 limit_req
模塊,限制每秒的平均速率)後端
限制 遠程接口 調用速率緩存
限制 MQ
的消費速率bash
能夠根據 網絡鏈接數、網絡流量、CPU
或 內存負載 等來限流
有時候還可使用 計數器 來進行限流,主要用來限制 總併發數,好比 數據庫鏈接池、線程池、秒殺的併發數。經過 全局總請求數 或者 必定時間段的總請求數 設定的 閥值 來限流。這是一種 簡單粗暴 的限流方式,而不是 平均速率限流。
令牌桶限制的是 平均流入速率,容許突發請求,並容許必定程度 突發流量。
漏桶限制的是 常量流出速率,從而平滑 突發流入速率。
可使用池化技術來限制總資源數:鏈接池、線程池。好比分配給每一個應用的數據庫鏈接是 100
,那麼本應用最多可使用 100
個資源,超出了能夠 等待 或者 拋異常。
若是你使用過 Tomcat
,其 Connector
其中一種配置有以下幾個參數:
maxThreads: Tomcat
能啓動用來處理請求的 最大線程數,若是請求處理量一直遠遠大於最大線程數,可能會僵死。
maxConnections: 瞬時最大鏈接數,超出的會 排隊等待。
acceptCount: 若是 Tomcat
的線程都忙於響應,新來的鏈接會進入 隊列排隊,若是 超出排隊大小,則 拒絕鏈接。
使用 Java
中的 AtomicLong
,示意代碼:
try{
if(atomic.incrementAndGet() > 限流數) {
//拒絕請求
} else {
//處理請求
}
} finally {
atomic.decrementAndGet();
}
複製代碼
使用 Guava
的 Cache
,示意代碼:
LoadingCache counter = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(newCacheLoader() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return newAtomicLong(0);
}
});
longlimit =1000;
while(true) {
// 獲得當前秒
long currentSeconds = System.currentTimeMillis() /1000;
if(counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了: " + currentSeconds);
continue;
}
// 業務處理
}
複製代碼
以前的限流方式都不能很好地應對 突發請求,即 瞬間請求 可能都被容許從而致使一些問題。所以在一些場景中須要對突發請求進行改造,改造爲 平均速率 請求處理。
Guava RateLimiter
提供了 令牌桶算法實現:
平滑突發限流 (SmoothBursty
)
平滑預熱限流 (SmoothWarmingUp
) 實現
RateLimiter limiter = RateLimiter.create(5);
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());
System.out.println(limiter.acquire());
複製代碼
將獲得相似以下的輸出:
0.0
0.198239
0.196083
0.200609
0.199599
0.19961
複製代碼
RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);
for(inti = 1; i < 5; i++) {
System.out.println(limiter.acquire());
}
Thread.sleep(1000L);
for(inti = 1; i < 5; i++) {
System.out.println(limiter.acquire());
}
複製代碼
將獲得相似以下的輸出:
0.0
0.51767
0.357814
0.219992
0.199984
0.0
0.360826
0.220166
0.199723
0.199555
複製代碼
SmoothWarmingUp
的建立方式:
RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit);
複製代碼
速率是 梯形上升 速率的,也就是說 冷啓動 時會以一個比較大的速率慢慢到平均速率;而後趨於 平均速率(梯形降低到平均速率)。能夠經過調節 warmupPeriod
參數實現一開始就是平滑固定速率。
分佈式限流最關鍵的是要將 限流服務 作成 原子化,而解決方案可使用 redis + lua
或者 nginx + lua
技術進行實現。
接入層 一般指請求流量的入口,該層的主要目的有:
對於 Nginx
接入層限流 可使用 Nginx
自帶了兩個模塊:鏈接數限流模塊 ngx_http_limit_conn_module
和 漏桶 算法實現的 請求限流模塊 ngx_http_limit_req_module
。還可使用 OpenResty
提供的 Lua
限流模塊 lua-resty-limit-traffic
進行 更復雜的 限流場景。
limit_conn: 用來對某個 KEY
對應的 總的網絡鏈接數 進行限流,能夠按照如 IP
、域名維度 進行限流。
limit_req: 用來對某個 KEY
對應的 請求的平均速率 進行限流,並有兩種用法:平滑模式(delay
)和 容許突發模式 (nodelay
)。
OpenResty
提供的 Lua
限流模塊 lua-resty-limit-traffic
能夠進行更復雜的限流場景。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。