Семплирующий профилировщик запросов

Cемплирующий профилировщик запросов (sampling query profiler) предоставляет возможность интроспекции производительности запросов в ADQM. Например, с его помощью в исходном коде можно найти функции, которые наиболее часто вызываются во время выполнения запроса, или отследить затрачиваемое процессорное и реальное время, включая время простоя.

Включение профилировщика запросов

  1. В интерфейсе ADCM на странице конфигурации сервиса ADQMDB активируйте опцию Show advanced и в секции Log settings включите настройку trace_log, чтобы профилировщик запросов начал собирать трассировки стека и сохранять их в таблицу trace_log. В этой же секции настройте параметры таблицы trace_log:

    • Database — имя базы данных, в которой будет храниться таблица;

    • Flush interval, milliseconds — интервал сброса данных из буфера памяти в таблицу (в миллисекундах);

    • TTL, days — время хранения строк в таблице (в днях).

    Нажмите Save и выполните действие Reconfig and restart для сервиса ADQMDB, чтобы сохранить информацию об указанных параметрах конфигурации и перезапустить сервис.

    ПРИМЕЧАНИЕ
    • Ключом партиционирования таблицы trace_log автоматически устанавливается столбец event_date — дата в момент получения экземпляра трассировки стека. Подробнее столбцы таблицы описаны в статье trace_log.

    • Данные в таблице trace_log актуальны только для работающего сервера. После перезапуска сервера ADQM таблица не очищается и все сохраненные адреса виртуальной памяти могут стать недействительными.

  2. С помощью параметров query_profiler_real_time_period_ns и query_profiler_cpu_time_period_ns настройте таймер реального времени и таймер CPU, чтобы определить периодичность, с которой профилировщик запросов будет собирать трассировки стека и записывать их в таблицу trace_log.

     
    По умолчанию оба таймера настроены на выполнение одной выборки в секунду. Такая частота выборки позволяет собрать достаточно информации о кластере ADQM, при этом профилирование не влияет на производительность серверов ADQM. Если необходимо профилировать каждый запрос отдельно, можно попробовать использовать более высокую частоту выборки, например, 100 раз в секунду:

    SET query_profiler_real_time_period_ns=10000000;
    SET query_profiler_cpu_time_period_ns=10000000;

    Собирать трассировки стека с частотой выше нескольких тысяч выборок в секунду обычно невозможно.

Анализ отчетов профилировщика

  1. Установите пакет adqm-clickhouse-debuginfo.

  2. Включите функции интроспекции для профилирования запросов (в целях безопасности эти функции отключены по умолчанию):

    SET allow_introspection_functions = 1;
  3. Используйте функции интроспекции для анализа данных таблицы trace_log (см. следующий раздел).

Функции интроспекции

В этом разделе описаны функции, которые можно использовать для анализа форматов ELF и DWARF при профилировании запросов.

Предварительные настройки для выполнения примеров
  1. Перед выполнением примеров использования функций интроспекции, убедитесь, что пакет adqm-clickhouse-debuginfo установлен и активирована настройка allow_introspection_functions = 1.

  2. В приведенных примерах функции интроспекции применяются для анализа первого экземпляра трассировки стека из таблицы trace_log. Используйте следующий запрос, чтобы получить первую строку этой таблицы:

    SELECT * FROM system.trace_log LIMIT 1 FORMAT Vertical;
    Row 1:
    ──────
    event_date:              2023-09-04
    event_time:              2023-09-04 10:18:01
    event_time_microseconds: 2023-09-04 10:18:01.858935
    timestamp_ns:            1693822681858935963
    revision:                54472
    trace_type:              Memory
    thread_id:               11216
    query_id:                7b53b488-6e38-495a-ab63-e0ed985bf2de
    trace:                   [282096402,282019952,282019801,282176985,373506363,387689543,282729773,282744526,140712295374501,140712292395789]
    size:                    8760
    event:
    increment:               0

    Поле trace содержит трассировку стека в момент выборки, где каждый элемент массива — это адрес виртуальной памяти внутри процесса сервера ADQM. Функции интроспекции можно применять к одному адресу или ко всей трассировке стека.

addressToLine

Функция addressToLine преобразует адрес виртуальной памяти внутри процесса сервера ADQM в имя файла и номер строки в исходном коде.

Cинтаксис вызова функции:

addressToLine(<address_of_binary_instruction>)

где <address_of_binary_instruction> — адрес инструкции в запущенном процессе (значение типа UInt64).

Функция возвращает значение типа String:

  • Имя файла исходного кода и номер строки в этом файле, разделяемые двоеточием.

  • Имя бинарного файла, если функция не может найти отладочную информацию.

  • Пустую строку, если передаваемый в качестве аргумента адрес не является допустимым.

