怎麼樣通過Nginx實現限流?

作者 |  雪山上的蒲公英
來源 |  https://www.cnblogs.com/zjfjava/p/10947264.html

流量限制(rate-limiting),是Nginx中一個非常實用,卻經常被錯誤理解和錯誤配置的功能。我們可以用來限制用戶在給定時間內HTTP請求的數量。請求,可以是一個簡單網站首頁的GET請求,也可以是登錄表單的POST請求。

流量限制可以用作安全目的,比如可以減慢暴力密碼破解的速率。通過將傳入請求的速率限制爲真實用戶的典型值,並標識目標URL地址(通過日誌),還可以用來抵禦DDOS攻擊。更常見的情況,該功能被用來保護上游應用服務器不被同時太多用戶請求所壓垮。

本篇文章將會介紹Nginx的 流量限制 的基礎知識和高級配置,”流量限制”在Nginx Plus中也適用。

Nginx如何限流

Nginx的”流量限制”使用漏桶算法(leaky bucket algorithm),該算法在通訊和分組交換計算機網絡中廣泛使用,用以處理帶寬有限時的突發情況。就好比,一個桶口在倒水,桶底在漏水的水桶。如果桶口倒水的速率大於桶底的漏水速率,桶裏面的水將會溢出;同樣,在請求處理方面,水代表來自客戶端的請求,水桶代表根據”先進先出調度算法”(FIFO)等待被處理的請求隊列,桶底漏出的水代表離開緩衝區被服務器處理的請求,桶口溢出的水代表被丟棄和不被處理的請求。

配置基本的限流

“流量限制”配置兩個主要的指令,limit_req_zone和limit_req,如下所示:


limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=mylimit;

        proxy_pass http://my_upstream;
    }
}

limit_req_zone指令定義了流量限制相關的參數,而limit_req指令在出現的上下文中啓用流量限制(示例中,對於”/login/”的所有請求)。limit_req_zone指令通常在HTTP塊中定義,使其可在多個上下文中使用,它需要以下三個參數:

  • Key - 定義應用限制的請求特性。示例中的Nginx變量remote_addr,佔用更少的空間)
  • Zone - 定義用於存儲每個IP地址狀態以及被限制請求URL訪問頻率的共享內存區域。保存在內存共享區域的信息,意味着可以在Nginx的worker進程之間共享。定義分爲兩個部分:通過zone=keyword標識區域的名字,以及冒號後面跟區域大小。16000個IP地址的狀態信息,大約需要1MB,所以示例中區域可以存儲160000個IP地址。
  • Rate - 定義最大請求速率。在示例中,速率不能超過每秒10個請求。Nginx實際上以毫秒的粒度來跟蹤請求,所以速率限制相當於每100毫秒1個請求。因爲不允許”突發情況”(見下一章節),這意味着在前一個請求100毫秒內到達的請求將被拒絕。

當Nginx需要添加新條目時存儲空間不足,將會刪除舊條目。如果釋放的空間仍不夠容納新記錄,Nginx將會返回 503狀態碼(Service Temporarily Unavailable)。另外,爲了防止內存被耗盡,Nginx每次創建新條目時,最多刪除兩條60秒內未使用的條目。

limit_req_zone指令設置流量限制和共享內存區域的參數,但實際上並不限制請求速率。所以需要通過添加limit_req指令,將流量限制應用在特定的location或者server塊。在上面示例中,我們對/login/請求進行流量限制。

現在每個IP地址被限制爲每秒只能請求10次/login/,更準確地說,在前一個請求的100毫秒內不能請求該URL。


處理突發

如果我們在100毫秒內接收到2個請求,怎麼辦?對於第二個請求,Nginx將給客戶端返回狀態碼503。這可能並不是我們想要的結果,因爲應用本質上趨向於突發性。相反地,我們希望緩衝任何超額的請求,然後及時地處理它們。我們更新下配置,在limit_req中使用burst參數:

location /login/ {
    limit_req zone=mylimit burst=20;
    proxy_pass http://my_upstream;
}

burst參數定義了超出zone指定速率的情況下(示例中的mylimit區域,速率限制在每秒10個請求,或每100毫秒一個請求),客戶端還能發起多少請求。上一個請求100毫秒內到達的請求將會被放入隊列,我們將隊列大小設置爲20。

這意味着,如果從一個給定IP地址發送21個請求,Nginx會立即將第一個請求發送到上游服務器羣,然後將餘下20個請求放在隊列中。然後每100毫秒轉發一個排隊的請求,只有當傳入請求使隊列中排隊的請求數超過20時,Nginx纔會向客戶端返回503。

如果您正在學習Spring Boot,推薦一個連載多年還在繼續更新的免費教程:http://blog.didispace.com/spring-boot-learning-2x/

無延遲的排隊

配置burst參數將會使通訊更流暢,但是可能會不太實用,因爲該配置會使站點看起來很慢。在上面的示例中,隊列中的第20個包需要等待2秒才能被轉發,此時返回給客戶端的響應可能不再有用。要解決這個情況,可以在burst參數後添加nodelay參數:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;

    proxy_pass http://my_upstream;
}

使用nodelay參數,Nginx仍將根據burst參數分配隊列中的位置,並應用已配置的速率限制,而不是清理隊列中等待轉發的請求。相反地,當一個請求到達“太早”時,只要在隊列中能分配位置,Nginx將立即轉發這個請求。將隊列中的該位置標記爲”taken”(佔據),並且不會被釋放以供另一個請求使用,直到一段時間後纔會被釋放(在這個示例中是,100毫秒後)。

假設如前所述,隊列中有20個空位,從給定的IP地址發出的21個請求同時到達。Nginx會立即轉發這個21個請求,並且標記隊列中佔據的20個位置,然後每100毫秒釋放一個位置。如果是25個請求同時到達,Nginx將會立即轉發其中的21個請求,標記隊列中佔據的20個位置,並且返回503狀態碼來拒絕剩下的4個請求。

現在假設,第一組請求被轉發後101毫秒,另20個請求同時到達。隊列中只會有一個位置被釋放,所以Nginx轉發一個請求並返回503狀態碼來拒絕其他19個請求。如果在20個新請求到達之前已經過去了501毫秒,5個位置被釋放,所以Nginx立即轉發5個請求並拒絕另外15個。

效果相當於每秒10個請求的“流量限制”。如果希望不限制兩個請求間允許間隔的情況下實施“流量限制”,nodelay參數是很實用的。

注意:對於大部分部署,我們建議使用burst和nodelay參數來配置limit_req指令。

高級配置示例

通過將基本的“流量限制”與其他Nginx功能配合使用,我們可以實現更細粒度的流量限制。

白名單

下面這個例子將展示,如何對任何不在白名單內的請求強制執行“流量限制”:


