天天看點

行業場景限流方案

前言

   行業中許多三方服務上線支付寶後,ISV對自身的業務系統沒有任何的流量保護,往往會被瞬間的流量,高并發導緻系統承載力出現瓶頸,甚至被打垮的挑戰。面對行業場景業務的穩定性痛點,我們往往采取限流熔斷的機制來保障ISV自身系統在承諾容量下的穩定運作。

   這裡我們聊聊針對行業場景中合作夥伴該采取哪種限流方案的選擇?

常見的限流算法政策

   在了解具體限流方案之前,先來熟悉下常見 "兩窗兩桶" 的限流算法。

計數器固定視窗算法:

   固定視窗算法簡單粗暴。大緻原理是: 維護一個機關時間内的計數值,每個請求通過後,計數值加1,當計數值超過限流門檻值時,機關時間内的其他請求就會被限流。一個周期的機關時間結束,計數器會清零,流量恢複正常,進入下一個周期。

   缺點: 無法限制突發尖刺流量! 因為兩個時間視窗之間沒有聯系,調用者可以在一個時間視窗的結束到下一個時間視窗的開始這個間隙内發超過限流門檻值的請求。

計數器滑動視窗算法:

   滑動視窗其實就是細分後的計數器,相比固定視窗,将時間視窗細化分割,每過一個時間視窗,整個時間視窗就會往右移動一格。

漏桶算法:

   漏桶算法類似一個限制出水速度的水桶,固定出口的速率。無論你流入速率多大,都按照既定的速率去處理,如果桶滿則拒絕服務(觸發限流機制)。

注: 圖為轉發
行業場景限流方案

令牌桶算法:

   對于很多場景來說,除了要求能夠限制平均傳輸速率外,還要求允許某種程度的突發請求,比如 秒殺,這時候漏桶算法就不合适了。令牌桶算法是Traffic Shaping和Rate Limiting最常使用的一種算法。

   令牌桶原理: 以一個恒定的速度往桶裡放令牌,有請求需要被處理,就從桶裡擷取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。令牌桶的這種特性可以對突增的流量進行平緩處理,讓系統負載更加均勻。

行業場景限流方案

舉個例子: 去電影院看電影買票,賣出的電影票是固定的, 如果來晚了(後面的請求)就沒票了, 要麼等待下一場(等待新的令牌發放), 要麼不看了(被拒絕)。

  • 如果你的系統沒有尖刺流量,對于流量絕對均勻有很強的要求,建議使用漏鬥。
  • 如果你的系統有少量突增流量,同時你希望限流算法簡單易實作,建議滑動時間視窗。
  • 如果你的系統經常有突增流量,為了系統整體穩定性,建議使用令牌桶。

行業限流解決方案介紹

常見的限流方式有:

  • 限制總并發數(資料庫連接配接池、線程池等等)
  • 限制瞬時并發數(如Nginx的limit_conn子產品)
  • 限制時間視窗的平均速率(如Guava的RateLimiter、Nginx的limit_req子產品)
  • 限制遠端接口的調用速率、限制消息系統的消費速率

接入層限流

   抗疫項目中,一般ISV會把Nginx作為業務的接入層,通過Nginx将請求分發到後端的應用叢集上。接入層(流量層)是整個系統的咽喉入口, 越早擋掉請求越好,扛不住的流量越往後端傳遞,對後端的壓力越大。

Nginx限流

行業場景限流方案

官方提供兩個子產品:

  • limit_conn子產品 (限制瞬時并發數)
  • limit_req子產品 (限制機關時間内的請求數,速率限制,采用的漏桶算法)
limit_req

某口罩預約項目NG限流配置:

http {
  ......
  # $binary_remote_addr 通過remote_addr這個辨別來做限制
  # zone=xxx:10m 生成一個大小為10M,名字為xxx的記憶體區域,用來存儲通路的頻次資訊,64位可以存放163840個IP位址。
  # rate=xx r/s 限制相同辨別的用戶端的通路頻次
  limit_req_zone $binary_remote_addr zone=req_perip:10m rate=30r/m; #30r/m: 限制兩秒鐘一個請求
  limit_req_zone $binary_remote_addr zone=req_query:10m rate=5r/s; #5r/m: 限制每秒五個請求
 }
server { 
    location /mask-appointment-service/appointment/ { 
                ......
            limit_req zone=req_perip burst=4; # 通過req_perip區域來限制
        limit_req_status 507;      # 限流傳回值507
        ......
    }
    location /mask-appointment-service/query/ {
        ......
        limit_req zone=req_query; # 通過req_query區域來限制
        limit_req_status 509;     # 限流傳回值507
        ......
    }
}           

上述Nginx配置分别配置了 limit_req 兩個規則,預約 和 查詢。

  • 預約: 針對某個唯一的來源IP做速率的控制,速率為30r/m(每2秒1個請求)。
  • 查詢: 針對某個唯一的來源IP做速率的控制,速率為5r/s (每1秒5個請求)。

特别注意:

rate=30r/m 很多時候會讓人了解成每分鐘30個請求, 其實并不是。官網的解釋:

速率以每秒請求數r/s的形式指定。如果希望速率小于每秒一個請求,可以按每分鐘請求數 r/m 來指定。例如,每秒半個請求為: 30r/m。

The rate is specified in requests per second (r/s). If a rate of less than one request per second is desired, it is specified in request per minute (r/m). For example, half-request per second is 30r/m.

幾個寫法含義:

limit_req_zone $binary_remote_addr zone=limit_login:10m rate=10r/m;
代表單ip每6秒1個請求(60/10=6)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=20r/m;
代表單ip每3秒1個請求(60/20=3)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=30r/m;
代表單ip每2秒1個請求(60/30=2)(官方文檔叫half-request per second 1秒半個請求)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=1r/s;
代表單ip每秒1個請求
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=2r/s;
代表單ip每秒2個請求           

PS:

size ?該如何設定呢?

   一個二進制的IP位址在64位機器上占用63個位元組,設定10M的話: 10x1024x1024/64 = 163840,64位可以存放163840個IP位址。

limit_conn

   limit_conn 這裡不詳細說了,有興趣同學可以自行了解一下。

Nginx限流總結

   Nginx屬于接入層面,不能解決所有問題,比如内部調用的一些接口無法保證是否有限流控制,同時也隻能擋掉http請求,比如rpc請求無法限制,是以在應用層實作限流還是很有必要的。上述Nginx限流都是對于單NG的,如果行業合作夥伴的接入層有多個Nginx,該怎麼辦呢?

  • 解決方案1: Nginx前面部署負載均衡,通過一緻性哈希按照限流的key把請求轉發到接入層的NG(相同key的請求打到同一台Nginx上)
  • 解決方案2: 通過 Nginx+Lua(OpenResty)調用分布式限流邏輯實作。

更多資訊可以檢視官方文檔

[limit_req - Nginx] http://nginx.org/en/docs/http/ngx_http_limit_req_module.html [limit_conn - Nginx] http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html

Openresty

    有需要動态和精細控制的需求,可以通過Openresty做接入,OpenResty提供了lua-resty-limit-traffic 的Lua限流子產品,基于Nginx的limit.conn和limit.req實作,通過可程式設計的方式進行複雜場景限流設計。比如變化限流速率,變化桶的大小等動态特性,可以按照更複雜的業務邏輯進行限流處理。

lua-resty-limit-traffic

resty.limit.req 子產品

示例代碼

local limit_req = require "resty,limit.req"
local lim, err = limit_req.new("mylimit", 5, 9)

local delay, err = lim:incoming(ngx.var.binary_remote_addr, true)
if not delay then
    if err == "rejected" then
              return ngx.exit(503)
          end 
              return ngx.exit(500)
  end

