¡Sí, por supuesto que es posible en NGINX!
Lo que podría hacer es implementar el siguiente DFA :
Implemente la limitación de velocidad, basada en $http_referer
, posiblemente, utilizando algunas expresiones regulares a través de a map
para normalizar los valores. Cuando se excede el límite, se abre una página de error interno, que puede atrapar a través de un error_page
controlador según una pregunta relacionada , yendo a una nueva ubicación interna como una redirección interna (no visible para el cliente).
En la ubicación anterior para los límites excedidos, realiza una solicitud de alerta, permitiendo que la lógica externa realice la notificación; Esta solicitud se almacena en caché posteriormente, lo que garantiza que solo obtendrá 1 solicitud única por una ventana de tiempo determinada.
Capture el código de estado HTTP de la solicitud anterior (devolviendo un código de estado ≥ 300 y usando proxy_intercept_errors on
, o, alternativamente, use el no construido por defecto auth_request
o add_after_body
para hacer una subrequest "libre"), y complete la solicitud original como si el paso anterior no estuvo involucrado. Tenga en cuenta que debemos habilitar el error_page
manejo recursivo para que esto funcione.
Aquí está mi PoC y un MVP, también en https://github.com/cnst/StackOverflow.cnst.nginx.conf/blob/master/sf.432636.detecting-slashdot-effect-in-nginx.conf :
limit_req_zone $http_referer zone=slash:10m rate=1r/m; # XXX: how many req/minute?
server {
listen 2636;
location / {
limit_req zone=slash nodelay;
#limit_req_status 429; #nginx 1.3.15
#error_page 429 = @dot;
error_page 503 = @dot;
proxy_pass http://localhost:2635;
# an outright `return 200` has a higher precedence over the limit
}
recursive_error_pages on;
location @dot {
proxy_pass http://127.0.0.1:2637/?ref=$http_referer;
# if you don't have `resolver`, no URI modification is allowed:
#proxy_pass http://localhost:2637;
proxy_intercept_errors on;
error_page 429 = @slash;
}
location @slash {
# XXX: placeholder for your content:
return 200 "$uri: we're too fast!\n";
}
}
server {
listen 2635;
# XXX: placeholder for your content:
return 200 "$uri: going steady\n";
}
proxy_cache_path /tmp/nginx/slashdotted inactive=1h
max_size=64m keys_zone=slashdotted:10m;
server {
# we need to flip the 200 status into the one >=300, so that
# we can then catch it through proxy_intercept_errors above
listen 2637;
error_page 429 @/.;
return 429;
location @/. {
proxy_cache slashdotted;
proxy_cache_valid 200 60s; # XXX: how often to get notifications?
proxy_pass http://localhost:2638;
}
}
server {
# IRL this would be an actual script, or
# a proxy_pass redirect to an HTTP to SMS or SMTP gateway
listen 2638;
return 200 authorities_alerted\n;
}
Tenga en cuenta que esto funciona como se esperaba:
% sh -c 'rm /tmp/slashdotted.nginx/*; mkdir /tmp/slashdotted.nginx; nginx -s reload; for i in 1 2 3; do curl -H "Referer: test" localhost:2636; sleep 2; done; tail /var/log/nginx/access.log'
/: going steady
/: we're too fast!
/: we're too fast!
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.1" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:49 +0200] "GET / HTTP/1.0" 200 16 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 200 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:51 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET / HTTP/1.1" 200 19 "test" "curl/7.26.0"
127.0.0.1 - - [26/Aug/2017:02:05:53 +0200] "GET /?ref=test HTTP/1.0" 429 20 "test" "curl/7.26.0"
%
Puede ver que la primera solicitud da como resultado un hit de front-end y un backend, como se esperaba (tuve que agregar un backend ficticio a la ubicación que tiene limit_req
, porque return 200
tendría prioridad sobre los límites, un backend real no es necesario para el resto del manejo).
La segunda solicitud está por encima del límite, por lo tanto, enviamos la alerta (obteniendo 200
) y la almacenamos en caché, regresando 429
(esto es necesario debido a la limitación mencionada anteriormente de que las solicitudes por debajo de 300 no pueden ser capturadas), que posteriormente es capturado por el front-end , que ahora es libre de hacer lo que quiera.
La tercera solicitud aún excede el límite, pero ya hemos enviado la alerta, por lo que no se envía ninguna nueva.
¡Hecho! ¡No olvides bifurcarlo en GitHub!