27.5. Динамическое трассирование#

27.5. Динамическое трассирование

27.5. Динамическое трассирование

Tantor SE предоставляет средства для поддержки динамического трассирования сервера баз данных. Это позволяет вызывать внешнюю утилиту в определенных точках кода и тем самым отслеживать выполнение.

В исходный код уже вставлено несколько зондов или точек трассировки. Эти зонды предназначены для использования разработчиками и администраторами баз данных. По умолчанию зонды не компилируются в Tantor SE; пользователю необходимо явно указать скрипту configure, чтобы сделать зонды доступными.

В настоящее время поддерживается утилита DTrace, которая, на момент написания этого документа, доступна на Solaris, macOS, FreeBSD, NetBSD и Oracle Linux. Проект SystemTap для Linux предоставляет аналог DTrace и также может быть использован. Теоретически возможна поддержка других утилит динамического трассирования путем изменения определений макросов в src/include/utils/probes.h.

27.5.1. Компиляция для динамического трассирования

По умолчанию зонды недоступны, поэтому необходимо явно указать скрипту configure, чтобы сделать зонды доступными в Tantor SE. Чтобы включить поддержку DTrace, укажите --enable-dtrace в configure.

27.5.2. Встроенные зонды

В исходном коде предоставляется ряд стандартных проб, как показано в Таблица 27.47; Таблица 27.48 показывает типы, используемые в пробах. Безусловно, можно добавить больше проб для улучшения наблюдаемости Tantor SE.

Таблица 27.47. Встроенные зонды DTrace