if delay >= 0.001 then
    ngx.sleep(delay)
end           

resty.limit.req 和 Nginx 的 limit_req 實作的效果和功能一樣,但是這裡用 Lua 來表達限速邏輯,可以在别的代碼裡面去引入。

更多其它子產品和用法可以檢視官方文檔:

[lua-resty-limit-traffic - Openresty] https://github.com/openresty/lua-resty-limit-traffic

四層限流

   OSI 四層限流可以通過SLB、WAF等四層接入進行流量的限流,也可以通過F5等硬負載四層裝置來實作對流量控制。四層流量控制異常設計的面比較大,一般情況下慎用。

API路由網關

   在微服務架構中,API路由網關作為内部系統的入口,非常适合做API限流操作,網關層的限流可以簡單地針對不同業務的接口進行限流。業界較有名的Zuul、Dubbo都可以對限流進行較好的設計,當然還有我們的 Sentinel開源方案,Sentinel 支援對 Spring Cloud Gateway、Zuul 等主流的 API Gateway 進行限流。這裡就不詳細說了,有需要查閱相關資料。

應用層限流

單機單應用限流

   我們可以在伺服器内部通過編寫一些算法進行限流,也可以調用一些類庫中存在的API,比如Google Guava類庫的RateLimiter,Guava RateLimiter 是 Google Guava 提供的生産級别的限流工具,RateLimiter 基于令牌桶流控算法,可以有效控制 單 JVM 下某個邏輯操作的頻率。

   Guava 的 RateLimiter 提供了以下核心方法 create()acquire()tryAcquire(),其中 create() 建立令牌桶,acquire() 擷取令牌,支援同時擷取 N 個令牌,tryAcquire(long timeout, TimeUint unit)支援在限定時間内擷取令牌,如果擷取不到将傳回 false。

示例代碼如下:

import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiterDemo {
    // 每秒 100 個令牌
    private final RateLimiter rateLimiter = RateLimiter.create(100.0);
    void doBusiness() {
        rateLimiter.acquire();
        // 繼續操作使用者業務
    }
}           

由于 acquire() 會一直等待,并且 Guava 不保證公平分發,是以會出現線程持續等待情況,需要考慮逾時情況下面請求直接結束。

import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class GuavaRateLimiterDemo {
    // 每秒 100 個令牌
    private final RateLimiter rateLimiter = RateLimiter.create(100.0);
    void doBusinessWithTryAcquire() {
        boolean isPermit = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
        if (!isPermit) {
            throw new RuntimeException("business overheated.");
        }
        // 繼續操作使用者業務
    }
}           

RateLimiter 不僅實作了令牌桶,還做了不少優化,可以滿足不同場景的需求。

它支援「預熱」warmupPeriod,tryAcquire 操作還支援立即計算傳回,避免無效等待。

[RateLimiter (Guava: Google Core Libraries for Java 19.0 API)]) https://guava.dev/releases/19.0/api/docs/index.html?com/google/common/util/concurrent/RateLimiter.html

缺點:

Guava RateLimiter(以及其他基于令牌桶算法的實作)缺點是隻能針對單機,無法在分布式環境下面共享流量資料, 不适用于分布式服務。如果想使用的話,需要提前計算好每個示例負載均衡之後的流量門檻值。

分布式限流

   這一類的限流政策跟上面 API 路由網關模式的限流相似,同樣是依賴配置中心管理,限流邏輯會配套服務化的架構完成。

