併發三劍客之限流方案總結

前言

對於高併發的系統,有三把利器用來保護系統:緩存降級限流。限流常見的應用場景是秒殺、下單和評論等 突發性 併發問題。java

  1. 緩存 的目的是提高 系統訪問速度系統吞吐量node

  2. 降級 是當服務 出問題 或者影響到核心流程的性能,則須要 暫時屏蔽掉,待 高峯 或者 問題解決後 再打開。nginx

  3. 有些場景並不能用 緩存降級 來解決,好比稀缺資源(秒殺、搶購)、寫服務(如評論、下單)、頻繁的複雜查詢(最新的評論)。所以需有一種手段來限制這些場景的 併發/請求量,即 限流redis

正文

限流的目的

限流的目的是經過對 併發訪問/請求進行 限速,或者一個 時間窗口 內的的請求進行限速來 保護系統,一旦達到限制速率則能夠 拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊等待(好比秒殺、評論、下單)、降級(返回託底數據或默認數據,如商品詳情頁庫存默認有貨)。算法

限流的方式

  1. 限制 總併發數(好比 數據庫鏈接池線程池數據庫

  2. 限制 瞬時併發數(如 nginxlimit_conn 模塊,用來限制 瞬時併發鏈接數編程

  3. 限制 時間窗口內的平均速率(如 GuavaRateLimiternginxlimit_req 模塊,限制每秒的平均速率)後端

  4. 限制 遠程接口 調用速率緩存

  5. 限制 MQ 的消費速率bash

  6. 能夠根據 網絡鏈接數網絡流量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. 限流某個接口的時間窗請求數

使用 GuavaCache,示意代碼:

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 提供了 令牌桶算法實現

  1. 平滑突發限流 (SmoothBursty)

  2. 平滑預熱限流 (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 能夠進行更復雜的限流場景。


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索