NGINX上的限流(譯)

本文是對Rate Limiting with NGINX and NGINX Plus的主要內容(去掉了關于NGINX Plus相關內容)的翻譯。

限流(rate limiting)是NGINX眾多特性中最有用的,也是經常容易被誤解和錯誤配置的,特性之一。該特性可以限制某個用戶在一個給定時間段內能夠產生的HTTP請求數。請求可以簡單到就是一個對于主頁的GET請求或者一個登陸表格的POST請求。

限流也可以用于安全目的上,比如減慢暴力密碼破解攻擊。通過限制進來的請求速率,并且(結合日志)標記出目標URLs來幫助防范DDoS攻擊。一般地說,限流是用在保護上游應用服務器不被在同一時刻的大量用戶請求湮沒。

下面介紹NGINX限流的基本用法。

NGINX限流是如何工作的

NGINX限流使用漏桶算法(leaky bucket algorithm),該算法廣泛應用于通信和基于包交換計算機網絡中,用來處理當帶寬被限制時的突發情況。和一個從上面進水,從下面漏水的桶的原理很相似;如果進水的速率大于漏水的速率,這個桶就會發生溢出。


圖片來自于NGINX BLOG

在請求處理過程中,水代表從客戶端來的請求,而桶代表了一個隊列,請求在該隊列中依據先進先出(FIFO)算法等待被處理。漏的水代表請求離開緩沖區并被服務器處理,溢出代表了請求被丟棄并且永不被服務。

配置基本的限流功能

有兩個主要的指令可以用來配置限流:limit_req_zonelimit_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 在它出現的環境中啟用了限流(在上面的例子中,作用在所有對于/login/的請求上),則limit_req_zone指令定義了限流的參數。

limit_req_zone指令一般定義在http塊內部,使得該指令可以在多個環境中使用。該指令有下面三個參數:

  • Key — 在限流應用之前定義了請求的特征。在上面例子中,它是$binary_remote_addr(NGINX變量),該變量代表了某個客戶端IP地址的二進制形式。這意味著我們可以將每個特定的IP地址的請求速率限制為第三個參數所定義的值。(使用這個變量的原因是因為它比用string代表客戶端IP地址的$remote_addr變量消耗更少的空間。)

  • Zone — 定義了存儲每個IP地址狀態和它訪問受限請求URL的頻率的共享內存區域。將這些信息保存在共享內存中,意味著這些信息能夠在NGINX工作進程之間共享。定義有兩個部分:由zone=關鍵字標識的區域名稱,以及冒號后面的區域大小。約16000個IP地址的狀態信息消耗1M內存大小,因此我們的區域(zone)大概可以存儲約160000個地址。
    當NGINX需要添加新的記錄時,如果此時存儲耗盡了,最老的記錄會被移除。如果釋放的存儲空間還是無法容納新的記錄,NGINX返回503 (Service Temporarily Unavailable)狀態碼。此外,為了防止內存被耗盡,每次NGINX創建一個新的記錄的同時移除多達兩條前60秒內沒有被使用的記錄。

  • Rate — 設置最大的請求速率。在上面的例子中,速率不能超過10個請求每秒。NGINX事實上可以在毫秒級別追蹤請求,因此這個限制對應了1個請求每100毫秒。因為我們不允許突刺(bursts,短時間內的突發流量,詳細見下一部分。),這意味著如果某個請求到達的時間離前一個被允許的請求小于100毫秒,它會被拒絕。

limit_req_zone指令設置限流和共享內存區域的參數,但是該指令實際上并不限制請求速率。為了限制起作用,需要將該限制應用到某個特定的locationserver塊(block),通過包含一個limit_req指令的方式。在上面的例子中,我們將請求限制在/login/上。

所以現在對于/login/,每個特定的IP地址被限制為10個請求每秒— 或者更準確地說,不能在與前一個請求間隔100毫秒時間內發送請求。

處理流量突刺(Bursts)

如果在100毫秒內得到2個請求會怎么樣?對于第2個請求,NGINX返回503狀態碼給客戶端。這可能不是我們想要的,因為事實上,應用是趨向于突發性的。相反,我們想要緩存任何過多的請求并且及時地服務它們。下面是我們使用limit_reqburst參數來更新配置:

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

      proxy_pass http://my_upstream;
}

