Nginx состоит из модулей и именно они выполняют всю реальную работу. Стандартные модули позволяют решить большинство задач, но наступает момент, когда необходимо осуществить какие-то нетипичные действия и тогда мы либо берем сторонний модуль, либо пишем свой собственный.
При этом, даже если модуль давно написан и имеет хорошие отзывы, нет никакой гарантии, что его работа не вызовет проблем, причем, возможно, исключительно в нашей конфигурации или сборке. А Nginx, как известно, рождён для производительности, и, добавляя модули, мы не хотим этой производительности потерять. Поэтому каждая новая сборка должна завершаться отладкой и профилированием.
В недавней статье мы сняли верхний слой грунта, но чтобы локализовать возможную проблему нам придётся копать намного глубже. В самом Nginx есть режим отладки, и он действительно помогает выявлять проблемы, но в качестве профилировщика не годится, так как сам вносит приличную задержку. Поэтому нам потребуется сторонний инструмент и тут, как нельзя лучше, подойдёт dtrace.
Вообще, настоящие экскаваторщики готовят dtrace вот так. Но этот подход, из-за тьмы перекрестных зависимостей, требует либо включения множества файлов заголовков, либо ручного определения структур и типов. Мы, простые чернорабочие, поступаем проще.
Для начала попросим Nginx работать с одним рабочим процессом
worker_processes 1;
и создадим простейшую конфигурацию:
server { root /www/example_com; index index.html; listen *:80; server_name .example.com; location / { } }
Посмотрим какой путь проходит наш запрос, создав сценарий на языке D (который не стоит путать с языком D). Для этого воспользуемся опцией flowindent, чтобы построить дерево вызовов.
#!/usr/sbin/dtrace -s #pragma D option flowindent pid$1::ngx_http_init_connection:entry { self->traceme = 1; } pid$1:nginx:ngx_http*:entry { } pid$1:nginx:ngx_http*:return { } pid$1::ngx_http_finalize_request:return { trace(arg1); self->traceme = 0; exit(0); }
Исполнив сценарий, мы получим симпатичное дерево иллюстрирующее путь нашего запроса.
Аккуратно, 169 строк
CPU FUNCTION 3 -> ngx_http_init_connection 3 <- ngx_http_init_connection 3 -> ngx_http_wait_request_handler 3 -> ngx_http_alloc_request 3 <- ngx_http_alloc_request 3 <- ngx_http_wait_request_handler 3 -> ngx_http_process_request_line 3 -> ngx_http_read_request_header 3 <- ngx_http_read_request_header 3 -> ngx_http_parse_request_line 3 <- ngx_http_parse_request_line 3 -> ngx_http_process_request_uri 3 <- ngx_http_process_request_uri 3 -> ngx_http_process_request_headers 3 -> ngx_http_read_request_header 3 <- ngx_http_read_request_header 3 -> ngx_http_parse_header_line 3 <- ngx_http_parse_header_line 3 -> ngx_http_process_header_line 3 <- ngx_http_process_header_line 3 -> ngx_http_parse_header_line 3 <- ngx_http_parse_header_line 3 -> ngx_http_process_connection 3 <- ngx_http_process_connection 3 -> ngx_http_parse_header_line 3 <- ngx_http_parse_header_line 3 -> ngx_http_process_host 3 -> ngx_http_validate_host 3 <- ngx_http_validate_host 3 -> ngx_http_set_virtual_server 3 -> ngx_http_find_virtual_server 3 <- ngx_http_find_virtual_server 3 <- ngx_http_set_virtual_server 3 <- ngx_http_process_host 3 -> ngx_http_parse_header_line 3 <- ngx_http_parse_header_line 3 -> ngx_http_process_user_agent 3 <- ngx_http_process_user_agent 3 -> ngx_http_parse_header_line 3 <- ngx_http_parse_header_line 3 -> ngx_http_process_request_header 3 <- ngx_http_process_request_header 3 -> ngx_http_process_request 3 -> ngx_http_handler 3 -> ngx_http_core_generic_phase 3 -> ngx_http_realip_handler 3 <- ngx_http_realip_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_rewrite_phase 3 -> ngx_http_rewrite_handler 3 <- ngx_http_rewrite_handler 3 <- ngx_http_core_rewrite_phase 3 -> ngx_http_core_find_config_phase 3 -> ngx_http_core_find_location 3 -> ngx_http_core_find_location 3 <- ngx_http_core_find_location 3 <- ngx_http_core_find_location 3 -> ngx_http_update_location_config 3 <- ngx_http_update_location_config 3 <- ngx_http_core_find_config_phase 3 -> ngx_http_core_rewrite_phase 3 -> ngx_http_rewrite_handler 3 <- ngx_http_rewrite_handler 3 <- ngx_http_core_rewrite_phase 3 -> ngx_http_core_post_rewrite_phase 3 <- ngx_http_core_post_rewrite_phase 3 -> ngx_http_core_generic_phase 3 -> ngx_http_realip_handler 3 <- ngx_http_realip_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_generic_phase 3 -> ngx_http_limit_req_handler 3 <- ngx_http_limit_req_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_generic_phase 3 -> ngx_http_limit_conn_handler 3 <- ngx_http_limit_conn_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_access_phase 3 -> ngx_http_access_handler 3 <- ngx_http_access_handler 3 <- ngx_http_core_access_phase 3 -> ngx_http_core_access_phase 3 -> ngx_http_auth_basic_handler 3 <- ngx_http_auth_basic_handler 3 <- ngx_http_core_access_phase 3 -> ngx_http_core_access_phase 3 -> ngx_http_auth_request_handler 3 <- ngx_http_auth_request_handler 3 <- ngx_http_core_access_phase 3 -> ngx_http_core_post_access_phase 3 <- ngx_http_core_post_access_phase 3 -> ngx_http_core_generic_phase 3 -> ngx_http_try_files_handler 3 <- ngx_http_try_files_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_generic_phase 3 -> ngx_http_mirror_handler 3 <- ngx_http_mirror_handler 3 <- ngx_http_core_generic_phase 3 -> ngx_http_core_content_phase 3 -> ngx_http_index_handler 3 <- ngx_http_index_handler 3 <- ngx_http_core_content_phase 3 -> ngx_http_core_content_phase 3 -> ngx_http_autoindex_handler 3 <- ngx_http_autoindex_handler 3 <- ngx_http_core_content_phase 3 -> ngx_http_core_content_phase 3 -> ngx_http_gzip_static_handler 3 <- ngx_http_gzip_static_handler 3 <- ngx_http_core_content_phase 3 -> ngx_http_core_content_phase 3 -> ngx_http_static_handler 3 -> ngx_http_map_uri_to_path 3 <- ngx_http_map_uri_to_path 3 -> ngx_http_set_disable_symlinks 3 <- ngx_http_set_disable_symlinks 3 -> ngx_http_discard_request_body 3 <- ngx_http_discard_request_body 3 -> ngx_http_set_etag 3 <- ngx_http_set_etag 3 -> ngx_http_set_content_type 3 <- ngx_http_set_content_type 3 -> ngx_http_send_header 3 -> ngx_http_not_modified_header_filter 3 -> ngx_http_headers_filter 3 -> ngx_http_userid_filter 3 -> ngx_http_sub_header_filter 3 -> ngx_http_charset_header_filter 3 -> ngx_http_ssi_header_filter 3 -> ngx_http_gzip_header_filter 3 -> ngx_http_range_header_filter 3 -> ngx_http_chunked_header_filter 3 -> ngx_http_header_filter 3 -> ngx_http_time 3 <- ngx_http_time 3 -> ngx_http_write_filter 3 <- ngx_http_write_filter 3 <- ngx_http_header_filter 3 <- ngx_http_charset_header_filter 3 <- ngx_http_headers_filter 3 -> ngx_http_output_filter 3 -> ngx_http_range_body_filter 3 -> ngx_http_copy_filter 3 -> ngx_http_trailers_filter 3 -> ngx_http_sub_body_filter 3 -> ngx_http_charset_body_filter 3 -> ngx_http_ssi_body_filter 3 -> ngx_http_postpone_filter 3 -> ngx_http_gzip_body_filter 3 -> ngx_http_chunked_body_filter 3 -> ngx_http_write_filter 3 -> ngx_http_complex_value_size 3 <- ngx_http_complex_value_size 3 <- ngx_http_write_filter 3 <- ngx_http_chunked_body_filter 3 <- ngx_http_gzip_body_filter 3 <- ngx_http_ssi_body_filter 3 <- ngx_http_charset_body_filter 3 <- ngx_http_trailers_filter 3 <- ngx_http_copy_filter 3 <- ngx_http_output_filter 3 <- ngx_http_static_handler 3 -> ngx_http_finalize_request 3 | ngx_http_finalize_request:return 3 <- ngx_http_finalize_request 34399759824
Так как данная конфигурация не подразумевает никаких перенаправлений, мы рассчитываем, что запрос прошел именно тот простейший путь, который определен фазами Nginx. Убедимся в этом:
$ cat /tmp/tree.txt | grep -F phase | grep "\->" | uniq | awk '{print $3}' ngx_http_core_generic_phase ngx_http_core_rewrite_phase ngx_http_core_find_config_phase ngx_http_core_rewrite_phase ngx_http_core_post_rewrite_phase ngx_http_core_generic_phase ngx_http_core_access_phase ngx_http_core_post_access_phase ngx_http_core_generic_phase ngx_http_core_content_phase
Фаз обработки запроса 11, а здесь отобразилось 10 этапов. В этом нет никакой ошибки, так как мы закончили трассировку на этапе завершения запроса, а логирование произойдет только после закрытия соединения, то есть ещё исполнятся ngx_http_finalize_connection, ngx_http_free_request и только потом начнется этап записи в лог.
Соотнесем наши результаты со списком фаз:
1 | NGX_HTTP_POST_READ_PHASE | ngx_http_core_generic_phase |
2 | NGX_HTTP_SERVER_REWRITE_PHASE | ngx_http_core_rewrite_phase |
3 | NGX_HTTP_FIND_CONFIG_PHASE | ngx_http_core_find_config_phase |
4 | NGX_HTTP_REWRITE_PHASE | ngx_http_core_rewrite_phase |
5 | NGX_HTTP_POST_REWRITE_PHASE | ngx_http_core_post_rewrite_phase |
6 | NGX_HTTP_PREACCESS_PHASE | ngx_http_core_generic_phase |
7 | NGX_HTTP_ACCESS_PHASE | ngx_http_core_access_phase |
8 | NGX_HTTP_POST_ACCESS_PHASE | ngx_http_core_post_access_phase |
9 | NGX_HTTP_PRECONTENT_PHASE | ngx_http_core_generic_phase |
10 | NGX_HTTP_CONTENT_PHASE | ngx_http_core_content_phase |
11 | NGX_HTTP_LOG_PHASE | ngx_http_core_generic_phase |
Так как обычно модуль работает в какой-то одной фазе, то её пробу нам и нужно делать. Для примера возьмем NGX_HTTP_POST_ACCESS_PHASE и посмотрим сколько времени занимает её обработка:
#!/usr/sbin/dtrace -s pid$1::ngx_http_init_connection:entry { printf("init\n"); elapsed_post_access = 0; } pid$1::ngx_http_core_post_access_phase:entry { begin_post_access = timestamp; printf("begin (post_access): %d\n", begin_post_access); } pid$1::ngx_http_core_post_access_phase:return /begin_post_access > 0/ { elapsed_post_access += timestamp - begin_post_access; begin_post_access = 0; printf("elapsed (post_access): %d\n", elapsed_post_access); } pid$1::ngx_http_finalize_request:return /elapsed_post_access > 0/ { printf("final (post_access)\n"); @elapsed_post_access = avg(elapsed_post_access); elapsed_post_access = 0; exit(0); }
# dtrace -s ./post_access.d `pgrep -f 'nginx: worker process'` dtrace: script './post_access.d' matched 4 probes CPU ID FUNCTION:NAME 7 71748 ngx_http_init_connection:entry init 7 71754 ngx_http_core_post_access_phase:entry begin (post_access): 18833138568826655 7 71755 ngx_http_core_post_access_phase:return elapsed (post_access): 3543 7 71751 ngx_http_finalize_request:return final (post_access) 3543
Этот же подход можно применять ещё более точечно, например, копаясь в фильтрах тела запроса. Попробуем сделать практический анализ эффективности двух различных модулей, решающих одну и ту же задачу, для этого в качестве index.html используем страничку rfc2822, и соберем Nginx c поддержкой родного http_sub_module и стороннего ngx_http_substitutions_filter_module.
Соответственно, для теста мы имеем две конфигурации:
location / { sub_filter 'span' 'div'; sub_filter_once off; }
location / { subs_filter 'span' 'div'; }
И два сценария dtrace:
sub
#!/usr/sbin/dtrace -s pid$1::ngx_http_init_connection:entry { printf("init\n"); elapsed_sub_filter = 0; } pid$1::ngx_http_sub_body_filter:entry { begin_sub_filter = timestamp; printf("begin (sub_filter): %d\n", begin_sub_filter); } pid$1::ngx_http_sub_body_filter:return /begin_sub_filter > 0/ { elapsed_sub_filter += timestamp - begin_sub_filter; begin_sub_filter = 0; printf("elapsed (sub_filter): %d\n", elapsed_sub_filter); } pid$1::ngx_http_finalize_request:return /elapsed_sub_filter > 0/ { exit(0); }
subs
#!/usr/sbin/dtrace -s pid$1::ngx_http_init_connection:entry { printf("init\n"); elapsed_subs_filter = 0; } pid$1::ngx_http_subs_body_filter:entry { begin_subs_filter = timestamp; printf("begin (subs_filter): %d\n", begin_subs_filter); } pid$1::ngx_http_subs_body_filter:return /begin_subs_filter > 0/ { elapsed_subs_filter += timestamp - begin_subs_filter; begin_subs_filter = 0; printf("elapsed (subs_filter): %d\n", elapsed_subs_filter); } pid$1::ngx_http_finalize_request:return /elapsed_subs_filter > 0/ { exit(0); }
# dtrace -s ./sub_filter.d `pgrep -f 'nginx: worker process'` dtrace: script './sub_filter.d' matched 4 probes CPU ID FUNCTION:NAME 2 71748 ngx_http_init_connection:entry init 2 71752 ngx_http_sub_body_filter:entry begin (sub_filter): 19078115202094268 2 71753 ngx_http_sub_body_filter:return elapsed (sub_filter): 58548 2 71752 ngx_http_sub_body_filter:entry begin (sub_filter): 19078115202360871 2 71753 ngx_http_sub_body_filter:return elapsed (sub_filter): 118098 2 71752 ngx_http_sub_body_filter:entry begin (sub_filter): 19078115202697212 2 71753 ngx_http_sub_body_filter:return elapsed (sub_filter): 169395 2 71752 ngx_http_sub_body_filter:entry begin (sub_filter): 19078115202919509 2 71753 ngx_http_sub_body_filter:return elapsed (sub_filter): 225577 2 71752 ngx_http_sub_body_filter:entry begin (sub_filter): 19078115203186481 2 71753 ngx_http_sub_body_filter:return elapsed (sub_filter): 256872 2 71751 ngx_http_finalize_request:return
# dtrace -s ./subs_filter.d `pgrep -f 'nginx: worker process'` dtrace: script './subs_filter.d' matched 4 probes CPU ID FUNCTION:NAME 2 71748 ngx_http_init_connection:entry init 2 71749 ngx_http_subs_body_filter:entry begin (subs_filter): 19078177580261158 2 71750 ngx_http_subs_body_filter:return elapsed (subs_filter): 195538 2 71749 ngx_http_subs_body_filter:entry begin (subs_filter): 19078177580467254 2 71750 ngx_http_subs_body_filter:return elapsed (subs_filter): 336789 2 71749 ngx_http_subs_body_filter:entry begin (subs_filter): 19078177580616159 2 71750 ngx_http_subs_body_filter:return elapsed (subs_filter): 484982 2 71749 ngx_http_subs_body_filter:entry begin (subs_filter): 19078177580772832 2 71750 ngx_http_subs_body_filter:return elapsed (subs_filter): 623289 2 71749 ngx_http_subs_body_filter:entry begin (subs_filter): 19078177580916291 2 71750 ngx_http_subs_body_filter:return elapsed (subs_filter): 700474 2 71751 ngx_http_finalize_request:return final
Не будем доверять штучному тесту и выполним миллион запросов, собрав значения в агрегирующую функцию:
sub_filter_avg.d
#!/usr/sbin/dtrace -s pid$1::ngx_http_init_connection:entry { elapsed_sub_filter = 0; } pid$1::ngx_http_sub_body_filter:entry { begin_sub_filter = timestamp; } pid$1::ngx_http_sub_body_filter:return /begin_sub_filter > 0/ { elapsed_sub_filter += timestamp - begin_sub_filter; begin_sub_filter = 0; } pid$1::ngx_http_finalize_request:return /elapsed_sub_filter > 0/ { @elapsed_sub_filter = avg(elapsed_sub_filter); }
$ ab -k -c1 -n1000000 'example.com/index.html'
# dtrace -s ./sub_filter_avg.d `pgrep -f 'nginx: worker process'` dtrace: script './sub_filter_avg.d' matched 4 probes ^C 245971
# dtrace -s ./subs_filter_avg.d `pgrep -f 'nginx: worker process'` dtrace: script './subs_filter_avg.d' matched 4 probes ^C 651586
С одной стороны, преимущество в 2.6 раза, с другой - разница в 400 микросекунд не так уж и критична. Но по крайней мере без использования regex, использование стороннего модуля не даёт преимуществ.
Бытует мнение, что с веток 1.9+ sub_filter существенно ускорился. Ради интереса я откатился до более ранних версий
1.8.1:
# dtrace -s ./sub_filter_avg.d `pgrep -f 'nginx: worker process'` dtrace: script './sub_filter_avg.d' matched 4 probes ^C 244136
1.6.3:
# dtrace -s ./sub_filter_avg.d `pgrep -f 'nginx: worker process'` dtrace: script './sub_filter_avg.d' matched 4 probes ^C 240079
но на этих простых задачах ускорения не заметил.
Подводя итоги. На самом деле, хотя OpenResty и не упоминается в статье, всё вышесказанное было написано с целью ответа на комментарий к предыдущей публикации. Я не ответил на него прямо, так как это потребовало бы подтверждений, которых у меня сейчас нет, но не ответить совсем не мог, поэтому хотя бы показал тот путь, который привел меня к отказу, если не от OpenResty вообще, то, по крайней мере, к использованию его с крайней осторожностью и после тщательного тестирования, как, впрочем, и любой другой сборки или модуля.
