Nginx

Rate limit

Một trong những tính năng rất hữu ích nhưng lại hay bị hiểu nhầm hoặc cấu hình sai trong nginx là rate limiting - giới hạn tốc độc truy cập. Nó cho phép giới hạn lượng truy vấn HTTP đến máy chủ mà người dùng có thể tạo ra trong một khoảng thời gian. Truy cập ở đây có thể hiểu là những truy cập GET bình thường hoặc POST ở các form HTML.

Giới hạn tốc độ truy cập có thể dùng trong mục đích an ninh, ví dụ ngăn chặn người dùng dò mật khẩu, cứ hình dung mình chỉ cho truy cập 5 lần 1s thì dò đến mai. Nó cũng có tác dụng trong việc ngăn chặn DDoS bằng cách giới hạn tốc độ đến mức của một người dùng thường (ví dụ đo đạc thời điểm bình thường 1 IP chỉ truy cập tối đa là 10 lần 1s, vậy mình đặt giới hạn trên mức này một chút) và xác địch địa chỉ URL bị tấn công. Túm lại là giới hạn sẽ giúp máy chủ không bị quá tải mới quá nhiều truy cập đến từ một người dùng, dành đất cho những người dùng khác.

Cái này nó làm việc thế nào?

Nó dựa theo thuật toán cái xô thủng. Đại khái là có một cái xô, đục nhiều lỗ để nước có thể chảy qua. Nếu nước được rót từ trên xuống quá nhanh, không chảy kịp thì nó sẽ bị tràn.Cái xô này là hàng đợi chờ xử lý, nước rò ra khỏi xô là các truy vấn được xử lý, còn phần tràn ra là phần bị bỏ đi, không được phục vụ. Moá, nghe thế này nông dân cũng làm được :D

Cấu hình cơ bản giới hạn tốc độ trong nginx

Giới hạn tốc độ được cấu hình với 2 chỉ lệnh limit_req_zonelimit_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 định nghĩa các tham số cho việc giới hạn tốc độ. Trong khi đó limit_req chỉ định chỗ nào sẽ được áp dụng giới hạn. Ở ví dụ trên là /login/. limit_req_zone được khai báo trong khối http, nên nó có thể dùng được ở nhiều phạm vi khác nhau. Nó có 3 tham số:

  • Key - Định nghĩa thuộc tính của truy cập mà sẽ được áp dụng giới hạn. Ví dụ $binary_remote_addr đại diện cho IP của người dùng. Nó có nghĩa là mới mỗi IP của người dùng sẽ bị giới hạn theo định nghĩa ở tham số thứ 3. Cái này thì cũng giống $remote_addr nhưng nó ở dạng nhị phân nên tốn ít không gian hơn.

  • Zone - Định nghĩa một vùng bộ nhớ chia sẻ để lưu trạng thái của mỗi địa chỉ IP và số lần nó truy cập đến địa chỉ URL bị giới hạn. Do nó được lưu ở vùng nhớ chung nên có thể được dùng bởi các tiến trình nginx. Cái này chia thành 2 phần, tên của vùng nhớ xác định bằng từ khoá zone=, và dung lượng được định nghĩa sau dấu :. Bình thường khoảng 16000 địa chỉ IP sẽ tốn khoảng 1MB, như vậy với tham số trong ví dụ, chúng ta có thể lưu được trạng thái của 160000 địa chỉ IP. Nếu vùng nhớ này bị đầy, mà nginx vẫn muốn thêm thông tin của các địa chỉ IP mới, IP cũ nhất sẽ bị xoá đi. Nếu không gian được giải phóng vẫn không đủ để tiếp nhận các bản ghi mới, nginx sẽ trả về mã lỗi 503 (Service Temporarily Unavailable). Để phòng tránh việc này, mỗi lần nginx thêm mục mới, nó sẽ thực hiện xoá 2 mục không dùng tới trong vòng 60s.

  • Rate - Đặt tốc độ truy cật lớn nhất, như ở ví dụ, tốc độ lớn nhất không vượt quá 10 truy cập 1s. Nginx thực tế theo dõi truy cập theo ms, giới hạn trên thực tế là 1 truy cập trong vòng 100ms. Truy cập sẽ bị loại bỏ nếu xuất hiện trong vòng 100ms từ lần truy cập trước.

Chỉ lệnh limit_req_zone đặt tham số cho việc giới hạn tốc độ và định nghĩa vùng nhớ chung, nhưng nó không thực hiện việc giới hạn. Chúng ta cần áp dụng nó vào một location cụ thể hoặc khối server bằng việc thêm chỉ lệnh limit_req. Như trong ví dụ chúng ta giới hạn tốc độ truy cập đến /login/.