阿裡雲應用高可用-Ahas

    Ahas是阿裡雲提供的應用高可用服務(Application High Availability Service)産品,基于 Sentinel 提供了專業的多樣化的流控降級手段。

    和常見的網關限流相比,Ahas 流控降級有以下優勢:

  • 提供 Web Servlet、Dubbo、Spring Boot、Spring Cloud、gRPC、Apache RocketMQ、Netflix Zuul 等多種主流架構的适配,隻需要引入相應的依賴并進行少量配置即可接入。
  • ECS 叢集和容器服務 Kubernetes 叢集均支援使用 Ahas 實作限流降級。
  • 全方位的監控, 可以實時監控展示通過的 QPS、拒絕的 QPS、響應時間等資訊,同時支援檢視單機指定資源的監控資料。
  • 自建機房,伺服器,私有雲,其它雲都可接入 Ahas 進行限流降級管理。

    實際項目應用截圖:

行業場景限流方案
行業場景限流方案
行業場景限流方案

更多資訊可以檢視阿裡雲官方文檔

[AHAS 使用指引] https://help.aliyun.com/document_detail/144439.html?spm=a2c4g.11186623.6.541.75203a78CViDbJ

不同限流方案該怎麼選擇?

行業場景中,推薦使用Nginx、OpenResty、Guava RateLimiter、Ahas、四層限流等方案,基本能滿足需求。

限流方案 分類 優勢 劣勢
Nginx ISV配置簡單,成本低

• 限流力度靜态,手動配置,面對流量波動業務,很難去定義合理門檻值。

• 無法對服務層單服務做有效限流政策配置

•對接入層性能有較高要求

OpenResty 可程式設計,可進行豐富、靈活的流控政策 隻能擋掉HTTP請求
Guava RateLimiter 應用層限流單機單應用

• 精度高,高效友善

• ISV可以對内部調用的三方接口進行單獨的限流

• 無法跨JVM,不适用于分布式服務
Ahas

• 阿裡雲提供現成的高可用限流熔斷功能, 一鍵接入

• 支援多種主流架構适配

• 可視化配置,監控,簡單高效

• 外網要求 (ISV接入Ahas需滿足外網聯通下發流控規則,某些政務專網業務可能不适用)

• 收費

SLB、 WAF、 硬體防火牆

• 四層流量攔截效率高

• 流量第一層防護

• 隻能針對總體流量進行限流,一般配合其它層限流使用

抗擊疫情-某口罩預約項目實戰

挑戰:

    在抗疫期間,我們聯合政府機構,外部的合作夥伴緊急上線了口罩預約的服務。由于在疫情期間,口罩預約是個大熱點,在開放預約入口1分鐘内,預約系統面臨着每秒峰值XX w+的并發壓力。針對這種秒殺預約的場景,聯合ISV對業務系統進行了一系列的改造優化, 這裡我們重點來看看該項目是怎麼通過不斷消減請求和限流來保障流量突增時系統的穩定性的。

實戰

行業場景限流方案

第一層: 合法性限流攔截

   在口罩預約開放時間段内,使用者執行的第一個動作是填寫預約資訊,并擷取手機驗證碼進行送出。我們可以在這一層就将不合法的請求給攔截掉(刷單行為、是否已登入,預約管道是否合法), 允許合法的使用者通路到伺服器。

  1. 預約時使用手機驗證碼,用來拉長使用者通路時間。(輸入驗證碼到送出預約整個過程可能需要五秒時間,降低通路峰值)
  2. 防火牆進行IP限制政策配置,防止不合法刷單行為。
  3. 非預約時間段内隐藏口罩預約入口頁面。

第二層: 負載限流與服務限流

   合法性限流隻能限制無效、非法的請求。但對于口罩預約場景來說,仍然會有大量的合法有效請求進入系統。在項目上線前,協助ISV進行如下的分流和限流的優化。

  1. 查詢和預約讀寫分離,分流資料庫的壓力。
  2. 靜态資源請求上CDN,分擔了總請求處理的壓力。
  3. 擴容了Nginx代理層和背景服務叢集,共同來負載請求壓力。
  4. Nginx入層進行了第二層的限流熔斷。(詳細配置請參考上述文檔中的Nginx限流方案)。
  5. 擷取手機驗證碼會調用三方短信服務網關,在應用接口層面單獨進行了限流。