В предыдущей публикации (переводе статьи) рассматривалась возможность инструментировать код Postgres при помощи User Statically-Defined Tracing (USDT) probe. Это замечательный подход, но с некоторой особенностью.
Чтобы использовать эти пробы, бинарники должны быть скомпилированы (собраны) неким специальным образом, например, для сборки PostgreSQL вы должны использовать специальный флаг --enable-dtrace
, т.е. вызвать configure --enable-dtrace
перед вызовом make
.
Это ограничивает область применения данной техники: вряд ли вам позволят поставить такую кастомную сборку на промышленных или близких к ним средах. Почему это не приветствуется - предмет для отдельного обсуждения, которое вызвано не только и не столько техническими аспектами такой инсталляции.
Сразу возникла идея сделать подобное инструментирование с использованием uprobe/uretprobe. Как известно, вызов почти любой функции может быть динамически инструментирован при помощи так называемых uprobe (динамические пробы в user space).
Давайте посмотрим, как выглядит инструментирование кода Postgres изнутри
https://github.com/postgres/postgres/blob/master/src/backend/tcop/postgres :
static void
exec_simple_query(const char *query_string)
{
CommandDest dest = whereToSendOutput;
MemoryContext oldcontext;
List *parsetree_list;
ListCell *parsetree_item;
bool save_log_statement_stats = log_statement_stats;
bool was_logged = false;
bool use_implicit_block;
char msec_str[32];
/*
* Report query to various monitoring facilities.
*/
debug_query_string = query_string;
pgstat_report_activity(STATE_RUNNING, query_string);
TRACE_POSTGRESQL_QUERY_START(query_string);
Макрос TRACE_POSTGRESQL_QUERY_START является тем самым USDT-инструментированием.
Соответствующий код в bpftrace-скрипте
https://github.com/FritsHoogland/postgres-bpftrace/blob/main/query-analyzer.bt
usdt:/usr/lib/postgresql/15/bin/postgres:query__start
{
$time = nsecs;
printf("[%5u]Query start: : : %s\n", pid, str(arg0));
@query_start[pid] = $time;
@phase_done[pid] = $time;
@query_trigger[pid] = 1;
}
Из вышеприведённого видно, что вместо USDT-пробы TRACE_POSTGRESQL_QUERY_START можно инструментировать вызов функции exec_simple_query. Соответствующий код будет выглядеть так:
uprobe:/usr/local/pgsql/bin/postgres:exec_simple_query
{
$time = nsecs;
printf("[%5u]Query start: : : %s\n", pid, str(arg0));
@query_start[pid] = $time;
@phase_done[pid] = $time;
@query_trigger[pid] = 1;
}
Для отслеживания момента окончания вызова функции (аналог USDT-пробы TRACE_POSTGRESQL_QUERY_DONE) нужно будет использовать uretprobe-пробу.
uretprobe:/usr/local/pgsql/bin/postgres:exec_simple_query
{
$time = nsecs;
$query_end = $time - @query_start[pid];
printf("[%5u]Query done : (%10u) : %10u:\n", pid, ($time - @phase_done[pid])/1000, $query_end/1000);
@parse[pid] = (uint64)0;
@rewrite[pid] = (uint64)0;
@plan[pid] = (uint64)0;
@execute[pid] = (uint64)0;
@query_trigger[pid] = 0;
}
Целиком переработанный скрипт можно увидеть здесь:
https://hub.mos.ru/drema201/bpftrace_4pg/-/blob/main/scripts/query_uprobe_phase.bt
Для сравнения приведу исходный скрипт:
https://github.com/FritsHoogland/postgres-bpftrace/blob/main/query-analyzer.bt
Результат выполнения нового скрипта соответствует результатам Frits Hoogland:
sudo ./query_uprobe_phase.bt
Attaching 17 probes...
PostgreSQL statement execution analyzer.
Time in microseconds (us).
pid :Phase :time to phase :time in phase : query
------|-----------|--------------|--------------|------
[14697]Query start: : : SELECT 1;
[14697] parse : ( 110) : 214:
[14697] plan : ( 106) : 328:
[14697] execute : ( 84) : 34:
[14697]Query done : ( 49) : 927:
С одним незначительным отличием: uretprobe не поддерживают аргументы arg0..N, так что в Query done в вывод не включён текст запроса (впрочем, это может быть решено другими средствами).
В статье было показано, что многие аспекты внутренней жизни Postgres могут быть исследованы и без специального способа компиляции.
PS
Я собираюсь перевести следующую статью Frits Hoogland, в которой он пробует воспроизвести функционал OWI (Oracle Wait Interface) на Postgres при помощи bpftrace. И здесь мой подход может быть не применим, по крайней мере напрямую. To be continued :)