概述
在微服務、API 化、雲原生大行其道的今天,服務治理不可或缺,而服務治理中限流幾乎是必不可少的手段;微服務化往往伴隨著分散式的架構,那麼僅僅單機限流是不夠的,還需要分散式的限流。
那麼問題就來了:分散式限流中,往往會出現「限流不均衡」或「限流誤差」的情況,這是為什麼呢?
限流
國慶假期,限流這個詞在新聞中應該能頻繁聽到,就是「景區限流」。這裡以無錫的兩個景點為例:
?示例:
- 無錫蠡園:最大承載量調整至 20000 人;瞬時最大承載量調整至 4000 人;
- 無錫東林書院:書院接待日最大承載量即時降至 1500 人,瞬時承載量降至 300 人。
在計算機網路中,限流就是用於控制網路介面控制器傳送或接收請求的速率[1],由此延伸為:限制到達系統的併發請求數,以此來保障系統的穩定性(特別是在微服務、API 化、雲原生系統上)。
常見的限流演算法
- 固定視窗計數器
- 滑動視窗計數器
- 漏桶
- 令牌桶
單機限流和分散式限流
本質上單機限流和分散式限流的區別就在於「承載量」存放的位置。
單機限流直接在單臺伺服器上實現,而在微服務、API 化、雲原生系統上,應用和服務是叢集部署的,因此需要叢集內的多個例項協同工作,以提供叢集範圍的限流,這就是分散式限流。
?為什麼分散式限流會出現不均衡的情況?
比如上面提到的滑動視窗的演算法,可以將計數器存放至 Redis 這樣的 KV 資料庫中。
例如滑動視窗的每個請求的時間記錄可以利用 Redis 的 zset
儲存,利用 ZREMRANGEBYSCORE
刪除時間視窗之外的資料,再用 ZCARD
計數。
示例程式碼[2]如下:
package com.lizba.redis.limit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
/**
* <p>
* Limiting current by sliding window algorithm through zset
* </p>
*
* @Author: Liziba
* @Date: 2021/9/6 18:11
*/
public class SimpleSlidingWindowByZSet {
private Jedis jedis;
public SimpleSlidingWindowByZSet(Jedis jedis) {
this.jedis = jedis;
}
/**
* Judging whether an action is allowed
*
* @param userId User id
* @param actionKey Behavior key
* @param period Current Limiting Cycle
* @param maxCount Maximum number of requests (sliding window size)
* @return
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = this.key(userId, actionKey);
long ts = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
pipe.zadd(key, ts, String.valueOf(ts));
// Remove data other than sliding windows
pipe.zremrangeByScore(key, 0, ts - (period * 1000));
Response<Long> count = pipe.zcard(key);
// Set the expiration time of the behavior, and if the data is cold, zset will be deleted to save memory space
pipe.expire(key, period);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
/**
* Current limiting key
*
* @param userId
* @param actionKey
* @return
*/
public String key(String userId, String actionKey) {
return String.format("limit:%s:%s", userId, actionKey);
}
}
像令牌桶也可以將令牌數量放到 Redis 中。
?答案一:批次導致的誤差
不過以上的方式相當於每一個請求都需要去 Redis 判斷一下能不能透過,在效能上有一定的損耗,所以針對大併發系統,有個最佳化點就是 「批次」。例如每次取令牌不是一個一取,而是取一批,不夠了再去取一批。這樣可以減少對 Redis 的請求。
但是,批次獲取就會導致一定範圍內的限流誤差。比如 a 例項此刻取了 100 個,等下一秒再用,那下一秒叢集總承載量就有可能超過閾值。
這是一種原因。
?答案二:負載均衡負載不均
分散式限流還有一種做法是「平分」,比如之前單機限流 100,現在叢集部署了 5 個例項,那就讓每臺繼續限流 100,即在總的入口做總的流量限制,比如 500,然後每個例項再自己實現限流。
這種情況下,假設總的入口放入了 500 請求,這些請求需要透過負載均衡演算法(如:輪詢、最小連線數、最小連線時間等)以及會話保持策略(如:源地址保持、cookie 保持或特定引數的 hash),分到每臺的請求就可能是不均衡的,比如 a 例項有 70 個,b 例項有 130 個。那麼 a 例項的 70 個會透過,而 b 例項的 130 個可能只有 100 個會透過。這時就出現了「限流不均衡」或「限流偏差」的情況。
這是第二種原因。
總結
由於本人經驗所限,本文只列出了我目前能想到的 2 個答案給大家參考,歡迎各位交流補充。
真實的業務場景是很複雜的,具體到一個工程,限流需要考慮的條件和資源有很多。我們要做的就是透過估算、壓測、試執行、調整、再生產驗證再調整來逼近理想情況。
三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.