ИмяПараметрыОписание
transaction-start(LocalTransactionId)Проба, которая срабатывает в начале новой транзакции. arg0 - это идентификатор транзакции.
transaction-commit(LocalTransactionId)Проба, которая срабатывает, когда транзакция успешно завершается. arg0 - это идентификатор транзакции.
transaction-abort(LocalTransactionId)Проба, которая срабатывает, когда транзакция завершается неудачно. arg0 - это идентификатор транзакции.
query-start(const char *)Проба, которая срабатывает, когда начинается обработка запроса. arg0 - это строка запроса.
query-done(const char *)Проба, которая срабатывает, когда обработка запроса завершена. arg0 - это строка запроса.
query-parse-start(const char *)Проба, которая срабатывает при начале разбора запроса. arg0 - это строка запроса.
query-parse-done(const char *)Проба, которая срабатывает, когда разбор запроса завершен. arg0 - это строка запроса.
query-rewrite-start(const char *)Проба, которая срабатывает при начале переписывания запроса. arg0 - это строка запроса.
query-rewrite-done(const char *)Проба, которая срабатывает, когда переписывание запроса завершено. arg0 - это строка запроса.
query-plan-start()Проба, которая срабатывает при начале планирования запроса.
query-plan-done()Проба, которая срабатывает, когда планирование запроса завершено.
query-execute-start()Проба, которая срабатывает при начале выполнения запроса.
query-execute-done()Проба, которая срабатывает, когда выполнение запроса завершено.
statement-status(const char *)Проба, которая срабатывает каждый раз, когда процесс сервера обновляет свое pg_stat_activity.status. arg0 - это новая строка статуса.
checkpoint-start(int)Проба, которая срабатывает при запуске контрольной точки. arg0 содержит битовые флаги, используемые для различения различных типов контрольных точек, таких как выключение, немедленное или принудительное.
checkpoint-done(int, int, int, int, int)Проба, которая срабатывает, когда завершается контрольная точка. (Пробы, перечисленные далее, срабатывают последовательно во время обработки контрольной точки). arg0 - количество записанных буферов. arg1 - общее количество буферов. arg2, arg3 и arg4 содержат количество добавленных, удаленных и переработанных файлов журнала предзаписи соответственно.
clog-checkpoint-start(bool)Проба, которая срабатывает при начале CLOG-части контрольной точки. arg0 равно true для обычной контрольной точки и false для контрольной точки при выключении.
clog-checkpoint-done(bool)Проба, которая срабатывает, когда завершена часть CLOG контрольной точки. arg0 имеет тот же смысл, что и для clog-checkpoint-start.
subtrans-checkpoint-start(bool)Проба, которая запускается при начале подтранзакций контрольной точки. arg0 равно true для обычной контрольной точки и false для контрольной точки при выключении.
subtrans-checkpoint-done(bool)Проба, которая срабатывает, когда завершена часть SUBTRANS контрольной точки. arg0 имеет тот же смысл, что и для subtrans-checkpoint-start.
multixact-checkpoint-start(bool)Проба, которая срабатывает при начале части MultiXact контрольной точки. arg0 равно true для обычной контрольной точки и false для контрольной точки при выключении.
multixact-checkpoint-done(bool)Проба, которая срабатывает, когда завершена часть контрольной точки MultiXact. arg0 имеет тот же смысл, что и для multixact-checkpoint-start.
buffer-checkpoint-start(int)Проба, которая срабатывает, когда начинается запись буфера во время контрольной точки. arg0 содержит битовые флаги, используемые для различения различных типов контрольных точек, таких как выключение, немедленное или принудительное.
buffer-sync-start(int, int)Проба, которая срабатывает, когда мы начинаем записывать грязные буферы во время контрольной точки (после определения, какие буферы должны быть записаны). arg0 - общее количество буферов. arg1 - количество буферов, которые в настоящее время являются грязными и должны быть записаны.
buffer-sync-written(int)Проба, которая срабатывает после записи каждого буфера во время контрольной точки. arg0 - это идентификационный номер буфера.
buffer-sync-done(int, int, int)Проба, которая срабатывает, когда все грязные буферы были записаны. arg0 - общее количество буферов. arg1 - количество буферов, фактически записанных процессом контрольной точки. arg2 - количество буферов, которые должны были быть записаны (arg1 из buffer-sync-start); любое отличие отражает другие процессы, сбрасывающие буферы во время контрольной точки.
buffer-checkpoint-sync-start()Проба, которая срабатывает после записи грязных буферов в ядро и перед началом отправки запросов fsync.
buffer-checkpoint-done()Проба, которая срабатывает, когда синхронизация буферов с диском завершена.
twophase-checkpoint-start()Проба, которая запускается при начале двухфазной части контрольной точки.
twophase-checkpoint-done()Проба, которая срабатывает, когда двухфазная часть контрольной точки завершена.
buffer-read-start(ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool)Проба, которая срабатывает при начале чтения буфера. arg0 и arg1 содержат номера форка и блока страницы (но arg1 будет -1, если это запрос на расширение отношения). arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера. arg6 - true для запроса на расширение отношения, false для обычного чтения.
buffer-read-done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool, bool)Проба, которая срабатывает, когда чтение буфера завершено. arg0 и arg1 содержат номера форка и блока страницы (если это запрос на расширение отношения, arg1 теперь содержит номер блока, который был только что добавлен). arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера. arg6 - true для запроса на расширение отношения, false для обычного чтения. arg7 - true, если буфер был найден в пуле, false, если нет.
buffer-flush-start(ForkNumber, BlockNumber, Oid, Oid, Oid)Проба, которая срабатывает перед отправкой любого запроса на запись для общего буфера. arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение.
buffer-flush-done(ForkNumber, BlockNumber, Oid, Oid, Oid)Проба, которая срабатывает, когда запрос на запись завершен. (Обратите внимание, что это только отражает время передачи данных в ядро; они обычно еще не записаны на диск). Аргументы такие же, как и для buffer-flush-start.
buffer-write-dirty-start(ForkNumber, BlockNumber, Oid, Oid, Oid)Проба, которая срабатывает, когда серверный процесс начинает записывать грязный буфер. (Если это происходит часто, это означает, что shared_buffers слишком мал или параметры управления фоновым писателем требуют настройки). arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы табличного пространства, базы данных и отношения.
buffer-write-dirty-done(ForkNumber, BlockNumber, Oid, Oid, Oid)Проба, которая срабатывает, когда запись грязного буфера завершена. Аргументы такие же, как и для buffer-write-dirty-start.
wal-buffer-write-dirty-start()Проба, которая срабатывает, когда серверный процесс начинает записывать грязный буфер WAL, потому что больше нет свободного места в буфере WAL. (Если это происходит часто, это означает, что параметр wal_buffers слишком маленький).
wal-buffer-write-dirty-done()Probe that fires when a dirty WAL buffer write is complete.
wal-insert(unsigned char, unsigned char)Проба, которая срабатывает при вставке записи WAL. arg0 - это идентификатор ресурсного менеджера (rmid) для записи. arg1 содержит флаги информации.
wal-switch()Проба, которая срабатывает при запросе переключения сегмента WAL.
smgr-md-read-start(ForkNumber, BlockNumber, Oid, Oid, Oid, int)Проба, которая срабатывает при начале чтения блока из отношения. arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера.
smgr-md-read-done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int)Проба, которая срабатывает, когда чтение блока завершено. arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера. arg6 - это количество фактически прочитанных байтов, а arg7 - количество запрошенных (если они отличаются, это указывает на проблемы).
smgr-md-write-start(ForkNumber, BlockNumber, Oid, Oid, Oid, int)Проба, которая срабатывает при начале записи блока в отношение. arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера.
smgr-md-write-done(ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int)Проба, которая срабатывает, когда запись блока завершена. arg0 и arg1 содержат номера форка и блока страницы. arg2, arg3 и arg4 содержат идентификаторы OID табличного пространства, базы данных и отношения, идентифицирующие отношение. arg5 - это идентификатор backend, который создал временное отношение для локального буфера, или InvalidBackendId (-1) для общего буфера. arg6 - это количество фактически записанных байтов, а arg7 - количество запрошенных (если они отличаются, это указывает на проблемы).
sort-start(int, bool, int, int, bool, int)Проба, которая запускается при начале сортировки. arg0 указывает на сортировку кучи, индекса или данных. arg1 равно true для обеспечения уникальности значений. arg2 - количество ключевых столбцов. arg3 - количество килобайт рабочей памяти, разрешенной для использования. arg4 равно true, если требуется случайный доступ к результату сортировки. arg5 указывает на последовательность, когда 0, параллельного рабочего процесса, когда 1, или параллельного лидера, когда 2.
sort-done(bool, long)Проба, которая срабатывает, когда сортировка завершена. arg0 равно true для внешней сортировки, false для внутренней сортировки. arg1 - это количество блоков на диске, используемых для внешней сортировки, или килобайт памяти, используемых для внутренней сортировки.
lwlock-acquire(char *, LWLockMode)Проба, которая срабатывает, когда LWLock был захвачен. arg0 - это транша LWLock. arg1 - запрашиваемый режим блокировки, либо эксклюзивный, либо разделяемый.
lwlock-release(char *)Проба, которая срабатывает, когда LWLock был освобожден (но обратите внимание, что любые освобожденные ожидающие процессы еще не были активированы). arg0 - это транша LWLock.
lwlock-wait-start(char *, LWLockMode)Проба, которая срабатывает, когда LWLock не был немедленно доступен и серверный процесс начал ожидать, пока блокировка станет доступной. arg0 - это транша LWLock. arg1 - запрашиваемый режим блокировки, либо эксклюзивный, либо разделяемый.
lwlock-wait-done(char *, LWLockMode)Проба, которая срабатывает, когда процесс сервера был освобожден от ожидания LWLock (на самом деле он еще не имеет блокировки). arg0 - это транш LWLock. arg1 - запрошенный режим блокировки, либо эксклюзивный, либо разделяемый.
lwlock-condacquire(char *, LWLockMode)Проба, которая срабатывает, когда LWLock был успешно захвачен, когда вызывающий указал отсутствие ожидания. arg0 - это транша LWLock. arg1 - запрошенный режим блокировки, либо эксклюзивный, либо разделяемый.
lwlock-condacquire-fail(char *, LWLockMode)Проверка, которая срабатывает, когда LWLock не был успешно захвачен, когда вызывающий указал отсутствие ожидания. arg0 - это транша LWLock. arg1 - запрошенный режим блокировки, либо эксклюзивный, либо разделяемый.
lock-wait-start(unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, LOCKMODE)Проба, которая срабатывает, когда запрос на получение тяжеловесного блокировки (блокировки lmgr) начинает ожидать, потому что блокировка недоступна. arg0 до arg3 - это поля тега, идентифицирующие объект, который блокируется. arg4 указывает тип блокируемого объекта. arg5 указывает тип запрашиваемой блокировки.
lock-wait-done(unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, LOCKMODE)Проба, которая срабатывает, когда запрос на получение тяжеловесного блокировки (блокировки lmgr) завершается ожиданием (т.е. блокировка была получена). Аргументы такие же, как и для lock-wait-start.
deadlock-found()Проба, которая срабатывает, когда обнаруживается взаимоблокировка с помощью детектора взаимоблокировок.

Таблица 27.48. Определенные типы, используемые в параметрах пробы

ТипОпределение
LocalTransactionIdunsigned int
LWLockModeint
LOCKMODEint
BlockNumberunsigned int
Oidunsigned int
ForkNumberint
boolunsigned char

27.5.3. Использование зондов

Приведенный ниже пример показывает скрипт DTrace для анализа количества транзакций в системе в качестве альтернативы созданию снимка pg_stat_database до и после производительного теста:

#!/usr/sbin/dtrace -qs

postgresql$1:::transaction-start
{
      @start["Start"] = count();
      self->ts  = timestamp;
}

postgresql$1:::transaction-abort
{
      @abort["Abort"] = count();
}

postgresql$1:::transaction-commit
/self->ts/
{
      @commit["Commit"] = count();
      @time["Total time (ns)"] = sum(timestamp - self->ts);
      self->ts=0;
}

При выполнении примера D-скрипта выводится следующий результат:

# ./txn_count.d `pgrep -n postgres` or ./txn_count.d <PID>
^C

Start                                          71
Commit                                         70
Total time (ns)                        2312105013

Примечание

SystemTap использует другую нотацию для трассировочных скриптов, чем DTrace, хотя основные точки трассировки совместимы. Одно важное замечание состоит в том, что на данный момент SystemTap-скрипты должны ссылаться на имена проб с использованием двойного подчеркивания вместо дефисов. Ожидается, что это будет исправлено в будущих версиях SystemTap.

