Nginx. Трассировка. Взгляд землекопа

Kate

Administrator
Команда форума
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 строк
Так как данная конфигурация не подразумевает никаких перенаправлений, мы рассчитываем, что запрос прошел именно тот простейший путь, который определен фазами 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 и только потом начнется этап записи в лог.

Соотнесем наши результаты со списком фаз:

1NGX_HTTP_POST_READ_PHASEngx_http_core_generic_phase
2NGX_HTTP_SERVER_REWRITE_PHASEngx_http_core_rewrite_phase
3NGX_HTTP_FIND_CONFIG_PHASEngx_http_core_find_config_phase
4NGX_HTTP_REWRITE_PHASEngx_http_core_rewrite_phase
5NGX_HTTP_POST_REWRITE_PHASEngx_http_core_post_rewrite_phase
6NGX_HTTP_PREACCESS_PHASEngx_http_core_generic_phase
7NGX_HTTP_ACCESS_PHASEngx_http_core_access_phase
8NGX_HTTP_POST_ACCESS_PHASEngx_http_core_post_access_phase
9NGX_HTTP_PRECONTENT_PHASEngx_http_core_generic_phase
10NGX_HTTP_CONTENT_PHASEngx_http_core_content_phase
11NGX_HTTP_LOG_PHASEngx_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 вообще, то, по крайней мере, к использованию его с крайней осторожностью и после тщательного тестирования, как, впрочем, и любой другой сборки или модуля.

Источник статьи: https://habr.com/ru/post/567978/
 
Сверху