Nginx. О чем не пишут в книгах
Эта статья родилась случайно. Слоняясь по книжному фестивалю и наблюдая, как дочка пытает консультантов, заставляя их искать Иэна Стюарта, мой глаз зацепился за знакомые буквы на обложке: «Nginx».
Надо же, на полках нашлось целых три книги — не полистать их было бы преступлением. Первая, вторая, третья… Ощущение, будто что-то не так. Ну вроде страниц много, текст связный, но каково содержание? Установка nginx, список переменных и модулей, а дальше docker, ansible. Открываем вторую: wget, лимиты запросов и памяти, балансировка, kubernetes, AWS. Третья: GeoIP, авторизация, потоковое вещание, puppet, Azure. Ребята, а где про то, как вообще работает nginx? На кого рассчитаны ваши книги? На состоявшегося админа, который и так знает архитектуру этого веб-сервера? Да он вроде с базовыми настройками и сам справится. На новичка, который не знает как пользоваться wget? Вы уверены, что ему знание о существовании ngx_http_degradation_module и тем паче «облака» важнее порядка прохождения запроса?
Итак. О чем не пишут в книгах.
(здесь и дальше мы говорим только о NGX_HTTP_)
Фазы обработки запроса
Nginx разделяет обработку запросов на одиннадцать этапов (фаз). Модуль, обрабатывая запрос, может реализовывать свои функции на одном или нескольких этапах. Эти этапы определены следующим образом:
/src/http/ngx_http_core_module.h
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;
В данном перечислении фазы расположены именно в том порядке, в котором они исполняются. Каждый запрос, проходя цепочку модулей, может быть обработан на определенном этапе, но именно в такой очередности. Строго говоря, в данной очередности может появиться петля, в случае изменения URI, но обо всём по порядку.
NGX_HTTP_POST_READ_PHASE — начальная фаза обработки запроса. На ней обрабатывается хэндлер только одного встроенного модуля ngx_http_realip_module, который позволяет менять адрес (и порт клиента) на переданные в поле заголовка определенном директивой real_ip_header.
Вторая и четвертая фазы (NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_REWRITE_PHASE) являются этапами перенаправлений, на которых обрабатываются директивы модуля ngx_http_rewrite_module (пример того, как хэндлеры модуля могут быть реализованы на разных фазах).
Как и следует из названия, NGX_HTTP_SERVER_REWRITE_PHASE обрабатывает директивы в контексте server, в то время как NGX_HTTP_REWRITE_PHASE обрабатывает директивы определенные в местоположении заданном на этапе NGX_HTTP_FIND_CONFIG_PHASE. Этот этап (третий из нашего списка) не позволяет навешивать собственные обработчики. На нем nginx определяет новое (отличного от дефолтного) местоположение обработки запроса.
Продемонстрируем вышесказанное на практическом примере. Для этого пересоберем nginx c поддержкой отладки и рассмотрим следующий конфиг:
server {
listen *:80;
server_name .example.com;
set $test 101;
location / {
set $test 201;
set $test 202;
return 200 "$test\n";
set $test 203;
}
set $test 102;
}
Делаем запрос
$ GET -S example.com
GET http://example.com
200 OK
202
и видим в логе:
$ grep -E 'http script (set|val)' /var/log/nginx/error.log | cut -d " " -f 6-
http script value: "101"
http script set $test
http script value: "102"
http script set $test
http script value: "201"
http script set $test
http script value: "202"
http script set $test
Директивы модуля ngx_http_rewrite_module (break, if, return, rewrite и set) обрабатываются последовательно, в порядке указанном в конфигурационном файле, но в очередности определенной фазами nginx, поэтому сначала в фазе NGX_HTTP_SERVER_REWRITE_PHASE переменной $test присвоилось значение 101, затем 102 (не смотря на его расположение ниже блока location), затем в фазе NGX_HTTP_REWRITE_PHASE она стала равна 201 и 202, после чего исполнилась директива return, на которой обработка запроса в данной фазе прервалась, и до »set $test 203;» ход уже не дошел.
Следующая (пятая) фаза NGX_HTTP_POST_REWRITE_PHASE также является служебной и не позволяет регистрировать собственные обработчики. По сути, на ней происходит повторный переход к фазе NGX_HTTP_FIND_CONFIG_PHASE в случае, если URI запроса изменился на предыдущем этапе. Именно об этом (возможном) цикле и говорилось ранее.
Важным моментом здесь является именно, то, что фактический rewrite происходит именно в фазе NGX_HTTP_POST_REWRITE_PHASE, а не в NGX_HTTP_REWRITE_PHASE, как может ожидаться исходя из того, что директива rewrite модуля ngx_http_rewrite_module обрабатывается именно в ней.
server {
listen *:80;
server_name .example.com;
location / {
rewrite ^ /one;
rewrite ^ /two;
rewrite ^ /three;
}
location /one {
return 200 "one\n";
}
location /two {
return 200 "two\n";
}
location /three {
return 200 "three\n";
}
}
$ GET -S example.com
GET http://example.com
200 OK
three
$ grep -E '(rewritten|finalize)' /var/log/nginx/error.log | cut -d " " -f 6-
rewritten data: "/one", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
rewritten data: "/two", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
rewritten data: "/three", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
http finalize request: 0, "/three?" a:1, c:1
Здесь ошибочно могло ожидаться перенаправление в location »/one» на этапе »rewrite ^ /one;». На самом же деле, сначала последовательно зафиксируются все три rewrite, и только после этого, в фазе NGX_HTTP_POST_REWRITE_PHASE (один раз) действительно выполнится перенаправление.
Ещё немного модифицируем вышеприведенный конфиг:
server {
listen *:80;
server_name .example.com;
location / {
set $test 101;
rewrite ^ /one;
set $test 102;
rewrite ^ /two;
set $test 103;
rewrite ^ /three;
}
location /one {
return 200 "$test\n";
}
location /two {
return 200 "$test\n";
}
location /three {
return 200 "$test\n";
}
}
$ GET -S example.com
GET http://example.com
200 OK
103
$ grep -E '(set|val|rewritten|finalize)' /var/log/nginx/error.log | cut -d " " -f 6-
kevent set event: 3: ft:-1 fl:0025
http script value: "101"
http script set $test
rewritten data: "/one", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
http script value: "102"
http script set $test
rewritten data: "/two", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
http script value: "103"
http script set $test
rewritten data: "/three", args: "", client: 12.34.56.78, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
http set discard body
http finalize request: 0, "/three?" a:1, c:1
Здесь интересны два момента:
1) Уже вышесказанное — директивы модуля ngx_http_rewrite_module (break, if, return, rewrite и set) обрабатываются в порядке следования в конфигурационном файле. При этом обработка может быть прервана только исполнением директивы return или в случае, если строка замены директивы rewrite начинается с «http://», «https://» или »$scheme».
Теперь становится понятно, почему в такой конфигурации
server {
listen *:80;
server_name .example.com;
location / {
rewrite ^ /one;
return 200 "zero\n";
}
location /one {
return 200 "one\n";
}
}
не происходит перенаправления в location one.
$ GET -S example.com
GET http://example.com
200 OK
zero
2) Время (а точнее место) жизни переменной не ограничивается одним location — после выполнения трех директив set переменная $test была установлена в значение »103» и не утратила этого значения при перемещении в location »/three». Это касается не только обычных перенаправлений, но и подзапросов.
Для наглядности добавим в сборку модуль echo-nginx-module, реализующий директиву echo, которая позволяет передать клиенту контент, в том числе, содержащий переменные. В целом я противник OpenResty, хотя и не такой категоричный как Максим Дунин:
Я бы не рекомендовал ничего искать в исходниках «echo», там у автора подход «если оно работает, то и хорошо». Имеет смысл для начала смотреть в исходники самого nginx’а и стандартных модулей.
Но для наглядности он бывает полезен, да и сама эта статья во многом перекликается с http://openresty.org/download/agentzh-nginx-tutorials-en.html
Вопрос (в общем-то, простой, но, на мой взгляд, весьма неплохой для того же собеседования): «Какой ответ получит клиент, при выполнении запроса к корню сайта со следующим конфигом?» Отмечу, что echo-nginx-module реализован на этапе NGX_HTTP_CONTENT_PHASE.
server {
listen *:80;
server_name .example.com;
location / {
set $test one;
echo "root: $test";
set $test "$test two";
auth_request /auth;
set $test "$test three";
}
location /auth {
set $test "$test auth_pre";
return 200 "auth: $test\n";
set $test "$test auth_post";
}
}
Ответ$ GET -S example.com
GET http://example.com
200 OK
root: one two three auth_pre
Сначала исполняются все директивы set в локейшине »/»;
Затем выполняется подзапрос auth_request /auth;
В локейшине »/auth» выполняется ещё одна директива set, при этом предыдущее значение переменной $test не теряется;
После выполнения подзапроса исполнение возвращается в локейшин »/» и происходит отдача контента клиенту директивой echo.
Отдельным интересным моментом является, то, что return 200 «auth: $test\n»; не возвращает клиенту никакого контента, так как тело ответа от подзапроса «отбрасывается» модулем ngx_http_auth_request_module.
NGX_HTTP_PREACCESS_PHASE — здесь работают, как редкий, уже упомянутый выше, известный только фряшникам, ngx_http_degradation_module, так и весьма популярные ngx_http_limit_req_module и ngx_http_limit_conn_module. То есть это этап, на котором ещё не требуется выяснять права доступа, но уже можно произвести контроль количества соединений, запросов и потребляемой памяти.
Седьмая фаза (NGX_HTTP_ACCESS_PHASE) — место где работают модули ngx_http_access_module, ngx_http_auth_basic_module и ngx_http_auth_request_module. Клиент должен пройти проверку авторизации всех (поведение можно изменить установкой директивы satisfy в any) хэндлеров, данных модулей, чтобы перейти к следующей фазе NGX_HTTP_POST_ACCESS_PHASE. В этой фазе и происходит обработка директивы satisfy и также нельзя навесить собственные обработчики.
Почти добрались до контента, но на этапе NGX_HTTP_PRECONTENT_PHASE ещё есть возможность проверить существование файла (ngx_http_try_files_module), которому передать запрос или отзеркалировать (ngx_http_mirror_module) его.
NGX_HTTP_CONTENT_PHASE — этап генерации ответа, самая «популярная» фаза. Хэндлеры исполняются последовательно, пока один из них не сформирует и вернет ответ.
Ну и заключительный этап NGX_HTTP_LOG_PHASE — фаза логирования. На ней работает только один стандартный модуль ngx_http_log_module. Этой фазой завершается обработка запроса.
Следует отметить, что модуль не обязательно должен исполняться на определенном этапе — он может быть «фазонезависим». Классическим примером является ngx_http_map_module. Да, вычисление директивы map происходит именно в момент использования переменной, но определение «формулы» по которой происходит вычисление переменной — это просто декларация не зависящая от фазы обработки запроса.
На этом можно было бы закончить с фазами, так как сказанное ниже больше относится к модулям (а это тема отдельного цикла статей), но всё же пару строк о хэндлерах и встраивании модуля в nginx на определенной фазе.
Ожидается, что хэндлеры могут возвращать следующие коды:
NGX_OK — Обработка завершена успешно, переходим к следующей фазе.
NGX_DECLINED — Запрос не предназначен данному хэндлеру, необходимо перейти к следующему хэндлеру текущей фазы. Если же текущий хэндлер является последним в текущей фазе, то переходим к следующей фазе.
NGX_AGAIN (для фаз NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACCESS_PHASE) или NGX_DONE (для фазы NGX_HTTP_CONTENT_PHASE) — выполнение хэндлера завершено успешно, необходимо подождать появления некоторого события (например, асинхронной операцией ввода-вывода) и повторить вызов хэндлера;
NGX_ERROR или NGX_HTTP_* — при выполнении хэндлера произошла ошибка. В случае NGX_ERROR соединение будет прервано, иначе будет возвращен HTTP код ошибки.
Зарегистрировать хэндлеры можно одним из двух способов:
1) Обратиться к основной конфигурации модуля ngx_http_core_module и добавить хэндлер к одному из элементов вектора phases. Пример регистрации хэндлера в фазе NGX_HTTP_CONTENT_PHASE:
static ngx_int_t
ngx_http_sample_init(ngx_conf_t *cf)
{
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_sample_handler;
return NGX_OK;
}
2) Получить конфигурацию location и указать функцию, которая будет обрабатывать запросы для этого location (данный метод применим только для фазы NGX_HTTP_CONTENT_PHASE):
static char *
ngx_http_sample(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_sample_handler;
return NGX_CONF_OK;
}
Теперь, зная способы встраивания «фазовых» модулей, мы можем «расшифровать» заглавную картинку и посмотреть какой из стандартных модулей на каком этапе реализует свои функции:
$ grep -E '[ ]+NGX_HTTP_.+_PHASE' ../ngx_http_core_module.h |\
> cut -d ',' -f 1 |\
> awk '{print $1}' |\
> xargs -tI %% grep -rF %% . |\
> cut -d ':' -f 1 \
>
grep -rF NGX_HTTP_POST_READ_PHASE .
./ngx_http_realip_module.c
grep -rF NGX_HTTP_SERVER_REWRITE_PHASE .
./ngx_http_rewrite_module.c
grep -rF NGX_HTTP_FIND_CONFIG_PHASE .
grep -rF NGX_HTTP_REWRITE_PHASE .
./ngx_http_rewrite_module.c
grep -rF NGX_HTTP_POST_REWRITE_PHASE .
grep -rF NGX_HTTP_PREACCESS_PHASE .
./ngx_http_limit_req_module.c
./ngx_http_limit_conn_module.c
./ngx_http_realip_module.c
./ngx_http_degradation_module.c
grep -rF NGX_HTTP_ACCESS_PHASE .
./ngx_http_auth_basic_module.c
./ngx_http_access_module.c
./ngx_http_auth_request_module.c
grep -rF NGX_HTTP_POST_ACCESS_PHASE .
grep -rF NGX_HTTP_PRECONTENT_PHASE .
./ngx_http_mirror_module.c
./ngx_http_try_files_module.c
grep -rF NGX_HTTP_CONTENT_PHASE .
./ngx_http_random_index_module.c
./ngx_http_static_module.c
./ngx_http_dav_module.c
./ngx_http_gzip_static_module.c
./ngx_http_index_module.c
./ngx_http_autoindex_module.c
grep -rF NGX_HTTP_LOG_PHASE .
./ngx_http_log_module.c
$ grep -rF 'clcf->handler = ' . |\
> cut -d ':' -f 1 \
>
./ngx_http_uwsgi_module.c
./ngx_http_uwsgi_module.c
./ngx_http_memcached_module.c
./ngx_http_grpc_module.c
./ngx_http_grpc_module.c
./ngx_http_mp4_module.c
./ngx_http_fastcgi_module.c
./ngx_http_fastcgi_module.c
./ngx_http_flv_module.c
./ngx_http_scgi_module.c
./ngx_http_scgi_module.c
./ngx_http_stub_status_module.c
./ngx_http_empty_gif_module.c
./ngx_http_proxy_module.c
./ngx_http_proxy_module.c
./perl/ngx_http_perl_module.c
На этом всё. Точнее ещё самое главное.
Спасибо!
Игорю Сысоеву
Максиму Дунину
Валентину Бартеневу
и всей команде Nginx за изумительный продукт
Роману Арутюняну
Валерию Холодкову (книга не попадалась :))
за замечательные модули и статьи
Ичунь Чжану за спорные, но интересные решения
Эвану Миллеру за «сушеный укроп»