Một trong những tính năng mạnh mẽ của NGINX nhưng thường bị hiểu sai hoặc cấu hình sai là “rate limiting”. NGINX rate limit cho phép bạn giới hạn số lượng HTTP request mà một user có thể gửi đến server trong một khoản thời gian nhất định. Một request có thể là một GET request đến home page, bài viết, hình ảnh, javascript hoặc là một POST request để login, submit dữ liệu…
Rate limit thường được dùng cho mục đích bảo mật, ví dụ như làm chậm quá trình tấn công brute-force, một kiểu tấn công dò mật khẩu. Nó còn rất hiệu quả trong chống DDoS bằng cách giới hạn tần số incoming request ở mức hợp lệ của những người dùng thật, những truy cập có tần số cao hơn con số này được xem là tấn công. Về tổng quan, nó giúp bảo vệ upstream application server (ứng dụng của chúng ta) không bị quá tải vì phải nhận quá nhiều request của user cùng một lúc.
Nội dung
Cơ chế hoạt động của NGINX rate limit
Tính năng rate limit của NGINX sử dụng thuật toán “leaky bucket”, một thuật toán được sử dụng rộng rãi để đối phó với vấn đề request tăng cao đột biến nhưng bandwidth thì bị giới hạn. Thuật toán này hoạt động tương tự như việc chúng ta có 1 cái xô (bucket), nước được đổ vào xô từ trên xuống và chảy ra ngoài (leak) bởi những cái lỗ ở đáy xô với tốc độ cố định. Nếu tốc độ đổ nước vào xô (incoming rate) cao hơn tốc độ nước chảy ra ngoài (outgoing rate) thì đến 1 lúc nào đó, nước trong xô sẽ đầy và tràn ra ngoài (overflow). Trong ngữ cảnh của xử lý request:
- “Nước” tượng trưng cho request của client
- “Xô” tượng trưng cho queue, nơi chứa các request đang đợi được xử lý, queue hoạt động theo thuật toán FIFO (First In – First Out), request nào đến trước sẽ được xử lý trước.
- “Nước chảy ra ở đáy xô” (leaking water) tượng trưng cho các request được lấy ra khỏi queue để server xử lý, tốc độ chảy ra của nước tượng trưng cho tần số (rate) mà ta quy định. Nếu nước đổ vào xô quá nhanh (client request quá nhanh) khiến xô bị đầy (full queue), phần nước chảy ra ngoài (overflow) tượng trưng cho các request sẽ bị lọai bỏ (discarded) và không bao giờ được phục vụ.
Cấu hình giới hạn tần số NGINX – NGINX rate limit
Rate limiting được cấu hình bằng 2 directives chính: limit_req_zone và limit_req, ví dụ:
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 khai báo các thông số cho tính năng rate limit, trong khi limit_req kích hoạt rate limit trong những khu vực cụ thể mà nó xuất hiện (trong ví dụ trên là áp dụng limit cho tất cả các request đến location /login/)
limit_req_zone directive được khi báo trong http block khi cấu hình NGINX, điều này giúp nó có thể được sử dụng ở nhiều context khác nhau. Directive này có 3 tham số:
- key – Khai báo đặc điểm của request sẽ được dùng làm key để tính toán tần số và apply limit. Ở ví dụ phía trên, key được sử dụng là biến $binary_remote_addr của NGINX, biến này chứa giá trị địa chỉ IP của client dưới dạng binary. Điều này có nghĩa, chúng ta sẽ giới hạn tần số truy cập của từng IP theo giá trị được khai báo ở tham số thứ ba (tham số rate). (Chúng ta sử dụng biến $binary_remote_addr vì biến này lưu trữ IP ở dạng binary, sử dụng ít bộ nhớ hơn biến $remote_addr).
- zone: khai báo shared memory zone dùng để lưu trữ trạng thái của từng IP address và tần số truy cập của chúng vào các URL đã apply limit. Lưu trữ các thông tin này trong shared memory cho phép chia sẻ dữ liệu giữa các NGINX worker, giúp cho việc tính toán limit được chính xác. Khi khai báo tham số này gồm 2 phần: phần zone name được khai báo bằng từ khóa zone=, và kích thước vùng nhớ được khai báo phía sau dấu “:”. Dữ liệu về trạng thái (state information) vào khoảng 16,000 IP sẽ chiếm 1 MB bộ nhớ, với khai báo như trên ví dụ (zone=mylimit:10m) ta có thể lưu trữ trạng thái của 160,000 IP.
- Trường hợp nếu toàn bộ shared memory của zone được sử dụng hết và NGINX muốn cần add entry mới, nó sẽ remove entry cũ nhất. Nếu bộ nhớ được giải phóng vẫn không đủ để chứa entrry mới, NGINX sẽ return code 503 (Service Temporary Unavailable). Thêm vào đó, để ngăn chặn tình trạng bộ nhớ bị xài cạn, mỗi khi NGINX tạo một entry mới, nó sẽ remove từ 0 đến 2 entry không được sử dụng trong 60 giây vừa qua.
- rate: set giới hạn tần số tối đa cho phép (maximum request rate). Trong ví dụ trên, rate không được phép vượt quá 10 request mỗi giây. NGINX theo dõi request với độ chính xác đến từng milisecond, có nghĩa limit trong ví dụ trên tương ứng với 1 request mỗi 100ms (1s = 1000 ms). Bởi vì chúng ta không cho phép “burst” (giải thích bên dưới), điều này có nghĩa request sẽ bị từ chối (reject) nếu nó đến server sớm hơn 100ms so với request đã được accept trước đó.
limit_req_zone directive set tham số cho rate limiting và shared memory zone, nhưng nó không thực hiện thao tác giới hạn tần số truy cập. Để thực hiện điều đó, ta cần apply limit vào những location hoặc server block cụ thể bằng cách khai báo limit_req directive ở đấy. Trong ví dụ trên, chúng ta giới hạn tần số truy cập vào location /login/.
Bây giờ, mỗi IP sẽ bị giới hạn tối đa 10 request mỗi giây cho /login/, hoặc chính xác hơn, không thể truy cập vào URL trên trong khoảng 100ms so với request trước đó.
Xử lý bursts – nhiều truy cập hợp lệ đến cùng lúc
Chuyện gì sẽ xảy ra nếu ta nhận được 2 đến cùng lúc trong khoảng 100ms? Request đầu tiên sẽ được chấp nhận, với request thứ 2, NGINX sẽ return code 503 về cho client. Đây chắc hẳn không phải là điều chúng ta mong muốn, bởi vì bản chất tự nhiên của ứng dụng là gửi nhiều request cùng 1 lúc (bursty in nature). Thay vào đó, chúng ta cần buffer những request đến sớm hơn giới hạn và phục vụ chúng sau đó. Đây là lúc chúng ta cần sử dụng tham số burst cho limit_req, update cấu hình như sau:
location /login/ {
limit_req zone=mylimit burst=20;
proxy_pass http://my_upstream;
}
Tham số burst khai báo số lượng request một client có thể thực hiện vượt quá rate được chỉ định cho một zone (trong ví dụ của chúng ta là mylimit zone, giới hạn là 10 request mỗi giây, tương ứng 1 request mỗi 100ms). Một request đến sớm hơn 100ms so với request trước đó sẽ được đưa vào queue, và ở đây, ta set queue size là 20.
Điều đó có nghĩa, nếu 21 request đến cùng lúc từ 1 IP, NGINX sẽ forward request đầu tiên đến upstream server group ngay lập tức và put 20 request còn lại vào queue. Sau đó, nó forward từng request trong queue sau mỗi 100ms, và return code 503 cho client chỉ khi số lượng request trong queue vượt quá con số 20.
Xử lý hàng đợi mà không delay request (Queueing with No Delay)
Việc cấu hình sử dụng burst giúp traffic flow trở nên mượt mà, tuy nhiên trong thực tế nó có thể khiến cho website của trông có vẻ bị chậm. Trong ví dụ của chúng ta, request thứ 20 trong queue phải đợi đến 2s mới được xử lý, mang lại trải nghiệm không tốt cho người dùng. Để giải quyết vấn đề này, sử dụng tham số nodelay bên cạnh tham số burst
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
proxy_pass http://my_upstream;
}
Với tham số nodelay, NGINX vẫn cấp (allocate) slots cho request ở trong queue theo đúng cấu hình của tham số burst và áp đặt rate limit, nhưng nó sẽ không giãn cách (không delay) việc forward các request ở trong queue. Thay vào đó, khi request đến “quá sớm”, NGINX forward request đến upstream application ngay lập tức, miễn là trong queue vẫn còn slot. Sau đó, NGINX sẽ đánh dấu (mark) slot đó là “đã sử dụng” (taken) và không cho các request khác sử dụng slot này cho đến khi thời gian tương ứng trôi qua (trong ví dụ của chúng ta là sau 100ms).
Giả sử giống như lần trước, ta đang có 20 slot trống trong queue và nhận được 21 request đến đồng thời từ một địa chỉ IP. NGINX sẽ forward 21 request này ngay lập tức và đánh dấu 20 slot trong queue là “đã dùng”, sau đó, NGINX sẽ giải phóng (free) 1 slot sau mỗi 100ms. Nếu giả sử có 25 request đến cùng lúc, NGINX sẽ forward 21 request, đánh dấu 20 slot là “đã dùng” và reject 4 request với status code 503.
Giả sử bây giờ là 101ms sau khi mớ request đầu tiên được forward và tiếp tục nhận được 20 request mới đến cùng lúc. Chỉ có 1 slot vừa được giải phóng, vì vậy, NGINX sẽ forward 1 request và reject 19 request còn lại với tatus code 503. Nếu thay vào đó, bây giờ là 501ms đã trôi qua trước khi nhận được 20 request mới, 5 slot đã được giải phóng và NGINX sẽ forward 5 request ngay lập tức và reject 15 request còn lại.
Hiệu ứng tạo ra cũng tương đương với việc limit 10 request/s, vì các request dù được forward ngay lập tức nhưng các slot trong queue được đánh dấu là đã sử dụng và không thể tiếp nhận vượt quá queue size. Tham số nodelay hữu ích trong trường hợp bạn muốn limit rate nhưng không ràng buộc sự giãn cách (spacing) giữa các request.
Lưu ý: trong phần lớn trường hợp, chúng ta nên sử dụng burst và nodelay đồng thời với directive limit_req
Giới hạn tần số hai tầng (Two-stage Rate Limiting)
Với NGINX Plus R17 hoặc NGINX Open Source bản 1.15.7, ta đã có thể cấu hình NGINX cho phép burst request để thích nghi với đặc điểm truy cập (request pattern) của các browser thông thường, và sau đó giới hạn những request đã vượt qua một ngưỡng ( ngưỡng A) nào đó, và nếu vượt trên một ngưỡng trên nữa (ngưỡng B), request sẽ bị reject. Trường hợp này được gọi là Two-stage rate limiting. Two-stage limiting được kích hoạt bằng cách khai báo tham số delay trong limit_req directive.
Để minh họa cho two-stage rate limiting, chúng ta sẽ cấu NGINX để bảo vệ một website bằng cách đặt giới hạn tần số truy cập ở mức 5 request/s. Website thường có 4-6 resource mỗi page, và không bao giờ có nhiều hơn 12 resource. Cấu hình cho phép burst tối đa 12 request, 8 request đầu tiên sẽ được xử lý ngay lập tức mà không bị delay. Delay sẽ được apply sau request thứ 8 để đảm bảo tần số 5 request/s. Sau 12 excessive requests, những request sau đó sẽ bị reject.
limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;
server {
listen 80;
location / {
limit_req zone=ip burst=12 delay=8;
proxy_pass http://website;
}
}
Tham số delay khai báo ngưỡng (point) mà ở đó, nằm trong burst size, các request vượt (excessive request) sẽ bị kiểm soát (delayed) để đáp ứng rate limit đã khai báo. Với cấu hình này, một kết nối của client thực hiện một loạt kết nối ở tần số 8 request/s sẽ được xử lý như sau
8 request đầu tiên (bằng giá trị khai báo của tham số delay) được xử lý mà không bị delay. 4 request tiếp theo (burst – delay) sẽ bị delay và tần số 5 request/s không bị vượt quá. 3 reques tiếp theo bị reject vì vượt quá burst size. Các request sau đó cũng bị delay.
Cấu hình nâng cao
Bằng cách kết hợp NGINX rate limit với các tính năng khác của NGINX cho phép bạn tạo ra những phương pháp limit traffic đa dạng.
Allowlisting
Ví dụ bên dưới cho thấy cách áp đặt giới hạn tần số request cho những ai không nằm trong “allowlist”.
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;
# ...
}
}
Ví dụ phía trên sử dụng 2 directive geo và map. geo block gán giá trị 0 vào biến $limit với các IP nằm trong allowlist và giá trị 1 cho các IP còn lại. Sau đó, chúng ta sử dụng map để chuyển đổi (translate) những giá trị đó thành key tương ứng:
- Nếu $limit là 0, $limit_key được set thành empty string
- Nếu $limit là 1, $limit_key được set thành IP của Client dưới định dạng binary
Kết hợp chúng lại, $limit_key sẽ được set thành empty string cho những IP nằm trong allowlist, và thành client IP address với những IP còn lại. Khi tham số đầu tiên của limit_req_zone directive (key) là một empty string, limit sẽ KHÔNG được áp dụng. Do đó, những IP nằm trong allowlist (subnet 10.0.0.0/8 và 192.168.0.0/24) sẽ không bị giới hạn. Tất cả những IP còn lại sẽ bị giới hạn ở tần số 5 request/s.
limit_req directive được áp dụng trên location / và cho phép burst tối đa 10 request và nodelay khi forward request trong queue.
Áp dụng nhiều limit_req directive trong một location
Ta có thể sử dụng nhiều limit_req directive trong cùng 1 location. Tất cả các limit match với request đều sẽ được apply, điều này có nghĩa limit chặt nhất sẽ được áp dụng. Ví dụ, nếu có nhiều hơn 1 directive áp đặt delay cho request, delay dài nhất sẽ được sử dụng. Tương tự, request sẽ bị reject nếu có directive nào đó match và reject request, kể cả trường hợp request đã được allow ở một directive khác.
Mở rộng ví dụ trên, ta có thể apply NGINX rate limit cho những IP nằm trong allowlist:
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;
# ...
}
}
}
Những IP nằm trong allowlist không match rate limit đầu tiên (req_zone) nhưng match rate limit thứ hai (req_zone_wl) và sẽ bị giới hạn 15 request/s. Những IP không nằm trong allowlist match cả 2 limit rate nên sẽ áp dụng limit chặt hơn: 5 request/s.
Cấu hình những tính năng liên quan
Logging
Mặc định, NGINX ghi log những request bị delay hoặc bị drop bởi rate limit, như ví dụ sau đây:
2021/03/20 16:35:18 [error] 2416158#0: *49232 limiting requests, excess: 2.000 by zone "antiddos", client: 192.168.1.10, server: vietnix.vn, request: "GET / HTTP/1.1", host: "vietnix.vn"
Những trường trong log entry bao gồm:
- 2021/03/20 16:35:18 – Thời gian log được ghi
- [error] – Severity level
- 2416158#0 – Process ID và Thead ID của NGINX worker, cách nhau bởi dấu #
- *49232 – ID của proxied connection bị giới hạn tần số
- limiting requests – cho biết log entry này ghi lại kết quả của rate limit
- excess – số lượng request trên mỗi miliseconds vượt quá tần số được cấu hình
- zone – tên zone đang dùng để áp dụng limit
- client – source IP của client
- server – IP address hoặc hostname của server
- request – HTTP request của client
- host – giá trị của Host HTTP header
Mặc định, NGINX ghi log những request bị reject ở error level, được thể hiện bằng [error] trong ví dụ bên dưới. NGINX log những delayed request ở log level thấp hơn, warn level. Để thay đổi logging level, sử dụng limit_req_log_level directive. Chúng ta sẽ cấu hình ghi log những request bị reject ở warn level:
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_log_level warn;
proxy_pass http://my_upstream;
}
Error Code trả về cho client
Mặc định, NGINX rate limit trả về code 503 (Service Temporarily Unavailable) khi một client vượt quá tần số quy định. Sử dụng limit_req_status directive để set status code khác (ví dụ 444):
location /login/ {
limit_req zone=mylimit burst=20 nodelay;
limit_req_status 444;
}
Block tất cả truy cập vào một location cụ thể
Nếu bạn muốn deny tất cả truy cập vào URL cụ thể, thay vì sử dụng limit, bạn có thể khai báo location riêng cho chúng và sử dụng deny all directive:
location /foo.php {
deny all;
}
Tổng kết
Chúng ta đã tìm hiểu qua nhiều tính năng của rate limiting mà NGINX hỗ trợ, kèm theo việc thiết lập giới hạn tần số truy cập cho những location khác nhau, cấu hình những tính năng mở rộng cho rate limiting như tham số burst và nodelay. Chúng ta cũng đã đi qua những tính năng cấu hình nâng cao để apply limit khác nhau cho các địa chỉ IP khác nhau như allowlisted và denylisted, hướng dẫn cách ghi log những request bị reject và request bị delay.
Biên dịch: Vietnix – Theo NGINX