Như vậy với mỗi địa chỉ IP, chúng ta có chỉ có thể truy cập nhiều nhất 10 lần 1s đến /login/, hoặc chúng ta không thể truy cập tới URL này trong vòng 100ms sau lần truy cập trước.

Xử lý bùng nổ (éo biết dịch thế nào)

Đại khái là điều gì xảy ra với cái truy cập bị loại bỏ? Ví dụ trong vòng 100ms chúng ta gửi 2 truy cập, thì truy cập thứ 2 sẽ trả về 503 cho người dùng. Cái này có vẻ không hay lắm, vì người dùng sẽ thấy ứng dụng rất là lởm. Thay vì đó chúng ta muốn lưu lại các truy cập này để xử lý chúng trong thời gian thích hợp, kiểu cho vào hàng đợi chứ không từ chối vội. Đây là lúc chúng ta sử dụng tham số burst trong limit_req, cập nhật cấu hình theo như sau:

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

Tham số burst định nghĩa số lượng truy cập người dùng có thể vượt so với giới hạn đã định. Theo ví dụ thì truy cập đến trong vòng 100ms sau truy cập trước đó sẽ được đưa vào hàng đợi, và chúng ta đang đặt cỡ của hàng đợi là 20 cái. Nó có nghĩa là nếu chúng ta gửi 21 truy cập đến địa chỉ /login/ trong vòng 100ms thì cái đầu tiên sẽ được xử lý ngay lập tức, 20 cái còn lại sẽ được gửi vào hàng đợi. Sau đó nó sẽ xử lý hàng đợi mỗi 100ms, mã lỗi 503 chỉ được trả về nếu truy cập đến quá nhanh và hàng đợi vượt quá 20.

Hàng đợi với No Delay (Jétstar airline à :D)

Một cấu hình với burst mong đợi một luồng truy cập mượt mà, không bị trả về lỗi nếu vượt quá giới hạn một chút, nhưng không thực tế lắm vì nó sẽ làm trang web của bạn có vẻ chậm đi. Trong ví dụ ở trên, truy cập thứ 20 trong hàng đợi sẽ phải chờ 20 * 100ms = 2s mới đến lượt, điều này nhiều khi trở nên vô ích với người dùng. Để xử lý tình huống này, thêm tham số nodelay cùng với 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 phân bổ các vị trí trong hàng đợi theo tham số burst và áp đặt giới hạn tốc độ đã được cấu hình, nhưng nó không làm ảnh hướng đến phần xử lý những truy cập trong hàng đợi. Thay vì đó nếu một truy cập đến quá sớm, NGINX chuyển nó đi ngay lập tức như là có một vị trí của nó trong hàng đợi. Nó đánh dấu vị trí này đã bị lấy trong hàng đợi và giữ nó đến khi khoảng thời gian giới hạn trôi qua (trong ví dụ này là 100ms).

Như lúc trước chúng ta giả sử có 20 vị trí trong hàng đợi và có 21 truy cập đồng thời đến từ một địa chỉ IP. NGINX chuyển tất cả 21 truy cập này ngay lập tức và đánh dấu 20 vị trí trong hàng đợi là “đã bị lấy”, sau đó làm trống mỗi vị trí sau 100ms. Nếu có 25 truy cập tương tự đến cùng lúc, NGINX sẽ xử lý ngay lập tức 21 truy cập, đánh dấu 20 vị trí trong hàng đợi là đã bị lấy, và từ chối 4 truy cập còn lại với trạng thái 503.

Giả sử sau 101ms sau khi lượng truy cập đầu tiên đổ vào, 20 truy cập đồng thời tiếp tục đến. Chỉ có 1 vị trí trong hàng đợi được làm trống, vì vậy NGINX xử lý 1 truy cập và từ chối 19 truy cập còn lại với trạng thái 503. Vào khoảng thời gian 501ms sau đợt truy cập vừa xong, 20 lượt truy cập đồng thời mới lại tới, có 5 vị trí trống nên NGINX sẽ xử lý 5 truy cập mới và từ chối 15 cái còn lại.

Hiệu ứng này tương tự với việc giới hạn 10 truy cập trong 1s. Tham số nodelay hữu ích khi chúng ta muốn áp đặt giới hạn truy cập mà không muốn bị chờ giữa các truy cập.

Tham số burstnodelay được khuyến khích dùng trong quá trình triển khai giới hạn truy cập.

Các ví dụ nâng cao

Kết hợp giới hạn tốc độ truy cập với các tính năng khác của NGINX chúng ta có thể tạo ra nhiều kiểu giới hạn truy cập khác nhau.

Whitelisting

