Хабру катастрофически не хватает такого формата постов как "продолжение" или "дополнение". После написания статьи зачастую появляется материал, который хотелось бы добавить к сказанному, но update'ить статью, с её сроком жизни в 1-2 дня, бессмысленно, а писать в комментариях невозможно из-за объёма материала. В то же время этого материала может быть недостаточно для новой статьи, да и, в силу того, что он сильно перекликается с предыдущей статьёй, придется либо постоянно её цитировать, либо оставлять пробелы, подразумевая, что читатель понимает о чем идет речь.
В итоге дополнительный материал, местами более важный чем сама статья, копится, пылится в заметках и пропадает с концами.
Так бы случилось и с этой статьей, но недосказанность заставляет вернуться к теме, так как разбор вопроса "нужны ли теоретические знания порядка прохождения запроса на практике" может помочь избежать составления неработающих конфигов. Поэтому продолжим разговор.
Для того чтобы понять как возникают подобные ошибки пройдёмся по хронологии создания конфига.
Допустим, у нас был сайт с разделом, доступ в который осуществляется по платной подписке и, для особо жадных пользователей, мы отдаём 402-й HTTP код ответа. Тогда, в крайне упрощенном виде, начальная конфигурация имеет вид:
add_header "Content-Type" "text/html; charset=UTF-8" always;
log_format example_com '$request_uri\t$msec\t$status';
access_log /var/log/nginx/example_com.log example_com;
server {
listen *:80;
server_name .example.com;
location /pay {
return 402 'Pay: Money, money, money\n';
}
}
$ GET -S example.com/pay ; GET -S example.com/pay
GET http://example.com/pay
402 Payment Required
Pay: Money, money, money
GET http://example.com/pay
402 Payment Required
Pay: Money, money, money
$ tail -F /var/log/nginx/example_com.log
/pay 1626155551.311 402
/pay 1626155551.648 402
Эти пользователи-крохоборы оказались не только мелочными, но и мстительными и устроили DDoS-атаку в виде HTTP-флуда на данный раздел. Чтобы отсеять высокочастотные запросы администратор решает воспользоваться директивой limit_req и отсечь паразитные запросы частотностью свыше одного в секунду:
limit_req_zone $binary_remote_addr zone=limit402:20m rate=1r/s;
server {
listen *:80;
server_name .example.com;
location /pay {
limit_req zone=limit402 nodelay;
return 402 'Pay: Money, money, money\n';
}
}
Однако, ничего из ожидаемого не происходит:
$ GET -Sd example.com/pay ; GET -Sd example.com/pay
GET http://example.com/pay
402 Payment Required
GET http://example.com/pay
402 Payment Required
Как определены фазы в Nginx
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_PRECONTENT_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
Модуль ngx_http_limit_req_module исполняет свои функции на этапе NGX_HTTP_PREACCESS_PHASE, в то время как ngx_http_rewrite_module работает в фазе NGX_HTTP_REWRITE_PHASE, как следствие, директива return завершает обработку запроса и ход до PREACCESS-фазы уже не доходит.
Значит, для решения проблемы нам необходимо "поменять" фазы местами. Конечно, сделать мы этого не можем, но можем совершить перенаправление, частично повторив цикл прохождения фаз. Сделать это необходимо после этапа NGX_HTTP_PREACCESS_PHASE.
Фаза NGX_HTTP_POST_ACCESS_PHASE является служебной, в фазах NGX_HTTP_ACCESS_PHASE и NGX_HTTP_LOG_PHASE нет возможности выполнить перенаправление, значит, остаются только NGX_HTTP_CONTENT_PHASE и NGX_HTTP_PRECONTENT_PHASE. В последней фазе нам вполне подходит модуль ngx_http_try_files_module и его директива try_files:
limit_req_zone $binary_remote_addr zone=limit402:20m rate=1r/s;
server {
listen *:80;
server_name .example.com;
location /pay {
limit_req zone=limit402 nodelay;
try_files $uri @fallback;
}
location @fallback {
return 402 'Fallback: Money, money, money\n';
}
}
И теперь всё работает именно в том порядке, который нам требовался:
$ GET -S example.com/pay ; GET -S example.com/pay
GET http://example.com/pay
402 Payment Required
Fallback: Money, money, money
GET http://example.com/pay
503 Service Temporarily Unavailable
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx</center>
</body>
</html>
Вспоминая из манула, о том, что в следующем примере директива try_files
location / {
try_files $uri @fallback;
}
аналогична директивам
location / {
error_page 404 = @fallback;
log_not_found off;
}
можем привести наш конфиг к более "популярному" виду:
limit_req_zone $binary_remote_addr zone=limit402:20m rate=1r/s;
server {
listen *:80;
server_name .example.com;
location /pay {
limit_req zone=limit402 nodelay;
error_page 404 = @fallback;
log_not_found off;
}
location @fallback {
return 402 'Fallback: Money, money, money\n';
}
}
$ GET -Sd example.com/pay ; GET -Sd example.com/pay
GET http://example.com/pay
402 Payment Required
GET http://example.com/pay
503 Service Temporarily Unavailable
Всё также работает, как и требовалось, но, для острастки, убедимся в отладчике, что мы не ошиблись в наших размышлениях:
debug
$ grep -E '(limit|HTTP|using|finalize|phase)' /var/log/nginx/error.log | grep -vF generic
2021/07/13 09:59:39 [debug] 38383#0: *15 http request line: "GET /pay HTTP/1.1"
2021/07/13 09:59:39 [debug] 38383#0: *15 rewrite phase: 1
2021/07/13 09:59:39 [debug] 38383#0: *15 using configuration "/pay"
2021/07/13 09:59:39 [debug] 38383#0: *15 rewrite phase: 3
2021/07/13 09:59:39 [debug] 38383#0: *15 post rewrite phase: 4
2021/07/13 09:59:39 [debug] 38383#0: *15 limit_req[0]: 0 0.000
2021/07/13 09:59:39 [debug] 38383#0: *15 access phase: 8
2021/07/13 09:59:39 [debug] 38383#0: *15 access phase: 9
2021/07/13 09:59:39 [debug] 38383#0: *15 access phase: 10
2021/07/13 09:59:39 [debug] 38383#0: *15 post access phase: 11
2021/07/13 09:59:39 [debug] 38383#0: *15 content phase: 14
2021/07/13 09:59:39 [debug] 38383#0: *15 content phase: 15
2021/07/13 09:59:39 [debug] 38383#0: *15 content phase: 16
2021/07/13 09:59:39 [debug] 38383#0: *15 content phase: 17
2021/07/13 09:59:39 [debug] 38383#0: *15 http finalize request: 404, "/pay?" a:1, c:1
2021/07/13 09:59:39 [debug] 38383#0: *15 using location: @fallback "/pay?"
2021/07/13 09:59:39 [debug] 38383#0: *15 rewrite phase: 3
2021/07/13 09:59:39 [debug] 38383#0: *15 HTTP/1.1 402 Payment Required
2021/07/13 09:59:39 [debug] 38383#0: *15 http write filter limit 0
2021/07/13 09:59:39 [debug] 38383#0: *15 http finalize request: 0, "/pay?" a:1, c:2
2021/07/13 09:59:39 [debug] 38383#0: *15 http finalize request: -4, "/pay?" a:1, c:1
2021/07/13 09:59:39 [debug] 38383#0: *16 http request line: "GET /pay HTTP/1.1"
2021/07/13 09:59:39 [debug] 38383#0: *16 rewrite phase: 1
2021/07/13 09:59:39 [debug] 38383#0: *16 using configuration "/pay"
2021/07/13 09:59:39 [debug] 38383#0: *16 rewrite phase: 3
2021/07/13 09:59:39 [debug] 38383#0: *16 post rewrite phase: 4
2021/07/13 09:59:39 [debug] 38383#0: *16 limit_req[0]: -3 0.650
2021/07/13 09:59:39 [error] 38383#0: *16 limiting requests, excess: 0.650 by zone "limit402", client: 12.34.56.78, server: example.com, request: "GET /pay HTTP/1.1", host: "example.com"
2021/07/13 09:59:39 [debug] 38383#0: *16 http finalize request: 503, "/pay?" a:1, c:1
2021/07/13 09:59:39 [debug] 38383#0: *16 HTTP/1.1 503 Service Temporarily Unavailable
2021/07/13 09:59:39 [debug] 38383#0: *16 http write filter limit 0
2021/07/13 09:59:39 [debug] 38383#0: *16 http finalize request: 0, "/pay?" a:1, c:1
Первый запрос с limit_req[0]: 0 0.000 был перенаправлен из локейшина /pay в @fallback, затем он совершил возврат в REWRITE-фазу и завершился с 402-м ответом. Второй, не уложившись в одну секунду (limit_req[0]: -3 0.650) закончился 503-м кодом без какого-либо перенаправления.
На сим всё. До следующего продолжения.
DDoSInsurance
try_files описана в ngx_http_core_module
simpleadmin Автор
Документация устарела. По факту сейчас (для версии 1.21):
Для старых версий действительно: