`systemtap-toolkit` и `stapxx`: Как использовать данные для решения сложных проблем?
API7.ai
December 22, 2022
Как было упомянуто в предыдущей статье, будучи инженерами серверной разработки, мы не углубляемся в изучение инструментов динамической отладки, а в основном остаемся на уровне использования и, в лучшем случае, пишем несколько простых скриптов. Более низкоуровневые аспекты, такие как кэш CPU, архитектура, компилятор и т.д., относятся к области инженеров по производительности.
В OpenResty есть два открытых проекта: openresty-systemtap-toolkit и stapxx. Это инструменты, основанные на systemtap, предназначенные для анализа и диагностики NGINX и OpenResty в реальном времени. Они охватывают такие функции и сценарии отладки, как on-CPU, off-CPU, shared dictionary, сборка мусора, задержка запросов, пулы памяти, пулы соединений, доступ к файлам и другие.
В этой статье мы рассмотрим эти инструменты и их использование, чтобы помочь нам быстро находить инструменты для локализации проблем при возникновении неполадок с NGINX и OpenResty. В мире OpenResty изучение этих инструментов — это верный способ продвинуться вперед и эффективный способ общения с другими разработчиками — ведь данные, генерируемые инструментами, будут более точными и детальными, чем мы можем описать словами.
Однако важно отметить, что в OpenResty версии 1.15.8 по умолчанию включен режим LuaJIT GC64, но openresty-systemtap-toolkit и stapxx не следуют соответствующим изменениям, что приведет к некорректной работе инструментов. Поэтому лучше использовать эти инструменты в старой версии OpenResty 1.13.
Большинство участников открытых проектов работают на добровольной основе и не обязаны поддерживать работоспособность инструментов, что нужно учитывать при использовании открытых проектов.
Пример: shared dict
Начнем с инструмента, который, вероятно, наиболее знаком и прост в использовании, — ngx-lua-shdict, чтобы начать сегодняшний пост.
ngx-lua-shdict — это инструмент для анализа shared dict в NGINX и отслеживания их операций. Вы можете использовать опцию -f, чтобы указать dict и key, чтобы получить данные из shared dict. Опция --raw позволяет экспортировать исходное значение указанного key.
Ниже приведен пример команды для получения данных из shared dict.
# Предположим, что PID NGINX Worker равен 5050 $ ./ngx-lua-shdict -p 5050 -f --dict dogs --key Jim --luajit20 Tracing 5050 (/opt/nginx/sbin/nginx)... type: LUA_TBOOLEAN value: true expires: 1372719243270 flags: 0xa
Аналогично, мы можем использовать опцию -w, чтобы отслеживать записи в dict для заданного key:
$./ngx-lua-shdict -p 5050 -w --key Jim --luajit20 Tracing 5050 (/opt/nginx/sbin/nginx)... Hit Ctrl-C to end set Jim exptime=4626322717216342016 replace Jim exptime=4626322717216342016 ^C
Давайте посмотрим, как реализован этот инструмент. ngx-lua-shdict — это Perl-скрипт, но реализация не имеет отношения к Perl, который используется только для генерации stap-скрипта и его запуска.
open my $in, "|stap $stap_args -x $pid -" or die "Cannot run stap: $!\n";
Мы можем написать его на Python, PHP, Go или любом другом языке. Ключевой момент в stap-скрипте — это следующая строка кода:
probe process("$nginx_path").function("ngx_http_lua_shdict_set_helper")
Это probe, который мы упоминали ранее, он отслеживает функцию ngx_http_lua_shdict_set_helper. Вызовы этой функции находятся в файле lua-nginx-module/src/ngx_http_lua_shdict.c модуля lua-nginx-module.
static int ngx_http_lua_shdict_add(lua_State *L) { return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_ADD); } static int ngx_http_lua_shdict_safe_add(lua_State *L) { return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_ADD |NGX_HTTP_LUA_SHDICT_SAFE_STORE); } static int ngx_http_lua_shdict_replace(lua_State *L) { return ngx_http_lua_shdict_set_helper(L, NGX_HTTP_LUA_SHDICT_REPLACE); }
Таким образом, мы можем отслеживать все операции с shared dict, просто отслеживая эту функцию.
on-CPU, off-CPU
При использовании OpenResty наиболее распространенной проблемой, с которой мы сталкиваемся, является производительность. Существует два основных типа низкой производительности, т.е. низкого QPS: слишком высокая загрузка CPU и слишком низкая загрузка CPU. Первая проблема может быть связана с тем, что мы не используем методы оптимизации производительности, которые мы обсуждали ранее, а вторая может быть вызвана блокирующими функциями. Соответствующие on-CPU и off-CPU flame graphs помогут нам определить корневую причину.
Для генерации C-level on-CPU flame graphs необходимо использовать sample-bt в systemtap-toolkit; в то время как Lua-level on-CPU flame graphs генерируются с помощью lj-lua-stacks в stapxx.
Давайте рассмотрим пример использования sample-bt. sample-bt — это скрипт, который сэмплирует стек вызовов любого пользовательского процесса, который мы укажем (не только процессы NGINX и OpenResty).
Например, мы можем сэмплировать работающий процесс NGINX Worker (с PID 8736) в течение 5 секунд следующим кодом:
$ ./sample-bt -p 8736 -t 5 -u > a.bt WARNING: Tracing 8736 (/opt/nginx/sbin/nginx) in user-space only... WARNING: Missing unwind data for module, rerun with 'stap -d stap_df60590ce8827444bfebaf5ea938b5a_11577' WARNING: Time's up. Quitting now...(it may take a while) WARNING: Number of errors: 0, skipped probes: 24
Он выводит файл результата a.bt, который можно использовать для генерации flame graph с помощью инструментария FlameGraph:
stackcollapse-stap.pl a.bt > a.cbt flamegraph.pl a.cbt > a.svg
a.svg — это сгенерированный flame graph, который мы можем открыть в браузере для просмотра. Однако в течение периода сэмплирования мы должны поддерживать определенное количество запросов. В противном случае, если количество сэмплов равно 0, flame graph не будет сгенерирован.
Далее мы рассмотрим, как сэмплировать off-CPU, используя скрипт sample-bt-off-cpu в systemtap-toolkit, который похож на sample-bt, следующим образом:
$ ./sample-bt-off-cpu -p 10901 -t 5 > a.bt WARNING: Tracing 10901 (/opt/nginx/sbin/nginx)... WARNING: _stp_read_address failed to access memory location WARNING: Time's up. Quitting now...(it may take a while) WARNING: Number of errors: 0, skipped probes: 23
В stapxx инструмент для анализа задержек — это epoll-loop-blocking-distr, который сэмплирует указанный пользовательский процесс и выводит распределение задержек между последовательными вызовами системной функции epoll_wait.
$ ./samples/epoll-loop-blocking-distr.sxx -x 19647 --arg time=60 Start tracing 19647... Please wait for 60 seconds. Distribution of epoll loop blocking latencies (in milliseconds) max/avg/min: 1097/0/0 value |-------------------------------------------------- count 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 18471 1 |@@@@@@@@ 3273 2 |@ 473 4 | 119 8 | 67 16 | 51 32 | 35 64 | 20 128 | 23 256 | 9 512 | 2 1024 | 2 2048 | 0 4096 | 0
Как мы видим, этот вывод показывает, что подавляющее большинство задержек составляет менее 1 мс, но есть несколько задержек выше 200 мс, и именно на них стоит обратить внимание.
Отслеживание Upstream и Phase
Помимо проблем с производительностью, которые могут возникать в самом коде OpenResty, когда OpenResty взаимодействует с вышестоящими сервисами через модули upstream, такие как cosocket или proxy_pass, если сам вышестоящий сервис имеет большую задержку, это также может значительно повлиять на общую производительность.
В этом случае мы можем использовать инструменты ngx-lua-tcp-recv-time, ngx-lua-udp-recv-time и ngx-single-req-latency для анализа. Вот пример ngx-single-req-latency.
Этот инструмент отличается от большинства инструментов в наборе. Большинство других инструментов основаны на большом количестве сэмплов и статистическом анализе для получения математического вывода о распределении. Вместо этого ngx-single-req-latency анализирует отдельные запросы и отслеживает время, затраченное на отдельные запросы на различных этапах в OpenResty, таких как rewrite, access, content, а также upstream.
Рассмотрим конкретный пример кода:
# делаем инструмент ./stap++ видимым в PATH: $ export PATH=$PWD:$PATH # предполагая, что PID процесса NGINX Worker равен 27327 $ ./samples/ngx-single-req-latency.sxx -x 27327 Start tracing process 27327 (/opt/nginx/sbin/nginx)... POST /api_json total: 143596us, accept() ~ header-read: 43048us, rewrite: 8us, pre-access: 7us, access: 6us, content: 100507us upstream: connect=29us, time-to-first-byte=99157us, read=103us $ ./samples/ngx-single-req-latency.sxx -x 27327 Start tracing process 27327 (/opt/nginx/sbin/nginx)... GET /robots.txt total: 61198us, accept() ~ header-read: 33410us, rewrite: 7us, pre-access: 7us, access: 5us, content: 27750us upstream: connect=30us, time-to-first-byte=18955us, read=96us
Этот инструмент будет отслеживать первый запрос, который он встретит после запуска. Вывод очень похож на opentracing, и мы можем даже рассматривать systemtap-toolkit и stapxx как неинвазивные версии APM (Application Performance Management) в OpenResty.
Заключение
Помимо общих инструментов, о которых я говорил сегодня, OpenResty, естественно, предоставляет множество других инструментов, которые вы можете изучить самостоятельно.
Есть еще одно важное отличие OpenResty от других языков и платформ разработки в области технологий трассировки, которое мы можем постепенно оценить.
Держите код чистым и стабильным, не добавляйте в него зонды, а сэмплируйте его с помощью внешних технологий динамической трассировки.
Какие инструменты вы использовали для отслеживания и анализа проблем при использовании OpenResty? Поделитесь этой статьей, и мы будем делиться и прогрессировать вместе.