前言
對於高併發的系統,有三把利器用來保護系統:快取、降級 和 限流。限流常見的應用場景是秒殺、下單和評論等 突發性 併發問題。
-
快取 的目的是提升 系統訪問速度 和 系統吞吐量。
-
降級 是當服務 出問題 或者影響到核心流程的效能,則需要 暫時遮蔽掉,待 高峰 或者 問題解決後 再開啟。
-
有些場景並不能用 快取 和 降級 來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(最新的評論)。因此需有一種手段來限制這些場景的 併發/請求量,即 限流。
正文
限流的目的
限流的目的是通過對 併發訪問/請求進行 限速,或者一個 時間視窗 內的的請求進行限速來 保護系統,一旦達到限制速率則可以 拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊 或 等待(比如秒殺、評論、下單)、降級(返回託底資料或預設資料,如商品詳情頁庫存預設有貨)。
限流的方式
-
限制 總併發數(比如 資料庫連線池、執行緒池)
-
限制 瞬時併發數(如
nginx
的limit_conn
模組,用來限制 瞬時併發連線數) -
限制 時間視窗內的平均速率(如
Guava
的RateLimiter
、nginx
的limit_req
模組,限制每秒的平均速率) -
限制 遠端介面 呼叫速率
-
限制
MQ
的消費速率 -
可以根據 網路連線數、網路流量、
CPU
或 記憶體負載 等來限流
限流的演算法
1. 令牌桶
![併發三劍客之限流方案總結](https://i.iter01.com/images/9c875bbd0e2df316c4ab91820967244f65e6975e62520d3eacc8b9bd0949590e.png)
2. 漏桶
![併發三劍客之限流方案總結](https://i.iter01.com/images/fa16266bfb7422d87f49613cb5d4afaa17b9a6b87b19661173dcb103d8a5cb8a.jpg)
3. 計數器
有時候還可以使用 計數器 來進行限流,主要用來限制 總併發數,比如 資料庫連線池、執行緒池、秒殺的併發數。通過 全域性總請求數 或者 一定時間段的總請求數 設定的 閥值 來限流。這是一種 簡單粗暴 的限流方式,而不是 平均速率限流。
令牌桶 vs 漏桶
令牌桶限制的是 平均流入速率,允許突發請求,並允許一定程度 突發流量。
漏桶限制的是 常量流出速率,從而平滑 突發流入速率。
應用級別限流
1. 限流總資源數
可以使用池化技術來限制總資源數:連線池、執行緒池。比如分配給每個應用的資料庫連線是 100
,那麼本應用最多可以使用 100
個資源,超出了可以 等待 或者 拋異常。
2. 限流總併發/連線/請求數
如果你使用過 Tomcat
,其 Connector
其中一種配置有如下幾個引數:
-
maxThreads:
Tomcat
能啟動用來處理請求的 最大執行緒數,如果請求處理量一直遠遠大於最大執行緒數,可能會僵死。 -
maxConnections: 瞬時最大連線數,超出的會 排隊等待。
-
acceptCount: 如果
Tomcat
的執行緒都忙於響應,新來的連線會進入 佇列排隊,如果 超出排隊大小,則 拒絕連線。
3. 限流某個介面的總併發/請求數
使用 Java
中的 AtomicLong
,示意程式碼:
try{
if(atomic.incrementAndGet() > 限流數) {
//拒絕請求
} else {
//處理請求
}
} finally {
atomic.decrementAndGet();
}
複製程式碼
4. 限流某個介面的時間窗請求數
使用 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;
}
// 業務處理
}
複製程式碼
5. 平滑限流某個介面的請求數
之前的限流方式都不能很好地應對 突發請求,即 瞬間請求 可能都被允許從而導致一些問題。因此在一些場景中需要對突發請求進行改造,改造為 平均速率 請求處理。
Guava RateLimiter
提供了 令牌桶演算法實現:
-
平滑突發限流 (
SmoothBursty
) -
平滑預熱限流 (
SmoothWarmingUp
) 實現
平滑突發限流(SmoothBursty)
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
複製程式碼
平滑預熱限流(SmoothWarmingUp)
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);
複製程式碼
- permitsPerSecond: 表示 每秒新增 的令牌數
- warmupPeriod: 表示在從 冷啟動速率 過渡到 平均速率 的時間間隔
速率是 梯形上升 速率的,也就是說 冷啟動 時會以一個比較大的速率慢慢到平均速率;然後趨於 平均速率(梯形下降到平均速率)。可以通過調節 warmupPeriod
引數實現一開始就是平滑固定速率。
分散式限流
分散式限流最關鍵的是要將 限流服務 做成 原子化,而解決方案可以使用 redis + lua
或者 nginx + lua
技術進行實現。
接入層限流
接入層 通常指請求流量的入口,該層的主要目的有:
- 負載均衡
- 非法請求過濾
- 請求聚合
- 快取、降級、限流
- A/B測試
- 服務質量監控
對於 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
可以進行更復雜的限流場景。
歡迎關注技術公眾號: 零壹技術棧
![零壹技術棧](https://i.iter01.com/images/ea700b08bf80b9a7dd8bcee1363b7d2a80ed620e6562d04060c8fd5f62c7b980.jpg)
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。