Пример
  1. Получите имя файла исходного кода и номер строки для одного адреса:

    SELECT addressToLine(282096402::UInt64);

    В выводе результата выполнения функции ./build/src/Common/StackTrace.cpp — имя файла, а 287 — номер строки:

    ┌─addressToLine(CAST('282096402', 'UInt64'))─┐
    │ ./build/src/Common/StackTrace.cpp:287      │
    └────────────────────────────────────────────┘
  2. Примените функцию ко всей трассировке стека. Используйте функцию arrayMap, чтобы обработать каждый отдельный элемент массива trace с помощью функции addressToLine:

    SELECT arrayStringConcat(arrayMap(x -> addressToLine(x), trace), '\n') AS trace_source_code_lines
    FROM system.trace_log
    LIMIT 1
    FORMAT Vertical;
    Row 1:
    ──────
    trace_source_code_lines: ./build/src/Common/StackTrace.cpp:287
    ./build/src/Common/MemoryTracker.cpp:202
    ./build/src/Common/MemoryTracker.cpp:338
    ./build/src/Common/ThreadStatus.cpp:185
    ./build/contrib/llvm-project/libcxx/include/__memory/unique_ptr.h:302
    ./build/contrib/llvm-project/libcxx/include/__memory/shared_ptr.h:701
    ./build/base/base/wide_integer_impl.h:789
    ./build/contrib/llvm-project/libcxx/include/__memory/unique_ptr.h:302
    /usr/lib64/libpthread-2.17.so
    /usr/lib64/libc-2.17.so

addressToLineWithInlines

Функция addressToLineWithInlines похожа на addressToLine, но возвращает массив со всеми встроенными функциями (поэтому она вычисляется медленнее).

Cинтаксис вызова функции:

addressToLineWithInlines(<address_of_binary_instruction>)

где <address_of_binary_instruction> — адрес инструкции в запущенном процессе (значение типа UInt64).

Функция возвращает массив строк:

  • Массив, в котором первый элемент содержит имя файла исходного кода и номер строки в этом файле (разделенные двоеточием), остальные элементы включают информацию о встроенных функциях — имя файла исходного кода с встроенной функцией, номер строки и имя функции.

  • Массив с одним элементом — именем бинарного файла, если функция не смогла найти отладочную информацию.

  • Пустой массив, если передаваемый в качестве аргумента адрес не является допустимым.

Пример
  1. Примените функцию к одному адресу:

    SELECT addressToLineWithInlines(373506363::UInt64) FORMAT Vertical;
    Row 1:
    ──────
    addressToLineWithInlines(CAST('373506363', 'UInt64')): ['./build/src/Interpreters/ThreadStatusExt.cpp:185','./build/contrib/llvm-project/libcxx/include/__memory/unique_ptr.h:302:std::__1::unique_ptr<DB::QueryProfilerReal, std::__1::default_delete<DB::QueryProfilerReal>>::reset[abi:v15000](DB::QueryProfilerReal*)','./build/src/Interpreters/ThreadStatusExt.cpp:415:DB::ThreadStatus::finalizeQueryProfiler()']
  2. Примените функцию ко всей трассировке стека. Используйте функцию arrayJoin, чтобы разделить массив на строки:

    SELECT address, addressToLineWithInlines(arrayJoin(trace) AS address)
    FROM system.trace_log
    WHERE query_id = '7b53b488-6e38-495a-ab63-e0ed985bf2de'
    FORMAT Vertical;
    Row 1:
    ──────
    address:                                    282096402
    addressToLineWithInlines(arrayJoin(trace)): ['./build/src/Common/StackTrace.cpp:287']
    
    Row 2:
    ──────
    address:                                    282019952
    addressToLineWithInlines(arrayJoin(trace)): ['./build/src/Common/MemoryTracker.cpp:202']
    
    Row 3:
    ──────
    address:                                    282019801
    addressToLineWithInlines(arrayJoin(trace)): ['./build/src/Common/MemoryTracker.cpp:338']
    
    Row 4:
    ──────
    address:                                    282176985
    addressToLineWithInlines(arrayJoin(trace)): ['./build/src/Common/ThreadStatus.cpp:185']
    
    Row 5:
    ──────
    address:                                    373506363
    addressToLineWithInlines(arrayJoin(trace)): ['./build/src/Interpreters/ThreadStatusExt.cpp:185','./build/contrib/llvm-project/libcxx/include/__memory/unique_ptr.h:302:std::__1::unique_ptr<DB::QueryProfilerReal, std::__1::default_delete<DB::QueryProfilerReal>>::reset[abi:v15000](DB::QueryProfilerReal*)','./build/src/Interpreters/ThreadStatusExt.cpp:415:DB::ThreadStatus::finalizeQueryProfiler()']
    
    ...

addressToSymbol

Функция addressToSymbol преобразует адрес виртуальной памяти внутри процесса сервера ADQM в символ из объектного файла.

Cинтаксис вызова функции:

addressToSymbol(<address_of_binary_instruction>)

где <address_of_binary_instruction> — адрес инструкции в запущенном процессе (значение типа UInt64).

