Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Счётчики, метрики и трейспоинты в ClickHouse (черновик).

Мотивация

В ClickHouse разным образом реализовано некоторое количество функциональности, имеющей общие черты. Это:

  • метрики на основе инкрементальных счётчиков (ProfileEvents);
  • метрики на основе количества одновременных процессов (Metrics);
  • метрики на основе асинхронно получаемых значений (AsynchronousMetrics);
  • ограничения на сложность выполнения запроса (встроены в Settings);
  • прогресс выполнения запроса (Progress, отправляется клиенту);
  • статистика выполнения запроса (количество прочитанных и записанных строк и байт в ProcessList и QueryLog, а также в формате JSON);
  • отслеживание потребления оперативки, её ограничение и логгирование (MemoryTracker);
  • квоты на количество чего либо в интервал времени (Quota);
  • ограничение на количество одновременных запросов и возможность подождать (внутри ProcessList);
  • ограничение на потребление сетевой полосы (Throttler);
  • приостановка или замедление низкоприоритетных запросов (QueryPriorities);
  • логгирование информации о скорости выполнения определённых действий (Stopwatch и LOG_TRACE);
  • возможно, в этот список можно поместить и логгирование в целом.

Каждая из этих возможностей реализована несколько по-своему, что затрудняет добавление необходимых фич.

Примеры:

  • при превышении ограничения на общее количество одновременных запросов на сервере, можно настроить время ожидания, а при превышении ограничения на количество одновременных запросов на пользователя, возможно только сразу получить исключение;
  • ограничение на потребление оперативки проверяется на каждую аллокацию, что работает неэффективно для мелких аллокаций, и поэтому включено только для отдельных аллокаций (а часть аллокаций просто не учитывается);
  • информация о потреблении оперативки запросом не выгружается в clickhouse-client;
  • для ограничения сетевой полосы на один запрос и на все запросы одного пользователя, используются не связанные между собой настройки;
  • нет возможность ограничить потребление ресурса в виде доли от общего потребления, если общее потребление и количество потребителей больше порога. Последний пункт особенно важен для разделения ресурсов.

В связи с этим возникает вопрос - можно ли обобщить реализацию всей эту функциональности и получить больше?

Предлагаемая схема работы

Событие идентифицируется произвольной строкой. При наступлении события, может быть передано произвольное целое знаковое число типа Int64, обозначающее количество чего-либо. Из этих значений аккумулируются счётчики. (Также рассматривается вариант - возможность передавать ещё и произвольную строку - сообщение.)

Для одного события может вычисляться несколько счётчиков (синоним - метрики, сенсоры), отличающихся способом их вычисления из переданных в события чисел. Счётчик имеет тип Float64.

Пример: количество произошедших событий, сумма соответствующих чисел и среднее. Для счётчиков, которые могут, как увеличиваться, так и уменьшаться, дополнительно может вычисляться пиковое значение. Ещё пример: квантили переданных значений. Перечень счётчиков, которых необходимо вычислять, задаётся отдельно для каждого события (предпочтительный вариант; другой вариант - вычислять всевозможные производные счётчики всегда, даже если они бессмысленны).

События бывают точечные и интервальные. Точечные события не связаны друг с другом. Интервальные события связаны в пары: событие начала чего либо и событие окончания чего либо. Например, начало и окончание выполнения запроса. Другой пример - выделение и освобождение памяти (в качестве числа передаётся объём выделенной памяти). Для работы с интервальными событиями, может быть использована RAII обёртка, которая в конструкторе вызывает одно событие, а в деструкторе - другое. (Альтернативный вариант - использовать одно событие, но передавать в него положительные и отрицательные значения. В качестве счётчиков будет как сумма, так и количество и сумма только положительных или отрицательных значений.)

