Проектирование схемы для мониторинга
Мы рекомендуем пользователям всегда создавать собственную схему для логов и трассировки по следующим причинам:
- Выбор первичного ключа - В схемах по умолчанию используется
ORDER BY
, который оптимизирован для определенных шаблонов доступа. Вряд ли ваши шаблоны доступа будут соответствовать этому. - Извлечение структуры - Пользователи могут захотеть извлечь новые колонки из существующих колонок, например, из колонки
Body
. Это можно сделать с помощью материализованных колонок (а в более сложных случаях – с помощью материализованных представлений). Это требует изменения схемы. - Оптимизация Maps - По умолчанию схемы используют тип Map для хранения атрибутов. Эти колонки позволяют хранить произвольные метаданные. Хотя это важная возможность, так как метаданные событий зачастую не определены заранее и, следовательно, не могут быть сохранены в строго типизированной базе данных, такой как ClickHouse, доступ к ключам map и их значениям не так эффективен, как доступ к обычным колонкам. Мы решаем эту проблему, модифицируя схему и обеспечивая, чтобы наиболее часто используемые ключи map были колонками верхнего уровня - см. "Извлечение структуры с помощью SQL". Это требует изменения схемы.
- Упрощение доступа к ключам map - Доступ к ключам в map требует более громоздкого синтаксиса. Пользователи могут смягчить это с помощью псевдонимов. См. "Использование псевдонимов", чтобы упростить запросы.
- Вторичные индексы - Схема по умолчанию использует вторичные индексы для ускорения доступа к Maps и ускорения текстовых запросов. Они обычно не требуются и требуют дополнительного места на диске. Их можно использовать, но необходимо протестировать, чтобы убедиться, что они необходимы. См. "Вторичные / Индексы пропуска данных".
- Использование кодеков - Пользователи могут захотеть настроить кодеки для колонок, если они понимают ожидаемые данные и имеют доказательства того, что это улучшает сжатие.
Мы подробно описываем каждый из вышеупомянутых случаев использования ниже.
Важно: Хотя пользователям рекомендуется расширять и модифицировать свою схему для достижения оптимального сжатия и производительности запросов, они должны следовать правилам наименования схемы OTel для основных колонок, где это возможно. Плагин ClickHouse для Grafana предполагает наличие некоторых базовых колонок OTel для помощи в построении запросов, таких как Timestamp и SeverityText. Обязательные колонки для логов и трассировок задокументированы здесь [1][2] и здесь, соответственно. Вы можете изменить эти имена колонок, переопределив значения по умолчанию в конфигурации плагина.
Извлечение структуры с помощью SQL
При загрузке структурированных или неструктурированных логов пользователям часто нужна возможность:
- Извлекать колонки из строковых блобов. Запросы к ним будут быстрее, чем использование строковых операций во время выполнения запроса.
- Извлекать ключи из map. В схеме по умолчанию произвольные атрибуты помещаются в колонки типа Map. Этот тип предоставляет возможность без схемы, что имеет преимущество, поскольку пользователи не нуждаются в предварительном определении колонок для атрибутов при создании логов и трассировок - это часто невозможно при сборе логов от Kubernetes и желании сохранить метки подов для последующего поиска. Доступ к ключам map и их значениям медленнее, чем запросы к обычным колонкам ClickHouse. Следовательно, извлечение ключей из map в корневые колонки таблицы часто является желаемым.
Рассмотрим следующие запросы:
Предположим, что мы хотим подсчитать, какие URL-адреса получают наибольшее количество POST-запросов с использованием структурированных логов. JSON-блоб хранится в колонке Body
в виде строки. Кроме того, он также может быть сохранен в колонке LogAttributes
как Map(String, String)
, если пользователь включил json_parser в сборщике.
Если LogAttributes
доступен, запрос для подсчета того, какие URL-адреса сайта получают наибольшее количество POST-запросов:
Обратите внимание на использование синтаксиса map здесь, например, LogAttributes['request_path']
, и на функцию path
для удаления параметров запроса из URL.
Если пользователь не включил парсинг JSON в сборщике, то LogAttributes
будет пустым, что заставит нас использовать функции JSON для извлечения колонок из строки Body
.
Мы обычно рекомендуем пользователям выполнять парсинг JSON в ClickHouse для структурированных логов. Мы уверены, что ClickHouse – это самая быстрая реализация парсинга JSON. Однако мы понимаем, что пользователи могут захотеть отправить логи в другие источники и не иметь этой логики в SQL.
Теперь рассмотрим то же самое для неструктурированных логов:
Аналогичный запрос для неструктурированных логов требует использования регулярных выражений через функцию extractAllGroupsVertical
.
Увеличенная сложность и стоимость запросов для парсинга неструктурированных логов (обратите внимание на разницу в производительности) – вот почему мы рекомендуем пользователям всегда использовать структурированные логи, где это возможно.
Вышеупомянутый запрос может быть оптимизирован для использования словарей регулярных выражений. См. Использование словарей для получения более детальной информации.
Оба эти случая использования могут быть реализованы с помощью ClickHouse, перемещая вышеуказанную логику запроса на время вставки. Мы исследуем несколько подходов ниже, подчеркивая, когда каждый из них подходит.
Пользователи также могут выполнять обработку с использованием процессоров и операторов OTel Collector, как описано здесь. В большинстве случаев пользователи обнаружат, что ClickHouse значительно более эффективен с точки зрения ресурсов и быстрее, чем процессоры сборщика. Основной недостаток выполнения всей обработки событий в SQL заключается в связывании вашего решения с ClickHouse. Например, пользователи могут захотеть отправить обработанные логи в альтернативные пункты назначения из сборщика OTel, например, в S3.
Материализованные колонки
Материализованные колонки предлагают наиболее простой способ извлечь структуру из других колонок. Значения таких колонок всегда рассчитываются во время вставки и не могут быть указаны в запросах INSERT.
Материализованные колонки требуют дополнительного объема хранилища, так как значения извлекаются в новые колонки на диске во время вставки.
Материализованные колонки поддерживают любое выражение ClickHouse и могут использовать любые аналитические функции для обработки строк (включая regex и поиск) и url, выполняя преобразования типов, извлекая значения из JSON или математические операции.
Мы рекомендуем использовать материализованные колонки для базовой обработки. Они особенно полезны для извлечения значений из map, продвижения их в корневые колонки и выполнения преобразования типов. Они часто наиболее полезны при использовании в очень простых схемах или в сочетании с материализованными представлениями. Рассмотрим следующую схему для логов, из которой JSON был извлечен в колонку LogAttributes
сборщиком:
Эквивалентная схема для извлечения с использованием функций JSON из строки Body
может быть найдена здесь.
Наши три материализованные колонки извлекают страницу запроса, тип запроса и домен реферера. Они обращаются к ключам map и применяют функции к их значениям. Наш последующий запрос значительно быстрее:
Материализованные колонки по умолчанию не будут возвращены в SELECT *
. Это сделано для сохранения инварианта, что результат SELECT *
всегда может быть повторно вставлен в таблицу с помощью INSERT. Это поведение можно отключить, установив asterisk_include_materialized_columns=1
, и можно включить в Grafana (см. Дополнительные настройки -> Пользовательские настройки
в конфигурации источника данных).
Материализованные представления
Материализованные представления предоставляют более мощный способ применения фильтрации SQL и преобразований к логам и трассировкам.
Материализованные представления позволяют пользователям переместить стоимость вычислений с времени выполнения запроса на время вставки. Материализованное представление ClickHouse – это просто триггер, который запускает запрос на блоках данных по мере их вставки в таблицу. Результаты этого запроса вставляются во вторую "целевую" таблицу.