burst參數定義了一個客戶端能夠產生超出區域(zone)規定的速率的請求數量(在我們示例mylimit區域中,速率限制是10個請求每秒,或1個請求每100毫秒)。一個請求在前一個請求后的100毫秒間隔內達到,該請求會被放入一個隊列,并且該隊列大小被設置為20.

這意味著如果從某個特定IP地址來的21個請求同時地達到,NGINX立即轉發第一個請求到上游的服務器組,并且將剩余的20個請求放入隊列中。然后,NGINX每100毫秒轉發一個隊列中的請求,并且只有當某個新進來的請求使得隊列中的請求數目超過了20,則返回503給客戶端。

無延遲排隊

帶有burst的配置產生平滑的網絡流量,但是不實用,因為該配置會使得你的網站表現的很慢。在上面的例子中,隊列中第20個數據包等待2秒才能被轉發,這時該數據包的響應可能對于客戶端已經沒有了意義。為了處理這種情況,除了burst參數外,添加nodelay參數。

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

      proxy_pass http://my_upstream
}

帶有nodelay參數,NGINX仍然會按照burst參數在隊列中分配插槽(slot)以及利用已配置的限流,但是不是通過間隔地轉發隊列中的請求。相反,當某個請求來的太快,只要隊列中有可用的空間(slot),NGINX會立即轉發它。該插槽(slot)被標記為“已使用”,并且不會被釋放給另一個請求,一直到經過適當的時間(在上面的例子中,是100毫秒)。

像之前一樣假設有20個插槽的隊列是空的,并且來自于給定的IP地址的21個請求同時地到達。NGINX立即轉發這21個請求以及將隊列中的20個插槽標記為“已使用”,然后每隔100毫秒釋放一個插槽。(相反,如果有25個請求,NGINX會立即轉發25個中的21個請求,標記20個插槽為“已使用”,并且用503狀態拒絕4個請求。)

現在假設在轉發第一個請求集合之后的101毫秒,有另外的20個請求同時地到達。隊列中只有1個插槽被釋放,因此NGINX轉發1個請求,并且用503狀態拒絕其它的19個請求。相反,如果在這20個新請求到達之前過去了501毫秒,則有5個插槽被釋放,因此NGINX立即轉發5個請求,并且拒絕其它15個請求。

效果等同于10個請求每秒的限流。如果你想利用請求之間的無限制性間隔的限流,nodelay選項則是非常有用的。

注意:對于大多數的部署,我們推薦在limit_req指令中包含burstnodelay參數。

高級設置的例子

通過結合基本的限流和其它的NGINX特性,你可以實現更多的細微的流量限制。

白名單

下面的例子展示了如何將限流作用在任何一個不在“白名單”中的請求上。

geo $limit {
        default 1;
        10.0.0.0/8 0;
        192.168.0.0/24 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;
                
                # ...
        }
}

這個例子同時使用了geomap指令。對于IP地址在白名單中的,geo塊分配0值給$limit;其它所有不在白名單中的IP地址,分配1值。然后我們使用一個map去將這些值映射到某個key中,例如:

  • 如果$limit0$limit_key被設置為空字符串
  • 如果$limit1$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指令將限制作用在/定位中,并且允許在沒有轉發延遲的情況下,轉發多達10個數據包。

在一個定位中包含多個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),但是能匹配第二個(req_zone_wl),因此這些IP地址被限制為15個請求每秒。不在白名單上的IP地址兩個限流都能匹配上,因此最嚴格的那個限流起作用:5個請求每秒。

配置相關的特性

日志(Logging)

默認,NGNIX記錄由于限流導致的延遲或丟棄的請求的日志,如下面的例子:

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, request: "GET / HTTP/1.0", host: "nginx.com"

該日志記錄包含的字段:

  • limiting requests — 日志條目記錄了某個限流的標志
  • excess — 超過這個請求代表的配置的速率的每毫秒請求數目
  • zone — 定義了啟用了限流的區域
  • client — 產生請求的客戶端IP地址
  • server — 服務器的IP地址或主機名
  • request — 客戶端產生的實際的HTTP請求
  • host — HTTP頭部主機名的值

默認,NGINX日志在error級別拒絕請求,如上面例子中的[error]所示。(它在低一個級別上記錄延遲的請求,因此默認是info。)用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;
}
拒絕對特定位置的所有請求

如果你想拒絕對于某個特定URL的所有請求,而不是僅僅的限制它們,可以為這個URL配置一個location塊,并且在其中包含deny all指令:

location /foo.php {
      deny all;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。