Ví dụ bên dưới mô tả cách giới hạn tốc độ truy cập từ bất kì ai, trừ những người trong whitelist

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ụ trên có sử dụng chỉ lệnh geomap. Khối geo gán 0 cho các địa chỉ trong whitelist và gán 1 cho các địa chỉ IP khác vào biến $limit. Sau đó chúng ta dùng map để chuyển đổi các giá trị này thành các $limit_key:

  • Nếu $limit=0, $limit_key được để trống

  • Nếu $limit=1, $limit_key là địa chỉ IP của người dùng theo định dạng nhị phân Tổng hợp lại thì ta sẽ có $limit_key được để trống nếu truy cập đến từ các địa chỉ trong whitelist (10.0.0.0/8, 192.168.0.0/24), và $limit_key là IP người dùng trong các trường hợp còn lại. Với $limit_key để trống trong chỉ lệnh limit_req_zone, giới hạn tốc độ sẽ không được áp dụng, vì vậy các địa chỉ IP trong whitelist có thể truy cập thoải mái, trong khi đó các địa chỉ IP khác sẽ bị giới hạn 5 truy cập trong 1s. Chỉ lệnh limit_req ở trên áp dụng ở vị trí / và cho phép vượt qua thới hạn đã đặt 10 truy cập với tham số nodelay đã nói ở trên.

Sử dụng nhiều chỉ lệnh limit_req trong một vị trí

Chúng ta có thể sử dụng nhiều chỉ lệnh limit_req trong một vị trí. Tất cả giới hạn phụ hợp với truy cập đều được áp dụng, điều này có nghĩa là giới hạn nào chặt hơn thì được dùng. Ví dụ, nếu nhiều chỉ lệnh với độ trễ được đưa ra thì độ trễ dài nhất được áp dụng. Để cho dễ hình dung thì truy cập bị từ chối nếu nó bị vào trong bất cứ chỉ lệnh limit_req nào, ví dụ có 3 giới hạn, chỉ cần 1 giới hạn đạt tới là truy cập bị từ chối, mặc dù 2 giới hạn còn lại cho phép nó chạy qua.

Mở rộng ví dụ trên chúng ta có thể đặt một giới hạn tốc độ truy cập riêng cho các IP tron whitelist:

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;
            # ...
        }
    }
}

Các địa chỉ IP trong whitelist không khớp với giới hạn đầu tiên (req_zone) nhưng lại khớp với giới hạn bên dưới (req_zone_wl) nên bị giới hạn 15 truy cập 1s. Các IP không trong whitelist khớp với cả 2 giới hạn nên giới hạn chặt hơn sẽ được sử dụng, ở đây là req_zone với 5 truy cập 1s.

Cấu hình các thành phần liên quan

Logging

Mặc định NGINX ghi lại các truy cập bị trễ hoặc từ chối, kiểu:

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"

Các trường bao gồm:

  • limiting requests - Chỉ ra rằng phần này ghi lại một truy cập bị giới hạn

  • excess - Số request mỗi ms theo như tốc độ giới hạn được cấu hình

  • client - Địa chỉ IP của người dùng

  • server - Địa chỉ IP hoặc hostname của máy chủ

  • request - Truy cập HTTP đến từ người dùng

  • host - Giá trị của biến host trong HTTP Header

Mặc định, NGINX ghi lại những truy cập bị từ chối ở mức độ error như ở ví dụ trên. Để thay đổi logging level, sử dụng chỉ lệnh limit_req_log_level. Chúng ta đặt chế độ log ở mức độ warn như sau:

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


    proxy_pass http://my_upstream;
}

Mã lỗi trả về cho người dùng

Mặc định, NGINX trả về mã trạng thái 503 (Service Temporarily Unavailable) khi người dùng truy cập quá tốc độ cho phép. Sử dụng chỉ lệnh limit_req_status để đặt mã trạng thái khác nếu muốn. Ví dụ chúng ta muốn chuyển sang mã 444:

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

Từ chối mọi truy cập tại một vị trí

Nếu muốn chặn truy cập đến một vị trí nào đó, thay vì sử dụng giới hạn tốc độ, chúng ta dùng khối location để tạo một vị trí và thêm chỉ lệnh deny all:

location /foo.php {
    deny all;
}

Tổng kết

Túm lại là với mấy cái này mình đã biết cách giới hạn tốc độ truy cập của người dùng. Chúng ta có thể đặt nhiều giới hạn cho từng vị trí khác nhau. Ngoài ra chúng ta có thêm 2 tham số hữu ích là burstnodelay. Chúng ta cũng có thể đặt whitelistblacklist cho từng tập địa chỉ IP sau đó áp dụng các giới hạn khác nhau. Ngoài ra là mấy cái lặt vặt như log, đổi mã trạng thái,…

Last updated