geo $limit {
    default         1;
    10.0.0.0/8         0;
    192.168.0.0/64     0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;

server {
    location / {
        limit_req zone=req_zone burst=10 nodelay;

        # ...
    }
}

這個例子同時使用了geo和map指令。geo塊將給在白名單中的IP地址對應的$limit變量分配一個值0,給其它不在白名單中的分配一個值1。然後我們使用一個映射將這些值轉爲key,如下:

  • 如果變量的值是,limit_key變量將被賦值爲空字符串
  • 如果變量的值是,limit_key變量將被賦值爲客戶端二進制形式的IP地址 兩個指令配合使用,白名單內IP地址的$limit_key變量被賦值爲空字符串,不在白名單內的被賦值爲客戶端的IP地址。當limit_req_zone後的第一個參數是空字符串時,不會應用“流量限制”,所以白名單內的IP地址(10.0.0.0/8和192.168.0.0/24 網段內)不會被限制。其它所有IP地址都會被限制到每秒5個請求。

limit_req指令將限制應用到/的location塊,允許在配置的限制上最多超過10個數據包的突發,並且不會延遲轉發。

另外,如果您正在學習Spring Cloud,推薦一個連載多年還在繼續更新的免費教程:https://blog.didispace.com/spring-cloud-learning/

location包含多limit_req指令

我們可以在一個location塊中配置多個limit_req指令。符合給定請求的所有限制都被應用時,意味着將採用最嚴格的那個限制。例如,多個指令都制定了延遲,將採用最長的那個延遲。同樣,請求受部分指令影響被拒絕,即使其他指令允許通過也無濟於事。

擴展前面將“流量限制”應用到白名單內IP地址的例子:


http {
    # ...

    limit_req_zone $limit_key zone=req_zone:10m rate=5r/s;
    limit_req_zone $binary_remote_addr zone=req_zone_wl:10m rate=15r/s;

    server {
        # ...
        location / {
            limit_req zone=req_zone burst=10 nodelay;
            limit_req zone=req_zone_wl burst=20 nodelay;
            # ...
        }
    }
}

白名單內的IP地址不會匹配到第一個“流量限制”,而是會匹配到第二個req_zone_wl,並且被限制到每秒15個請求。不在白名單內的IP地址兩個限制能匹配到,所以應用限制更強的那個:每秒5個請求。

配置相關功能

日誌記錄 默認情況下,Nginx會在日誌中記錄由於流量限制而延遲或丟棄的請求,如下所示:

2015/06/13 04:20:00 [error] 120315#0: *32086 limiting requests, excess: 1.000 by zone "mylimit", client: 192.168.1.2, server: nginx.com, <br>request: "GET / HTTP/1.0", host: "nginx.com"

日誌條目中包含的字段:

  • limiting requests - 表明日誌條目記錄的是被“流量限制”請求
  • excess - 每毫秒超過對應“流量限制”配置的請求數量
  • zone - 定義實施“流量限制”的區域
  • client - 發起請求的客戶端IP地址
  • server - 服務器IP地址或主機名
  • request - 客戶端發起的實際HTTP請求
  • host - HTTP報頭中host的值

默認情況下,Nginx以error級別來記錄被拒絕的請求,如上面示例中的[error]所示(Ngin以較低級別記錄延時請求,一般是info級別)。如要更改Nginx的日誌記錄級別,需要使用limit_req_log_level指令。這裏,我們將被拒絕請求的日誌記錄級別設置爲warn:

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_log_level warn;
    
    proxy_pass http://my_upstream;
}

發送到客戶端的錯誤代碼

一般情況下,客戶端超過配置的流量限制時,Nginx響應狀態碼爲503(Service Temporarily Unavailable)。可以使用limit_req_status指令來設置爲其它狀態碼(例如下面的444狀態碼):

location /login/ {
    limit_req zone=mylimit burst=20 nodelay;
    limit_req_status 444;
}

指定location拒絕所有請求

如果你想拒絕某個指定URL地址的所有請求,而不是僅僅對其限速,只需要在location塊中配置deny all指令:

location /foo.php {
    deny all;
}

總結

前文已經涵蓋了Nginx和Nginx Plus提供的“流量限制”的很多功能,包括爲HTTP請求的不同loation設置請求速率,給“流量限制”配置burst和nodelay參數。還涵蓋了針對客戶端IP地址的白名單和黑名單應用不同“流量限制”的高級配置,闡述瞭如何去日誌記錄被拒絕和延時的請求。


往期推薦



SpringBoot實現登錄攔截的原理

keepalived實現Nginx雙機熱備

一行代碼讓你擺脫U盤完成局域網文件傳輸

我把跨境當副業,一個星期賺了7000塊”:想給有夢想的人提個醒!!!

Docker簡易搭建 ElasticSearch 集羣

這個基於jedis的Redis工具類可以收藏一下

ZooKeeper實現分佈式隊列、分佈式鎖和選舉,很詳細

Spring SchedulingConfigurer 實現動態定時任務

從原則、方案、策略及難點闡述分庫分表

(建議收藏)服務器宕機了不要慌,這樣排查效率高

DAMS 2021:BATJ、美團等大廠和工行、農行等大型銀行如何最大化挖掘數據價值?


本文分享自微信公衆號 - 俠夢的開發筆記(xmdevnote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章