Для интервальных событий вычисляется несколько метрик:

  • Разность между началом и окончанием - количество чего-либо in-flight (в данный момент времени). Например - количество одновременно выполняющихся запросов или количество выделенной в данный момент памяти.
  • Время, прошедшее от начала до конца в наносекундах. Например - суммарное время выполнения запросов; суммарное время, потраченное на чтение данных.
  • Счётчик, делённый на суммарное время от начала до конца. Например - скорость чтения в байтах в наносекунду.

Счётчики аккумулируются в некотором контексте. Пример: контекст запроса, контекст пользователя, контекст базы данных, контекст сервера. Контексты образуют направленный ациклический граф. У каждого контекста может быть задано произвольное количество родительских контекстов. При возникновении события, счётчики обновляются во всех родительских контекстах (возможно сразу же, а возможно - асинхронно). Виды контекстов заранее определены, но некоторые виды контекстов могут быть параметризованы. Например, контекстом может быть хэш от нормализованного запроса (для throttling повторяющихся похожих запросов).

Для контекста может быть задан период полузатухания метрик для экспоненциального сглаживания. (Другой вариант - дополнительные метрики с экспоненциальным сглаживанием настраивается для каждой метрики по отдельности.) Например - для подсчёта среднего значения метрик за минуту, в частности - количества запросов пользователя в минуту. (Это нужно для реализации throttling и квот.) Для реализации этого сглаживания, сохраняется последнее время обновления метрики, и при следующем обновлении, последнее значение делится на степень двух, пропорциональную прошедшему времени. Так может быть задано несколько контекстов для подсчёта нескольких разных усреднений (за минуту, за час и т. п.).

Кроме того, все контексты начиная с контекста запроса и его родителей по цепочке, также имеют локальный и распределённый вариант (то есть, все метрики для контекстов, считаются сразу в двух вариантах). Для локального варианта, суммируются значения только в рамках одного сервера, а для распределённого - передаются и суммируются со всех удалённых серверов при распределённой обработке запроса. (Отдельно рассматривается вариант - также считать максимум по удалённым серверам, как например, максимум потребления оперативки по всем серверам). С помощью того же механизма, метрики передаются в clickhouse-client для возможности отображения.

Всё счётчики теряются при перезапуске сервера (хотя можно было бы рассмотреть асинхронный сброс в файл и загрузку). Глобальных счётчиков на кластер не существует - распределённые счётчики накапливаются на серверах, которые были точкой входа для распределённых запросов (хотя механизм счётчиков на весь кластер тоже можно было бы рассмотреть).

Для событий могут быть заданы триггеры. У триггера задаются условия срабатывания. Условием срабатывания является: тривиальное условие (срабатывает всегда); превышение счётчиком некоторого порога (пример, количество прочитанных запросом строк больше миллиарда); превышение счётчиком следующего кратного числа (пример, количество выделенной запросом оперативки превысило очередной гигабайт; для избежания дребезга, предыдущее значение запоминается); превышение счётчиком следующего числа, но с рандомизацией порога. Могут быть заданы несколько условий на один контекст и на родительский контекст (пример: потребление опретивки запросом больше 10 GB, а всеми запросами - больше 100 GB). (Возможно, потребуются сложные выражения со сравнениями и логическими связками).

Для триггера могут быть заданы действия. Примеры действий:

  • кинуть исключение (пример: превышено ограничение на количество запросов в минуту на пользователя);
  • подождать на condvar-е, пока условия не перестанут быть выполненными, но не более заданного времени, или продолжить / или кинуть исключение (пример: ограничение на количество одновременных запросов);
  • sleep с exponential backoff;
  • вывести сообщение о текущем значении метрики в лог (пример: вывести в лог сообщение при превышении потребления оперативки запросом очередного гигабайта);
  • записать информацию в системную таблицу;
  • sleep (для отладки);
  • trap (для отладки);

Срабатывание триггера также может рассматриваться как отдельное событие, для которого тоже вычисляются счётчики.

Должна быть возможность задавать и отключать триггеры динамически.

Во все крупные функции могут быть автоматически вставлены события во время компиляции программы с помощью LLVM XRay. События должны быть отключаемыми, чтобы в выключенном состоянии они были почти бесплатными.