Функция возвращает значение типа String:

  • Символ из объектного файла.

  • Пустую строку, если передаваемый в качестве аргумента адрес не является допустимым.

Пример
  1. Получите символ для одного адреса:

    SELECT addressToSymbol(282096402::UInt64);
    ┌─addressToSymbol(CAST('282096402', 'UInt64'))─┐
    │ _ZN10StackTrace10tryCaptureEv                │
    └──────────────────────────────────────────────┘
  2. Примените функцию ко всей трассировке стека. Используйте функцию arrayMap, чтобы обработать каждый отдельный элемент массива trace с помощью функции addressToSymbol:

    SELECT arrayStringConcat(arrayMap(x -> addressToSymbol(x), trace), '\n') AS trace_symbols
    FROM system.trace_log
    LIMIT 1
    FORMAT Vertical;
    Row 1:
    ──────
    trace_symbols: _ZN10StackTrace10tryCaptureEv
    _ZN13MemoryTracker9allocImplElbPS_
    _ZN13MemoryTracker9allocImplElbPS_
    _ZN2DB12ThreadStatus20flushUntrackedMemoryEv
    _ZN2DB12ThreadStatus15detachFromGroupEv
    _ZNSt3__110__function16__policy_invokerIFvvEE11__call_implINS0_20__default_alloc_funcIZN24ThreadFromGlobalPoolImplILb1EEC1IZN2DB28PullingAsyncPipelineExecutor4pullERNS9_5ChunkEmE3$_0JEEEOT_DpOT0_EUlvE_S2_EEEEvPKNS0_16__policy_storageE
    _ZN14ThreadPoolImplINSt3__16threadEE6workerENS0_15__list_iteratorIS1_PvEE
    _ZNSt3__114__thread_proxyB6v15000INS_5tupleIJNS_10unique_ptrINS_15__thread_structENS_14default_deleteIS3_EEEEZN14ThreadPoolImplINS_6threadEE12scheduleImplIvEET_NS_8functionIFvvEEElNS_8optionalImEEbEUlvE0_EEEEEPvSJ_
    start_thread
    __clone

demangle

Функция demangle преобразует символ, который можно получить с помощью функции addressToSymbol, в имя функции C++.

Cинтаксис вызова функции:

demangle(<symbol>)

где <symbol> — символ из объектного файла (значение типа String).

Функция возвращает значение типа String:

  • Имя функции C++.

  • Пустую строку, если передаваемый в качестве аргумента символ не является допустимым.

Пример
  1. Получите имя функции для одного адреса:

    SELECT demangle(addressToSymbol(282096402::UInt64));
    ┌─demangle(addressToSymbol(CAST('282096402', 'UInt64')))─┐
    │ StackTrace::tryCapture()                               │
    └────────────────────────────────────────────────────────┘
  2. Примените функцию ко всей трассировке стека. Используйте функцию arrayMap, чтобы обработать каждый отдельный элемент массива trace с помощью функции demangle:

    SELECT arrayStringConcat(arrayMap(x -> demangle(addressToSymbol(x)), trace), '\n') AS trace_functions
    FROM system.trace_log
    LIMIT 1
    FORMAT Vertical;
    Row 1:
    ──────
    trace_functions: StackTrace::tryCapture()
    MemoryTracker::allocImpl(long, bool, MemoryTracker*)
    MemoryTracker::allocImpl(long, bool, MemoryTracker*)
    DB::ThreadStatus::flushUntrackedMemory()
    DB::ThreadStatus::detachFromGroup()
    void std::__1::__function::__policy_invoker<void ()>::__call_impl<std::__1::__function::__default_alloc_func<ThreadFromGlobalPoolImpl<true>::ThreadFromGlobalPoolImpl<DB::PullingAsyncPipelineExecutor::pull(DB::Chunk&, unsigned long)::$_0>(DB::PullingAsyncPipelineExecutor::pull(DB::Chunk&, unsigned long)::$_0&&)::'lambda'(), void ()>>(std::__1::__function::__policy_storage const*)
    ThreadPoolImpl<std::__1::thread>::worker(std::__1::__list_iterator<std::__1::thread, void*>)
    void* std::__1::__thread_proxy[abi:v15000]<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct>>, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, long, std::__1::optional<unsigned long>, bool)::'lambda0'()>>(void*)
    start_thread
    __clone

tid

Функция tid возвращает идентификатор потока, в котором обрабатывается текущий Block.

Cинтаксис вызова функции:

tid()

Функция возвращает значение типа Uint64.

Пример
SELECT tid();
┌─tid()─┐
│ 10959 │
└───────┘

logTrace

Функция logTrace выводит сообщение в лог сервера для каждого Block.

Синтаксис вызова функции:

logTrace('<message>')

где <message> — сообщение, которое отправляется в серверный лог.

Функция всегда возвращает 0.

Пример
SELECT logTrace('logTrace message');
┌─logTrace('logTrace message')─┐
│                            0 │
└──────────────────────────────┘
Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней