一年一度的「雙 11」又要到了,阿里的碼農們進入了一年中最辛苦的時光。各種容量評估、壓測、擴容讓我們忙得不可開交。洛陽親友如相問,就說我搞雙十一。
如何讓系統在洶湧澎湃的流量面前談笑風生?我們的策略是不要讓系統超負荷工作。如果現有的系統扛不住業務目標怎麼辦?加機器!機器不夠怎麼辦?業務降級,系統限流!
正所謂「他強任他強,清風拂山崗;他橫任他橫,明月照大江」,降級和限流是大促保障中必不可少的神兵利器,丟卒保車,以暫停邊緣業務為代價保障核心業務的資源,以系統不被突發流量壓掛為第一要務。
集團的中介軟體有一個不錯的單機限流框架,支援兩種限流模式:控制速率和控制併發。限流這種東西,應該是來源於網路裡面的「流量整型」,通過控制資料包的傳輸速率和時機,來實現一些效能、服務質量方面的東西。令牌桶是一種常見的流控演算法,屬於控制速率型別的。控制併發則相對要常見的多,比如作業系統裡的「訊號量」就是一種控制併發的方式。
在 Wikipedia 上,令牌桶演算法是這麼描述的:
- 每秒會有 r 個令牌放入桶中,或者說,每過 1/r 秒桶中增加一個令牌
- 桶中最多存放 b 個令牌,如果桶滿了,新放入的令牌會被丟棄
- 當一個 n 位元組的資料包到達時,消耗 n 個令牌,然後傳送該資料包
- 如果桶中可用令牌小於 n,則該資料包將被快取或丟棄
令牌桶控制的是一個時間視窗內的通過的資料量,在 API 層面我們常說的 QPS、TPS,正好是一個時間視窗內的請求量或者事務量,只不過時間視窗限定在 1s 罷了。
現實世界的網路工程中使用的令牌桶,比概念圖中的自然是複雜了許多,「令牌桶」的數量也不是一個而是兩個,簡單的演算法描述可用參考中興的期刊[1]或者 RFC。
假如專案使用 Java 語言,我們可以輕鬆地藉助 Guava 的 RateLimiter 來實現基於令牌桶的流控。RateLimiter 令牌桶演算法的單桶實現,也許是因為在 Web 應用層面單桶實現就夠用了,雙筒實現就屬於過度設計。
RateLimiter 對簡單的令牌桶演算法做了一些工程上的優化,具體的實現是 SmoothBursty。需要注意的是,RateLimiter 的另一個實現 SmoothWarmingUp,就不是令牌桶了,而是漏桶演算法。也許是出於簡單起見,RateLimiter 中的時間視窗能且僅能為 1s,如果想搞其他時間單位的限流,只能另外造輪子。
SmoothBursty 積極響應李克強總理的號召,上個月的流量沒用完,可以挪到下個月用。其實就是 SmoothBursty 有一個可以放 N 個時間視窗產生的令牌的桶,系統空閒的時候令牌就一直攢著,最好情況下可以扛 N 倍於限流值的高峰而不影響後續請求。如果不想像三峽大壩一樣能扛千年一遇的洪水,可以把 N 設定為 1,這樣就只屯一個時間視窗的令牌。
RateLimiter 有一個有趣的特性是「前人挖坑後人跳」,也就是說 RateLimiter 允許某次請求拿走超出剩餘令牌數的令牌,但是下一次請求將為此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌為止[2]。這裡面就涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它先走掉後面的請求等一等呢?Guava 的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說。
當我們要實現一個基於速率的單機流控框架的時候,RateLimiter 是一個完善的核心元件,就彷彿 Linux 核心對 GNU 作業系統那樣重要。但是我們還需要其他的一些東西才能把一個流控框架跑起來,比如一個通用的 API,一個攔截器,一個線上配置流控閾值的後臺等等。
下面隨便寫了一個簡單的流控框架 API,至於攔截器和後臺就懶得寫了,有時間再自己造一套中介軟體的輪子吧~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
public class TrafficShaper { public static class RateLimitException extends Exception { private static final long serialVersionUID = 1L; private String resource; public String getResource() { return resource; } public RateLimitException(String resource) { super(resource + " should not be visited so frequently"); this.resource = resource; } @Override public synchronized Throwable fillInStackTrace() { return this; } } private static final ConcurrentMap<String, RateLimiter> resourceLimiterMap = Maps.newConcurrentMap(); public static void updateResourceQps(String resource, double qps) { RateLimiter limiter = resourceLimiterMap.get(resource); if (limiter == null) { limiter = RateLimiter.create(qps); RateLimiter putByOtherThread = resourceLimiterMap.putIfAbsent(resource, limiter); if (putByOtherThread != null) { limiter = putByOtherThread; } } limiter.setRate(qps); } public static void removeResource(String resource) { resourceLimiterMap.remove(resource); } public static void enter(String resource) throws RateLimitException { RateLimiter limiter = resourceLimiterMap.get(resource); if (limiter == null) { return; } if (!limiter.tryAcquire()) { throw new RateLimitException(resource); } } public static void exit(String resource) { //do nothing when use RateLimiter } } |