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_zone
và limit_req
, ví dụ:
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ỗi503 (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:
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
:
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ố burst
và nodelay
đượ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
Ví dụ trên có sử dụng chỉ lệnh geo
và map
. 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ốngNế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ỉ trongwhitelist
(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ệnhlimit_req_zone
, giới hạn tốc độ sẽ không được áp dụng, vì vậy các địa chỉ IP trongwhitelist
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ệnhlimit_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í
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
:
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:
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:
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
:
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
:
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à burst
và nodelay
. Chúng ta cũng có thể đặt whitelist
và blacklist
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
Was this helpful?