前言
對於高併發的系統,有三把利器用來保護系統:快取、降級 和 限流。限流常見的應用場景是秒殺、下單和評論等 突發性 併發問題。
-
快取 的目的是提升 系統訪問速度 和 系統吞吐量。
-
降級 是當服務 出問題 或者影響到核心流程的效能,則需要 暫時遮蔽掉,待 高峰 或者 問題解決後 再開啟。
-
有些場景並不能用 快取 和 降級 來解決,比如稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(最新的評論)。因此需有一種手段來限制這些場景的 併發/請求量,即 限流。
正文
限流的目的
限流的目的是通過對 併發訪問/請求進行 限速,或者一個 時間視窗 內的的請求進行限速來 保護系統,一旦達到限制速率則可以 拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊 或 等待(比如秒殺、評論、下單)、降級(返回託底資料或預設資料,如商品詳情頁庫存預設有貨)。
限流的方式
-
限制 總併發數(比如 資料庫連線池、執行緒池)
-
限制 瞬時併發數(如
nginx
的limit_conn
模組,用來限制 瞬時併發連線數) -
限制 時間視窗內的平均速率(如
Guava
的RateLimiter
、nginx
的limit_req
模組,限制每秒的平均速率) -
限制 遠端介面 呼叫速率
-
限制
MQ
的消費速率 -
可以根據 網路連線數、網路流量、
CPU
或 記憶體負載 等來限流
限流的演算法
1. 令牌桶
2. 漏桶
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
可以進行更復雜的限流場景。
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。