Материализованные представления в ClickHouse обновляются в реальном времени по мере поступления данных в таблицу, на которой они основаны, функционируя более как постоянно обновляемые индексы. Напротив, в других базах данных материализованные представления обычно являются статическими снимками запроса, которые необходимо обновить (по аналогии с обновляемыми материализованными представлениями ClickHouse).
Запрос, связанный с материализованным представлением, может теоретически быть любым запросом, включая агрегацию, хотя существуют ограничения при использовании Joins. Для преобразований и фильтрации, требуемых для логов и трассировок, пользователи могут считать любое заявление SELECT
возможным.
Пользователям стоит помнить, что запрос является просто триггером, выполняющимся над вставляемыми в таблицу строками (исходная таблица), а результаты отправляются в новую таблицу (целевая таблица).
Чтобы убедиться, что мы не сохраняем данные дважды (в исходной и целевой таблицах), мы можем изменить таблицу исходной таблицы на движок таблицы Null, сохраняя оригинальную схему. Наши сборщики OTel продолжат отправлять данные в эту таблицу. Например, для логов таблица otel_logs
становится:
Движок таблицы Null - это мощная оптимизация - рассмотрите его как /dev/null
. Эта таблица не будет хранить данные, но любые прикрепленные материализованные представления по-прежнему будут выполняться над вставленными строками до их удаления.
Рассмотрим следующий запрос. Он преобразует наши строки в формат, который мы хотим сохранить, извлекая все колонки из LogAttributes
(предполагаем, что это было установлено сборщиком с использованием оператора json_parser
), устанавливая SeverityText
и SeverityNumber
(на основе некоторых простых условий и определения этих колонок). В этом случае мы также выбираем только те колонки, которые, как мы знаем, будут заполнены - игнорируя такие колонки, как TraceId
, SpanId
и TraceFlags
.
Мы также извлекаем колонку Body
выше - на случай, если позже будут добавлены дополнительные атрибуты, которые не будут извлечены нашим SQL. Эта колонка должна хорошо сжиматься в ClickHouse и будет редко запрашиваться, что не повлияет на производительность запроса. Наконец, мы сокращаем Timestamp до DateTime (для экономии места - см. "Оптимизация типов") с приведением типа.
Обратите внимание на использование условных операторов выше для извлечения SeverityText
и SeverityNumber
. Они очень полезны для формирования сложных условий и проверки того, установлены ли значения в map – мы наивно предполагаем, что все ключи существуют в LogAttributes
. Рекомендуем пользователям ознакомиться с ними – они ваши друзья в парсинге логов, помимо функций для работы с значениями null!
Нам требуется таблица для получения этих результатов. Ниже целевая таблица соответствует вышеупомянутому запросу:
Выбранные типы основаны на оптимизациях, обсуждаемых в "Оптимизация типов".
Обратите внимание, как мы существенно изменили нашу схему. На самом деле пользователи, вероятно, также будут иметь колонки Trace, которые они захотят сохранить, а также колонку ResourceAttributes
(которая обычно содержит метаданные Kubernetes). Grafana может использовать колонки trace для предоставления функциональности ссылок между логами и трассировками - см. "Использование Grafana".
Ниже мы создаем материализованное представление otel_logs_mv
, которое выполняет указанный выше выбор для таблицы otel_logs
и отправляет результаты в otel_logs_v2
.
Это визуализировано ниже:

Если мы теперь перезапустим конфигурацию сборщика, используемую в "Экспорт в ClickHouse", данные появятся в otel_logs_v2
в нашем желаемом формате. Обратите внимание на использование типизированных функций извлечения JSON.
Эквивалентное материализованное представление, которое зависит от извлечения колонок из колонки Body
с использованием функций JSON, показано ниже:
Осторожно с типами
Вышеупомянутые материализованные представления зависят от неявного приведения типов - особенно в случае использования карты LogAttributes
. ClickHouse часто прозрачно приводит извлеченные значения к типу целевой таблицы, что уменьшает требуемый синтаксис. Однако мы рекомендуем пользователям всегда проверять свои представления, используя оператор SELECT
для этих представлений с оператором INSERT INTO
, используя целевую таблицу с той же схемой. Это должно подтвердить, что типы обрабатываются правильно. Особое внимание следует уделить следующим случаям:
- Если ключа нет в map, будет возвращена пустая строка. В случае чисел пользователям потребуется сопоставить их с подходящим значением. Это можно сделать с помощью условных операторов, например,
if(LogAttributes['status'] = ", 200, LogAttributes['status'])
, или функций приведения типов, если допустимы значения по умолчанию, например,toUInt8OrDefault(LogAttributes['status'] )
. - Некоторые типы не всегда будут приведены, например, строковые представления чисел не будут приведены к значениям перечисления.
- Функции извлечения JSON возвращают значения по умолчанию для своего типа, если значение не найдено. Убедитесь, что эти значения имеют смысл!
Избегайте использования Nullable в ClickHouse для данных Мониторинга. Чаще всего в логах и трассировках не требуется различать пустое значение и null. Эта функция требует дополнительного объема хранилища и отрицательно сказывается на производительности запроса. См. здесь для получения дополнительной информации.
Выбор первичного (упорядочивающего) ключа
После того, как вы извлекли желаемые колонки, вы можете начать оптимизацию вашего упорядочивающего/первичного ключа.
Некоторые простые правила могут быть применены, чтобы помочь выбрать упорядочивающий ключ. Следующее иногда может быть в конфликте, поэтому рассмотрите их по порядку. Пользователи могут идентифицировать несколько ключей в этом процессе, при этом 4-5 обычно достаточно:
- Выбирайте колонки, которые соответствуют вашим общим фильтрам и шаблонам доступа. Если пользователи обычно начинают расследования по мониторингу, фильтруя по определенной колонке, например, по имени пода, эта колонка будет часто использоваться в условиях
WHERE
. Приоритизируйте их включение в ваш ключ выше тех, которые используются реже. - Предпочитайте колонки, которые помогают исключить большой процент всех строк при фильтрации, тем самым уменьшая объем данных, которые нужно читать. Имена сервисов и коды состояния часто являются хорошими кандидатами - в последнем случае только если пользователи фильтруют по значениям, которые исключают большинство строк, например, фильтрация по 200 будет соответствовать большинству строк в большинстве систем, в отличие от ошибок 500, которые будут соответствовать небольшой подмножеству.
- Предпочитайте колонки, которые, вероятно, будут высоко коррелированы с другими колонками в таблице. Это поможет гарантировать, что эти значения также будут храниться непрерывно, улучшая сжатие.
- Операции
GROUP BY
иORDER BY
для колонок в упорядочивающем ключе могут быть более эффективными с точки зрения памяти.
Определив подмножество колонок для упорядочивающего ключа, их необходимо объявить в специфическом порядке. Этот порядок может существенно повлиять на эффективность фильтрации по вторичным ключевым колонкам в запросах и на коэффициент сжатия для файлов данных таблицы. В общем, лучше всего упорядочить ключи в порядке возрастания кардинальности. Это следует сбалансировать с учетом того, что фильтрация по колонкам, которые появляются позже в упорядочивающем ключе, будет менее эффективной, чем фильтрация по тем, которые появляются ранее в кортеже. Уравновесьте эти поведения и учитывайте ваши шаблоны доступа. Важнее всего протестировать варианты. Для дальнейшего понимания упорядочивающих ключей и их оптимизации мы рекомендуем данную статью.
Мы рекомендуем определиться с вашими упорядочивающими ключами после структурирования ваших логов. Не используйте ключи в атрибутах map для упорядочивающего ключа или выражений извлечения JSON. Убедитесь, что ваши упорядочивающие ключи являются корневыми колонками в вашей таблице.
Использование map
Ранее приведенные примеры показывают использование синтаксиса map map['key']
для доступа к значениям в колонках Map(String, String)
. А также специальные функции ClickHouse для map map functions доступны для фильтрации или выбора этих колонок.
Например, следующий запрос определяет все уникальные ключи, доступные в колонке LogAttributes
, используя функцию mapKeys
, после чего применяется функция groupArrayDistinctArray
(комбинатор).
Мы не рекомендуем использовать точки в названиях колонок Map и можем отказаться от их использования. Используйте _
.
Использование псевдонимов
Запросы к типам map медленнее, чем запросы к обычным колонкам - см. "Ускорение запросов". В дополнение, это более синтаксически сложно и может быть громоздко для пользователей. Чтобы решить эту последнюю проблему, мы рекомендуем использовать колонки Alias.
Колонки ALIAS вычисляются во время выполнения запроса и не хранятся в таблице. Следовательно, невозможно вставить значение в колонку этого типа. Используя псевдонимы, мы можем ссылаться на ключи map и упрощать синтаксис, прозрачным образом экспортируя записи map как обычную колонку. Рассмотрим следующий пример:
У нас есть несколько материализованных колонок и колонка ALIAS
, RemoteAddr
, которая обращается к map LogAttributes
. Теперь мы можем запрашивать значения LogAttributes['remote_addr']
через эту колонку, тем самым упрощая наш запрос, т.е.
Более того, добавление ALIAS
не вызывает трудностей через команду ALTER TABLE
. Эти колонки становятся доступными немедленно, например:
По умолчанию SELECT *
исключает колонки ALIAS. Это поведение можно отключить, установив asterisk_include_alias_columns=1
.
Оптимизация типов
Общие рекомендации по оптимизации типов Clickhouse применимы к случаю ClickHouse.
Использование кодеков
Помимо оптимизации типов, пользователи могут следовать общим рекомендациям по кодекам при попытке оптимизировать сжатие для схем наблюдения ClickHouse.
В общем, пользователи найдут кодек ZSTD
весьма применимым к наборам данных логов и трассировок. Увеличение значения сжатия с его значения по умолчанию 1 может улучшить сжатие. Это, однако, следует протестировать, так как более высокие значения накладывают большую нагрузку на CPU во время вставки. Обычно мы наблюдаем незначительное увеличение от увеличения этого значения.
Кроме того, временные метки, хотя и выигрывают от дельта-кодирования в отношении сжатия, были показаны замедляющими производительность запросов, если этот столбец используется в первичном/упорядочивающем ключе. Мы рекомендуем пользователям оценить соответствующие компромиссы между сжатием и производительностью запросов.
Использование словарей
Словари являются ключевой функцией ClickHouse, обеспечивающей представление данных из различных внутренних и внешних источников в памяти в виде ключ-значение, оптимизированное для супер-низкой задержки запросов.

Это полезно в различных сценариях, от обогащения встраиваемых данных на лету без замедления процесса инжекции до улучшения производительности запросов в целом, при этом JOINs особенно выигрывают. Хотя соединения редко требуются в сценариях мониторинга, словари все равно могут быть полезны для целей обогащения - как на времени вставки, так и во время запроса. Мы предоставим примеры обоих случаев ниже.
Пользователи, заинтересованные в ускорении соединений с помощью словарей, могут найти дополнительные подробности здесь.
Время вставки против времени запроса
Словари можно использовать для обогащения наборов данных как во время запроса, так и во время вставки. Каждому из этих подходов соответствуют свои плюсы и минусы. В резюме:
- Время вставки - Это обычно подходит, если значение обогащения не меняется и существует во внешнем источнике, который можно использовать для заполнения словаря. В этом случае обогащение строки во время вставки избегает поиска во времени запроса к словарю. Это происходит со стоимостью производительности вставки, а также дополнительным объемом хранилища, поскольку обогащенные значения будут храниться в виде колонок.
- Время запроса - Если значения в словаре часто меняются, запросы во времени запроса обычно более применимы. Это избегает необходимости обновлять колонки (и переписывать данные), если сопоставленные значения изменяются. Это гибкость происходит за счет стоимости поиска во времени запроса. Эта стоимость во времени запроса обычно ощутима, если поиск требуется для многих строк, например, при использовании поиска в словаре в условиях фильтрации. Для обогащения результата, т.е. в
SELECT
, данный издержки обычно не ощутимы.
Мы рекомендуем пользователям ознакомиться с основами словарей. Словари предоставляют таблицу поиска в памяти, из которой значения могут быть извлечены с помощью специализированных функций.
Для простых примеров обогащения см. руководство по словарям здесь. Ниже мы сосредоточим внимание на общих задачах обогащения мониторинга.
Использование IP-словарей
Географическое обогащение логов и трассировок значениями широты и долготы с использованием IP-адресов является распространенной задачей мониторинга. Мы можем достичь этого, используя структурированный словарь ip_trie
.
Мы используем общедоступный набор данных DB-IP на уровне города, предоставленный DB-IP.com на условиях лицензии CC BY 4.0.
Из чтения мы можем видеть, что данные структурированы следующим образом:
Учитывая эту структуру, давайте начнем с того, чтобы взглянуть на данные, используя url() функцию таблицы:
Чтобы облегчить себе задачу, давайте используем URL()
движок таблицы для создания объекта таблицы ClickHouse с нашими именами полей и подтвердим общее количество строк:
Поскольку наш словарь ip_trie
требует, чтобы диапазоны IP-адресов выражались в нотации CIDR, нам нужно будет преобразовать ip_range_start
и ip_range_end
.
Эта CIDR для каждого диапазона может быть лаконично вычислена с помощью следующего запроса:
В запросе выше происходит много действий. Для заинтересованных, прочтите это отличное объяснение. В противном случае примите, что выше вычисляется CIDR для диапазона IP.
Для наших целей нам нужны только диапазон IP, код страны и координаты, поэтому давайте создадим новую таблицу и вставим наши данные Geo IP:
Для выполнения запросов с низкой задержкой по IP в ClickHouse мы используем словари для хранения соответствия ключ -> атрибуты для наших данных Geo IP в памяти. ClickHouse предоставляет структуру словаря ip_trie
[/sql-reference/dictionaries#ip_trie], чтобы сопоставить наши сетевые префиксы (CIDR-блоки) с координатами и кодами стран. Следующий запрос задает словарь, используя эту структуру и вышеупомянутую таблицу в качестве источника.
Мы можем выбрать строки из словаря и подтвердить, что этот набор данных доступен для запросов:
Словари в ClickHouse периодически обновляются на основе данных исходной таблицы и срока действия, используемого выше. Чтобы обновить наш словарь Geo IP, чтобы отразить последние изменения в наборе данных DB-IP, нам просто нужно повторно вставить данные из удаленной таблицы geoip_url в нашу таблицу geoip
с примененными преобразованиями.
Теперь, когда у нас есть данные Geo IP, загруженные в наш словарь ip_trie
(который также удобно называется ip_trie
), мы можем использовать его для геолокации IP. Это можно сделать с использованием функции dictGet()
следующим образом:
Обратите внимание на скорость извлечения. Это позволяет нам обогащать логи. В этом случае мы выбираем выполнять обогащение во время запроса.
Вернувшись к нашему первоначальному набору данных логов, мы можем использовать вышеприведенное, чтобы агрегировать наши логи по стране. Следующее предполагает, что мы используем схему, полученную из нашего раннее описанного материализованного представления, которое содержит извлеченную колонку RemoteAddress
.
Поскольку сопоставление IP к географическому положению может меняться, пользователи, вероятно, захотят знать, откуда поступил запрос в момент его отправки, а не о том, где текущее географическое положение для того же адреса. По этой причине обогащение времени индекса, вероятно, будет предпочтительным здесь. Это можно сделать с помощью материализованных колонок, как показано ниже, или в выборке материализованного представления:
Пользователи, вероятно, захотят, чтобы словарь обогащения IP периодически обновлялся на основе новых данных. Это можно достичь с помощью условия LIFETIME
словаря, которое заставит словарь периодически перезагружаться из исходной таблицы. Чтобы обновить исходную таблицу, см. "Обновляемые материализованные представления".
Вышеупомянутые страны и координаты предлагают возможности визуализации, выходящие за рамки группировки и фильтрации по стране. Для вдохновения смотрите "Визуализация геоданных".
Использование словарей регулярных выражений (парсинг user agent)
Парсинг строк user agent является классической задачей регулярного выражения и распространенным требованием в наборах данных на основе логов и трассировок. ClickHouse предоставляет эффективный парсинг user agent с помощью Словарей Деревьев Регулярных Выражений.
Словари деревьев регулярных выражений определяются в ClickHouse с использованием источника словаря YAMLRegExpTree, который предоставляет путь к файлу YAML, содержащему дерево регулярных выражений. Если вы хотите предоставить собственный словарь регулярных выражений, информация о требуемой структуре может быть найдена здесь. Ниже мы сосредоточимся на парсинге user agent с использованием uap-core и загрузим наш словарь для поддерживаемого формата CSV. Этот подход совместим с OSS и ClickHouse Cloud.
Создайте следующие таблицы Memory. Они содержат наши регулярные выражения для парсинга устройств, браузеров и операционных систем.
Эти таблицы могут быть заполнены из следующих публично расположенных CSV-файлов, используя функцию таблицы url:
Заполнив наши таблицы памяти, мы можем загрузить наши словари регулярных выражений. Обратите внимание, что нам нужно будет указать значения ключей в качестве колонок - это будут атрибуты, которые мы сможем извлечь из user agent.
С этими загруженными словарями мы можем предоставить пример user agent и протестировать наши новые возможности извлечения из словаря:
Учитывая, что правила для user agent редко меняются, словарь только нужно будет обновлять в ответ на новые браузеры, операционные системы и устройства, имеет смысл выполнять это извлечение на этапе вставки.
Мы можем выполнить эту работу с помощью материализованной колонки или с помощью материализованного представления. Ниже мы изменяем материализованное представление, использованное ранее:
Это требует модифицировать схему для целевой таблицы otel_logs_v2
:
После перезапуска сборщика и загрузки структурированных логов на основе ранее задокументированных шагов, мы можем запросить наши только что извлеченные колонки Device, Browser и Os.
Обратите внимание на использование кортежей для этих колонок user agent. Кортежи рекомендованы для сложных структур, когда иерархия известна заранее. Подколонки предлагают такую же производительность, как обычные колонки (в отличие от ключей Map) и позволяют использовать разнородные типы.
Дальнейшее чтение
Для получения дополнительных примеров и подробностей о словарях, мы рекомендуем следующие статьи:
Ускорение запросов
ClickHouse поддерживает несколько методов для ускорения производительности запросов. Следующее следует рассматривать только после выбора подходящего первичного/упорядочивающего ключа для оптимизации наиболее популярных шаблонов доступа и максимизации сжатия. Обычно это окажет наибольшее влияние на производительность при наименьших усилиях.
Использование материализованных представлений (инкрементных) для агрегаций
На предыдущих страницах мы изучили использование материализованных представлений для трансформации и фильтрации данных. Однако материализованные представления также можно использовать для предварительного вычисления агрегаций во время вставки и хранения результата. Этот результат может обновляться результатами последующих вставок, таким образом, эффективно позволяя выполнять агрегацию во время вставки.
Основная идея здесь заключается в том, что результаты часто будут меньшим представлением оригинальных данных (частичный эскиз в случае агрегаций). Когда это сочетается с более простым запросом для чтения результатов из целевой таблицы, время выполнения запросов будет быстрее, чем если бы то же самое вычисление выполнялось на оригинальных данных.
Рассмотрим следующий запрос, где мы вычисляем общий трафик за час, используя наши структурированные логи:
Можно предположить, что это может быть общая линейная диаграмма, которую пользователи строят с помощью Grafana. Этот запрос, безусловно, очень быстрый - набор данных составляет всего 10 миллионов строк, и ClickHouse быстр! Тем не менее, если мы масштабируем это до миллиардов и триллионов строк, мы, по идее, хотели бы поддерживать такую производительность запроса.
Этот запрос будет в 10 раз быстрее, если мы используем таблицу otel_logs_v2
, которая является результатом нашего предыдущего материализованного представления, которое извлекает ключ размера из карты LogAttributes
. Мы используем здесь сырые данные только в иллюстративных целях и рекомендуем использовать предыдущее представление, если это общий запрос.
Нам нужна таблица для получения результатов, если мы хотим вычислить это во время вставки, используя материализованное представление. Эта таблица должна хранить только 1 строку на час. Если обновление получено для существующего часа, другие столбцы должны быть объединены в строку текущего часа. Для того чтобы это слияние инкрементальных состояний произошло, частичные состояния должны храниться для других столбцов.
Для этого требуется специальный тип движка в ClickHouse: SummingMergeTree. Он заменяет все строки с одинаковым упорядочивающим ключом одной строкой, которая содержит суммированные значения для числовых столбцов. Следующая таблица объединит любые строки с одной и той же датой, суммируя любые числовые столбцы.
Чтобы продемонстрировать наше материализованное представление, предположим, что наша таблица bytes_per_hour
пустая и еще не получила никаких данных. Наше материализованное представление выполняет указанный выше SELECT
на данных, вставленных в otel_logs
(это будет выполняться по блокам заданного размера), а результаты отправляются в bytes_per_hour
. Синтаксис показан ниже:
Клаузула TO
здесь имеет ключевое значение, указывая, куда будут отправлены результаты, т.е. в bytes_per_hour
.
Если мы перезапустим наш OTel Collector и повторно отправим логи, таблица bytes_per_hour
будет инкрементально заполняться результатом вышеуказанного запроса. По завершении мы можем подтвердить размер нашей таблицы bytes_per_hour
- у нас должно быть 1 строка на час:
Мы эффективно сократили количество строк здесь с 10 миллионов (в otel_logs
) до 113, сохраняя результат нашего запроса. Ключевое здесь то, что если новые логи вставляются в таблицу otel_logs
, новые значения будут отправлены в bytes_per_hour
для их соответствующего часа, где они автоматически объединяются асинхронно в фоновом режиме - сохраняя только одну строку на час, bytes_per_hour
будет всегда и маленьким, и актуальным.
Поскольку объединение строк происходит асинхронно, может быть больше одной строки на час, когда пользователь выполняет запрос. Чтобы гарантировать, что все не объединенные строки будут объединены во время запроса, у нас есть два варианта:
- Использовать
FINАL
модификатор в имени таблицы (как мы сделали для вышеупомянутого запроса на количество). - Аггрегировать по упорядочивающему ключу, использованному в нашей конечной таблице, т.е. Timestamp и суммировать метрики.
Как правило, второй вариант более эффективен и гибок (таблицу можно использовать для других задач), но первый может быть проще для некоторых запросов. Мы покажем оба варианта ниже:
Это ускорило наш запрос с 0.6s до 0.008s - более чем в 75 раз!
Эти преимущества могут быть еще больше на больших наборах данных с более сложными запросами. Смотрите здесь для примеров.
Более сложный пример
В приведенном выше примере выполняется агрегация простого подсчета за час с использованием SummingMergeTree. Статистика, выходящая за рамки простых сумм, требует другого типа движка целевой таблицы: AggregatingMergeTree.
Предположим, мы хотим вычислить количество уникальных IP-адресов (или уникальных пользователей) за день. Запрос для этого:
Чтобы сохранить счетчик кардинальности для инкрементального обновления, требуется AggregatingMergeTree.
Чтобы убедиться, что ClickHouse знает, что агрегатные состояния будут сохранены, мы определяем столбец UniqueUsers
как тип AggregateFunction
, указывая функцию-источник частичных состояний (uniq) и тип исходного столбца (IPv4). Как и в случае с SummingMergeTree, строки с одинаковым значением ключа ORDER BY
будут объединены (Hour в приведенном выше примере).
Связанное материализованное представление использует ранее указанный запрос:
Обратите внимание, как мы добавляем суффикс State
в конец наших агрегатных функций. Это гарантирует, что агрегатное состояние функции возвращается, а не итоговый результат. Это будет содержать дополнительную информацию, позволяющую этому частичному состоянию объединиться с другими состояниями.
Как только данные были перезагружены через перезапуск Collector-а, мы можем подтвердить, что в таблице unique_visitors_per_hour
доступно 113 строк.
Наш окончательный запрос должен использовать суффикс Merge для наших функций (так как столбцы хранят частичные агрегатные состояния):
Обратите внимание, что здесь мы используем GROUP BY
, вместо использования FINAL
.
Использование материализованных представлений (инкрементных) для быстрого поиска
Пользователи должны учитывать свои шаблоны доступа при выборе упорядочивающего ключа ClickHouse с колонками, которые часто используются в условиях фильтрации и агрегации. Это может быть ограничивающим фактором в сценариях мониторинга, где у пользователей более разнообразные шаблоны доступа, которые не могут быть обособлены в одном наборе колонок. Это лучше всего иллюстрируется на примере, встроенном в стандартные схемы OTel. Рассмотрим стандартную схему для трассировок:
Эта схема оптимизирована для фильтрации по ServiceName
, SpanName
и Timestamp
. В трассировке пользователям также нужна возможность выполнять поиск по конкретному TraceId
и извлекать связанные с ним прогоны трассировки. Хотя это присутствует в упорядочивающем ключе, его положение в конце означает, что фильтрация не будет такой эффективной и, вероятно, потребуется просканировать значительные объемы данных при извлечении одной трассировки.
OTel collector также устанавливает материализованное представление и связанную таблицу для решения этой проблемы. Таблица и представление показаны ниже:
Представление эффективно гарантирует, что таблица otel_traces_trace_id_ts
имеет минимальное и максимальное время метки для трассировки. Эта таблица, упорядоченная по TraceId
, позволяет эффективно извлекать эти временные метки. Эти диапазоны временных меток могут, в свою очередь, использоваться при запросе основной таблицы otel_traces
. Более конкретно, при извлечении трассировки по ее идентификатору, Grafana использует следующий запрос:
CTE здесь определяет минимальную и максимальную временную метку для идентификатора трассировки ae9226c78d1d360601e6383928e4d22d
, прежде чем использовать это для фильтрации основной таблицы otel_traces
для его связанных прогонах.
Этот же подход можно применить к аналогичным шаблонам доступа. Мы исследуем аналогичный пример в Моделировании данных здесь.
Использование проекций
Проекции ClickHouse позволяют пользователям указывать несколько клаузул ORDER BY
для таблицы.
В предыдущих разделах мы исследовали, как материализованные представления могут быть использованы в ClickHouse для предварительного вычисления агрегаций, трансформации строк и оптимизации запросов мониторинга для различных шаблонов доступа.
Мы предоставили пример, где материализованное представление отправляет строки в целевую таблицу с другим упорядочивающим ключом, чем оригинальная таблица, получающая вставки, чтобы оптимизировать для поиска по идентификатору трассировки.
Проекции могут быть использованы для решения той же проблемы, позволяя пользователю оптимизировать запросы по столбцу, который не является частью первичного ключа.
В теории, эта возможность может быть использована для предоставления нескольких упорядочивающих ключей для таблицы, с одним явным недостатком: дублирование данных. В частности, данные должны быть записаны в порядке основного первичного ключа в дополнение к порядку, указанному для каждой проекции. Это замедлит вставки и потребует больше дискового пространства.
Проекции предоставляют многие те же возможности, что и материализованные представления, но их следует использовать экономно, часто предпочитая последние. Пользователи должны понимать недостатки и когда они уместны. Например, хотя проекции могут быть использованы для предварительного вычисления агрегаций, мы рекомендуем пользователям использовать материализованные представления для этого.

Рассмотрим следующий запрос, который фильтрует нашу таблицу otel_logs_v2
по кодам ошибок 500. Это, вероятно, распространенный шаблон доступа для ведения логов с пользователями, желающими фильтровать по кодам ошибок:
Мы не выводим результаты здесь с использованием FORMAT Null
. Это заставляет читать все результаты, но не возвращать их, тем самым предотвращая преждевременное завершение запроса из-за LIMIT. Это просто, чтобы показать время, затраченное на сканирование всех 10 миллионов строк.
Вышеуказанный запрос требует линейного сканирования с нашим выбранным упорядочивающим ключом (ServiceName, Timestamp)
. Хотя мы могли бы добавить Status
в конец упорядочивающего ключа, улучшая производительность для вышеуказанного запроса, мы также можем добавить проекцию.
Обратите внимание, что сначала нужно создать проекцию, а затем материализовать ее. Эта последняя команда приводит к тому, что данные хранятся дважды на диске в двух различных порядках. Проекцию также можно определить при создании данных, как показано ниже, и она будет автоматически поддерживаться по мере вставки данных.
Важно отметить, что если проекция создается через ALTER
, ее создание происходит асинхронно, когда команда MATERIALIZE PROJECTION
выдается. Пользователи могут подтвердить ход этой операции с помощью следующего запроса, ожидая is_done=1
.
Если мы повторим вышеуказанный запрос, мы можем увидеть, что производительность значительно улучшилась за счет дополнительного хранения (см. "Измерение размера таблицы и сжатия" для измерения этого).
В приведенном примере мы указываем столбцы, использованные в предыдущем запросе, в проекции. Это означает, что только эти указанные столбцы будут храниться на диске как часть проекции, упорядоченные по Status. Если, наоборот, мы использовали бы SELECT *
здесь, все столбцы будут храниться. Хотя это позволило бы большему количеству запросов (с использованием любого подмножества столбцов) извлекать выгоду из проекции, потребуется дополнительное хранилище. Для измерения дискового пространства и сжатия смотрите "Измерение размера таблицы и сжатия".
Вторичные/индексы для пропуска данных
Как бы хорошо первичный ключ ни был настроен в ClickHouse, некоторые запросы неизбежно потребуют полных сканирований таблицы. Хотя это может быть смягчено с помощью материализованных представлений (и проекций для некоторых запросов), они требуют дополнительного обслуживания, и пользователи должны быть в курсе их доступности, чтобы гарантировать их использование. В то время как традиционные реляционные базы данных решают эту проблему с помощью вторичных индексов, они неэффективны в колонкоориентированных базах данных, таких как ClickHouse. Вместо этого ClickHouse использует индексы "Пропуск", которые могут значительно улучшить производительность запросов, позволяя базе данных пропускать большие фрагменты данных без соответствующих значений.
Стандартные схемы OTel используют вторичные индексы в попытке ускорить доступ к картам. Хотя мы считаем их в целом неэффективными и не рекомендуем копировать их в вашу пользовательскую схему, индексы для пропуска могут быть все же полезны.
Пользователи должны прочитать и понять руководство по вторичным индексам перед тем, как пытаться их применять.
В общем, они эффективны, когда существует сильная корреляция между первичным ключом и целевым, не первичным столбцом/выражением, и пользователи ищут редкие значения, т.е. те, которые не встречаются во многих гранулах.
Фильтры Блума для текстового поиска
Для запросов мониторинга вторичные индексы могут быть полезны, когда пользователи хотят выполнять текстовые поиски. В частности, индексы фильтров Блума на основе ngram и токенов ngrambf_v1
и tokenbf_v1
могут использоваться для ускорения поиска по строковым колонкам с операторами LIKE
, IN
и hasToken. Важно отметить, что индекс на основе токенов создает токены, используя неалфавитные символы в качестве разделителей. Это означает, что только токены (или целые слова) могут быть сопоставлены во время запроса. Для более детального сопоставления можно использовать N-gram фильтр Блума. Он разбивает строки на ngram с заданным размером, что позволяет выполнять сопоставление подслов.
Чтобы оценить токены, которые будут созданы и, следовательно, сопоставлены, можно использовать функцию tokens
:
Функция ngram
предоставляет подобные возможности, где размер ngram
может быть указан в качестве второго параметра:
ClickHouse также имеет экспериментальную поддержку обратных индексов в качестве вторичного индекса. В настоящее время мы не рекомендуем их для наборов данных логов, но ожидаем, что они заменят индексы на основе токенов фильтров Блума, когда они будут готовы к производству.
Для целей этого примера мы используем набор данных структурированных логов. Предположим, мы хотим подсчитать логи, в которых столбец Referer
содержит ultra
.
Здесь нам нужно сопоставить ngram размером 3. Таким образом, мы создаем индекс ngrambf_v1
.
Индекс ngrambf_v1(3, 10000, 3, 7)
здесь принимает четыре параметра. Последний из них (значение 7) представляет собой сид. Остальные представляют собой размер ngram (3), значение m
(размер фильтра) и количество хеш-функций k
(7). k
и m
требуют настройки и будут основаны на количестве уникальных ngram/токенов и вероятности, что фильтр даёт ложный отрицательный результат - таким образом подтверждая, что значение отсутствует в грануле. Мы рекомендуем эти функции для помощи в установлении этих значений.
Если правильно настроить, ускорение здесь может быть значительным:
Вышеуказанное представлено только для иллюстрации. Мы рекомендуем пользователям извлекать структуру из своих логов во время вставки, а не пытаться оптимизировать текстовые поиски, используя токены фильтров Блума. Тем не менее, существуют случаи, когда у пользователей есть трассировки стека или другие большие строки, для которых текстовый поиск может быть полезен из-за менее детерминированной структуры.
Некоторые общие рекомендации по использованию фильтров Блума:
Цель фильтра Блума – отфильтровать гранулы, таким образом избегая необходимости загружать все значения для столбца и выполнять линейное сканирование. Клаузу EXPLAIN
с параметром indexes=1
можно использовать для определения количества гранул, которые были пропущены. Рассмотрите ответы ниже для оригинальной таблицы otel_logs_v2
и таблицы otel_logs_bloom
с фильтром Блума ngram.
Фильтр Блума, как правило, будет быстрее только в том случае, если он меньше самого столбца. Если он больше, то вероятно, что преимущества производительности будут незначительными. Сравните размер фильтра с размером столбца, используя следующие запросы:
В приведенных примерах мы можем видеть, что вторичный индекс фильтра Блума составляет 12MB - почти в 5 раз меньше, чем сжатый размер самого столбца, который составляет 56MB.
Фильтры Блума могут потребовать значительной настройки. Мы рекомендуем следовать заметкам здесь, которые могут быть полезны для определения оптимальных настроек. Фильтры Блума также могут быть затратными во время вставки и слияния. Пользователи должны оценить влияние на производительность вставки перед добавлением фильтров Блума в производство.
Дополнительные сведения о вторичных индексах для пропуска данных можно найти здесь.
Извлечение из карт
Тип Map широко распространен в схемах OTel. Этот тип требует, чтобы значения и ключи имели один и тот же тип - что достаточно для метаданных, таких как метки Kubernetes. Будьте внимательны, что при запросе подполя типа Map загружается весь родительский столбец. Если в карте много ключей, это может привести к значительным штрафам за запрос, так как необходимо прочитать больше данных с диска, чем если бы ключ существовал как столбец.
Если вы часто запрашиваете определенный ключ, рассмотрите возможность его перемещения в собственный отдельный столбец на корневом уровне. Обычно это задача, которая происходит в ответ на общие шаблоны доступа и после развертывания, и ее сложно предсказать до производства. Смотрите "Управление изменениями схемы" для получения информации о том, как изменить свою схему после развертывания.
Измерение размера таблицы и сжатия
Одной из основных причин, по которым ClickHouse используется для мониторинга, является сжатие.
Помимо значительного снижения затрат на хранение, меньшее количество данных на диске означает меньшую I/O и более быстрые запросы и вставки. Снижение ввод-вывод перекрывает накладные расходы любого алгоритма сжатия относительно CPU. Таким образом, улучшение сжатия данных должно быть первым приоритетом при работе на обеспечении быстродействия запросов ClickHouse.
Подробности о измерении сжатия можно найти здесь.