Вы должны помнить, что сценарии DTrace должны быть тщательно написаны и отлажены, иначе собранная информация о трассировке может быть бессмысленной. В большинстве случаев, когда обнаруживаются проблемы, виной являются инструменты, а не базовая система. При обсуждении информации, полученной с помощью динамической трассировки, убедитесь, что включили скрипт, используемый для этого, чтобы его можно было проверить и обсудить.

27.5.4. Определение новых зондов

Новые зонды могут быть определены в коде в любом месте, где разработчик пожелает, хотя это потребует перекомпиляции. Ниже приведены шаги для вставки новых зондов:

  1. Принять решение о названиях зондов и данных, которые будут доступны через зонды.

  2. Добавьте определения зондов в src/backend/utils/probes.d

  3. Включите pg_trace.h, если он еще не присутствует в модуле(ях), содержащих точки проб, и вставьте макросы проб TRACE_POSTGRESQL в желаемые места в исходном коде

  4. Перекомпилируйте и убедитесь, что новые пробы доступны

Пример:  Вот пример того, как вы можете добавить зонд для отслеживания всех новых транзакций по идентификатору транзакции.

  1. Решите, что зонд будет называться transaction-start и требует параметр типа LocalTransactionId.

  2. Добавьте определение пробы в src/backend/utils/probes.d:

    probe transaction__start(LocalTransactionId);
    

    Обратите внимание на использование двойного подчеркивания в имени пробы. В скрипте DTrace, использующем пробу, двойное подчеркивание должно быть заменено на дефис, поэтому transaction-start - это имя, которое следует использовать для документации для пользователей.

  3. Во время компиляции, transaction__start преобразуется в макрос с именем TRACE_POSTGRESQL_TRANSACTION_START (обратите внимание, что здесь используется одиночное подчеркивание), который доступен при включении pg_trace.h. Добавьте вызов макроса в соответствующее место в исходном коде. В данном случае, это будет выглядеть следующим образом:

    TRACE_POSTGRESQL_TRANSACTION_START(vxid.localTransactionId);
    

  4. После перекомпиляции и запуска нового двоичного файла проверьте, что ваша новая проба доступна, выполнив следующую команду DTrace. Вы должны увидеть аналогичный вывод:

    # dtrace -ln transaction-start
       ID    PROVIDER          MODULE           FUNCTION NAME
    18705 postgresql49878     postgres     StartTransactionCommand transaction-start
    18755 postgresql49877     postgres     StartTransactionCommand transaction-start
    18805 postgresql49876     postgres     StartTransactionCommand transaction-start
    18855 postgresql49875     postgres     StartTransactionCommand transaction-start
    18986 postgresql49873     postgres     StartTransactionCommand transaction-start
    

Есть несколько вещей, о которых нужно быть осторожным при добавлении макросов трассировки в код на C:

  • Вы должны обратить внимание на то, чтобы типы данных, указанные для параметров пробы, соответствовали типам данных переменных, используемых в макросе. В противном случае, возникнут ошибки компиляции.

  • На большинстве платформ, если Tantor SE собран с использованием --enable-dtrace, аргументы макроса трассировки будут вычисляться каждый раз, когда управление проходит через макрос, даже если трассировка не выполняется. Обычно это не стоит беспокоиться, если вы только сообщаете значения нескольких локальных переменных. Но будьте осторожны с включением дорогостоящих вызовов функций в аргументы. Если вам нужно это сделать, рассмотрите защиту макроса с проверкой, включена ли трассировка на самом деле:

    if (TRACE_POSTGRESQL_TRANSACTION_START_ENABLED())
        TRACE_POSTGRESQL_TRANSACTION_START(some_function(...));
    

    Каждый макрос трассировки имеет соответствующий макрос ENABLED.