Для хранения значений счётчиков используется обычная хэш-таблица. Для более-менее эффективного подсчёта счётчиков, они начинают накапливаться в контексте потока (а потом попадают в родительские контексты). Во всех контекстах кроме контекста потока, используется обычный mutex (это нужно по двум причинам: чтобы можно было ждать на condvar-е, и чтобы не беспокоиться о ресайзах хэш-таблицы). Контекст потока отличается от других контекстов тем, что обновления счётчиков из него могут поступать в родительские контексты асинхронно. Для этого, в контексте потоков считаются дельты счётчиков, которые сбрасываются: - всегда при завершении работы в потоке; - при достижении определённой величины (например: в потоке было выделено или освобождено ещё хотя бы 64 КБ памяти); - (под вопросом) при прошествии некоторого времени, которое вычисляется очень быстро из VDSO как CLOCK_MONOTONIC_COARSE или с помощью rdtscp. (Альтернативный вариант - использовать lock freе очередь или циклический буфер. Но это не позволяет вызывать некоторые события синхронно в контексте потока, в котором они произошли (например, чтобы кинуть исключение), и всё-таки требует атомарных операций.)

Для интроспекции:

  • значения счётчиков контекста сервера, на данный момент, доступны в системной таблице;
  • значения счётчиков контекста сервера, снимаемые каждый интервал времени (каждую секунду), записываются в лог в системной таблице;
  • значения счётчиков контекста сервера, снимаемые каждый интервал времени, экспортируются в Graphite;
  • циклический буфер последних событий доступен в системной таблице;
  • (под вопросом) все события асинхронно записываются в системную таблицу.
  • значения распределённых счётчиков передаются в clickhouse-client.

Конфигурация событий определяет:

  • какие события следует учитывать;
  • какие метрики следует вычислять для события;
  • какие триггеры и с какими условиями, в каких контекстах и с какими действиями, навешаны на какие метрики.

Эта конфигурация может быть задана на запрос, на профиль пользователя, или глобально. Глобальная конфигурация может влиять на события, которые происходят не в контексте запроса (например - начало и окончание мержей).

Каждый поток копирует под mutex-ом себе COWPtr на конфигурацию при старте и использует её всё время, либо долгоживущий поток может обновлять её по своему желанию. Таким образом, непосредственно во время событий, доступ к конфигурации не требует синхронизации.

Как конфигурировать. Пока непонятно, есть дурацкие варианты:

ALTER EVENT read_uncompressed ADD METRIC count ALTER EVENT memory_allocation ADD TRIGGER memory_limit FOR USER WHERE sum > 10000000000 ACTION THROW

Похожие решения

В связи с некоторой спецификой (граф контекстов, локальные и распределённые счётчики, синхронный и асинхронный вызов событий), шанс на то, что удастся применить какую-либо готовую библиотеку, очень низкий. Но существующие библиотеки обязательно следует изучить.

HandyStats: https://github.com/yandex/handystats/

  • open-source от Яндекса;
  • заброшена и не поддерживается;
  • нет контекстов;
  • обработка событий идёт только асинхронно (message passing), что хорошо для производительности, но не позволяет вызвать триггер в том же потоке, в котором произошло событие.

User Statically-Defined Tracing: https://lwn.net/Articles/753601/

  • используется в MySQL, PostgreSQL, Node.js, Java, Glibc...
  • почти нулевой оверхед от неактивных проб;
  • поддержка на уровне ELF;
  • много инструментов для работы;
  • скорее всего, надо сделать так, что наши события будут также этими пробами. В отличие от этой функциональности, нашей целью является использование событий не только для профилирования и трассировки, но и для обычной бизнес логики, такой как ограничение на использование ресурсов, а также вычисление доступной пользователю статистики.

LWTrace: не open-source (см. внутреннюю Wiki)

  • сложный способ конфигурирования.
@filimonov

This comment has been minimized.

Copy link

filimonov commented Jul 1, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.