Перейти к основному содержимому
Перейти к основному содержимому

Проектирование схемы для мониторинга

Мы рекомендуем пользователям всегда создавать собственную схему для логов и трейсов по следующим причинам:

  • Выбор первичного ключа - Стандартные схемы используют ORDER BY, который оптимизирован для конкретных паттернов доступа. Вряд ли ваши паттерны доступа будут с этим совпадать.
  • Извлечение структуры - Пользователи могут захотеть извлечь новые столбцы из существующих, например, из колонки Body. Это можно сделать с помощью материализованных столбцов (и материализованных представлений в более сложных случаях). Это требует изменений схемы.
  • Оптимизация карт - Стандартные схемы используют тип Map для хранения атрибутов. Эти столбцы позволяют хранить произвольные метаданные. Хотя это важная возможность, поскольку метаданные событий часто не определяются заранее и, следовательно, не могут быть хранены в строго типизированной базе данных, такой как ClickHouse, доступ к ключам карты и их значениям не так эффективен, как доступ к обычному столбцу. Мы решаем эту проблему, модифицируя схему и обеспечивая, чтобы наиболее часто используемые ключи карты были верхнеуровневыми столбцами - смотрите "Извлечение структуры с помощью SQL". Это требует изменения схемы.
  • Упрощение доступа к ключам карты - Доступ к ключам в картах требует более многословного синтаксиса. Пользователи могут смягчить это с помощью псевдонимов. См. "Использование псевдонимов" для упрощения запросов.
  • Вторичные индексы - Стандартная схема использует вторичные индексы для ускорения доступа к картам и ускорения текстовых запросов. Обычно они не требуются и требуют дополнительного пространства на диске. Их можно использовать, но следует протестировать, чтобы убедиться, что они нужны. См. "Вторичные индексы / Индексы пропуска данных".
  • Использование кодеков - Пользователи могут захотеть настроить кодеки для столбцов, если они понимают ожидаемые данные и имеют доказательства того, что это улучшает сжатие.

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

Важно: Хотя пользователям рекомендуется расширять и изменять свою схему для достижения оптимального сжатия и производительности запросов, они должны придерживаться именования схемы OTel для основных столбцов, где это возможно. Плагин ClickHouse для Grafana предполагает наличие некоторых основных столбцов OTel для помощи в построении запросов, например, Timestamp и SeverityText. Обязательные столбцы для логов и трейсов документированы здесь [1][2] и здесь, соответственно. Вы можете изменить эти имена столбцов, переопределив значения по умолчанию в конфигурации плагина.

Извлечение структуры с помощью SQL

При приеме структурированных или неструктурированных логов пользователям часто требуется возможность:

  • Извлекать столбцы из строковых блобов. Запросы к этим столбцам будут быстрее, чем использование строковых операций во время выполнения запроса.
  • Извлекать ключи из карт. Стандартная схема помещает произвольные атрибуты в столбцы типа Map. Этот тип предоставляет возможность без схемы, что позволяет пользователям не определять столбцы для атрибутов при определении логов и трейсов - часто это невозможно при сборе логов из Kubernetes, если необходимо сохранить метки пода для последующего поиска. Доступ к ключам карты и их значениям может быть медленнее, чем запрос по обычным столбцам ClickHouse. Поэтому извлечение ключей из карт в корневые столбцы таблицы часто желательно.

Рассмотрим следующие запросы:

Допустим, мы хотим подсчитать, какие URL-адреса получают наибольшее количество POST-запросов, используя структурированные логи. JSON-блоб хранится в колонке Body в виде строки. Кроме того, он также может храниться в колонке LogAttributes как Map(String, String), если пользователь активировал json_parser в сборщике.

Предположим, LogAttributes доступен, запрос для подсчета, какие URL-адреса сайта получают наибольшее количество POST-запросов:

Обратите внимание на использование синтаксиса карты, например, LogAttributes['request_path'], а также на функцию path для удаления параметров запроса из URL.

Если пользователь не активировал парсинг JSON в сборщике, тогда LogAttributes будет пустым, что заставит нас использовать JSON функции для извлечения столбцов из строки Body.

Предпочитайте ClickHouse для парсинга

Мы в целом рекомендуем пользователям выполнять парсинг JSON в ClickHouse для структурированных логов. Мы уверены, что ClickHouse - это самая быстрая реализация парсинга JSON. Тем не менее, мы понимаем, что пользователи могут захотеть отправить логи в другие источники и не иметь этой логики в SQL.

Теперь рассмотрим аналогичный случай для неструктурированных логов:

Аналогичный запрос для неструктурированных логов требует использования регулярных выражений через функцию extractAllGroupsVertical.

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

Рассматривайте словари

Вышеупомянутый запрос можно оптимизировать для использования словарей регулярных выражений. См. Использование словарей для получения более подробной информации.

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

OTel или ClickHouse для обработки?

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

Материализованные столбцы

Материализованные столбцы предлагают самое простое решение для извлечения структуры из других столбцов. Значения таких столбцов всегда рассчитываются во время вставки и не могут быть указаны в запросах INSERT.

Нагрузки

Материализованные столбцы требуют дополнительного объема памяти, так как значения извлекаются в новые столбцы на диске во время вставки.

Материализованные столбцы поддерживают любые выражения ClickHouse и могут использовать любые аналитические функции для обработки строк (включая регулярные выражения и поиск и URL), выполняя преобразования типов, извлекая значения из JSON или математические операции.

Мы рекомендуем использовать материализованные столбцы для базовой обработки. Они особенно полезны для извлечения значений из карт, повышения их до корневых столбцов и выполнения преобразований типов. Они обычно наиболее полезны, когда используются в очень простых схемах или в сочетании с материализованными представлениями. Рассмотрим следующую схему для логов, из которой JSON был извлечен в колонку LogAttributes сборщиком:

Эквивалентная схема для извлечения с использованием JSON функций из строки Body может быть найдена здесь.

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

примечание

Материализованные столбцы по умолчанию не вернутся в SELECT *. Это необходимо для сохранения инварианта, что результат SELECT * всегда может быть вставлен обратно в таблицу с помощью INSERT. Это поведение можно отключить, установив asterisk_include_materialized_columns=1, и оно может быть включено в Grafana (см. Дополнительные настройки -> Пользовательские настройки в конфигурации источника данных).

Материализованные представления

Материализованные представления предоставляют более мощные средства применения фильтрации SQL и преобразований к логам и трейсам.

Материализованные представления позволяют пользователям переместить стоимость вычислений с времени запроса на время вставки. Материализованное представление ClickHouse - это просто триггер, который выполняет запрос над блоками данных, когда они вставляются в таблицу. Результаты этого запроса вставляются во вторую "целевую" таблицу.

Обновления в реальном времени

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

Запрос, связанный с материализованным представлением, теоретически может быть любым запросом, включая агрегации, хотя существуют ограничения со соединениями. Для преобразований и фильтрации, необходимых для логов и трейсов, пользователи могут считать любое SELECT утверждение возможным.

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

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

Движок Null - это мощная оптимизация - думайте о нем как о /dev/null. Эта таблица не будет хранить никаких данных, но любые прикрепленные материализованные представления по-прежнему будут выполняться над вставляемыми строками, прежде чем они будут удалены.

Рассмотрим следующий запрос. Он преобразует наши строки в формат, который мы хотим сохранить, извлекая все столбцы из LogAttributes (мы предполагаем, что это было установлено сборщиком с использованием оператора json_parser), устанавливая SeverityText и SeverityNumber (на основании некоторых простых условий и определения этих столбцов). В этом случае мы также выбираем только те столбцы, которые мы знаем будут заполнены - игнорируя такие столбцы, как TraceId, SpanId и TraceFlags.

Мы также извлекаем столбец Body выше - на случай, если позже будут добавлены дополнительные атрибуты, которые не извлекаются нашим SQL. Этот столбец должен хорошо сжиматься в ClickHouse и будет редко доступен, тем самым не влияя на производительность запроса. Наконец, мы уменьшаем Timestamp до DateTime (чтобы сэкономить место - смотрите "Оптимизация типов") с приведением типов.

Условные операторы

Обратите внимание на использование условных операторов выше для извлечения SeverityText и SeverityNumber. Они очень полезны для формулирования сложных условий и проверки наличия значений в картах - мы наивно предполагаем, что все ключи существуют в 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, используя целевую таблицу с той же схемой. Это должно подтвердить, что типы правильно обрабатываются. Особое внимание следует уделять следующим случаям:

  • Если ключ не существует в карте, будет возвращена пустая строка. В случае чисел пользователям необходимо сопоставить их с соответствующим значением. Это можно сделать с помощью условных операторов, например, if(LogAttributes['status'] = ", 200, LogAttributes['status']) или функций приведения типов, если значения по умолчанию приемлемы, например, toUInt8OrDefault(LogAttributes['status'] )
  • Некоторые типы не всегда будут приведены, например, строковые представления чисел не будут приведены к значениям перечислений.
  • Функции извлечения JSON возвращают значения по умолчанию для их типа, если значение не найдено. Убедитесь, что эти значения имеют смысл!
Избегайте Nullable

Избегайте использования Nullable в ClickHouse для данных мониторинга. Обычно нет необходимости различать пустые и нулевые значения в логах и трейсов. Эта функция требует дополнительного объема памяти и негативно сказывается на производительности запросов. См. здесь для получения дополнительной информации.

Выбор первичного (упорядоченного) ключа

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

Некоторые простые правила можно применить для выбора упорядоченного ключа. Следующие пункты иногда могут конфликтовать, поэтому рассматривайте их в порядке значимости. Пользователи могут определить несколько ключей из этого процесса, из которых 4-5 обычно достаточно:

  1. Выберите столбцы, которые соответствуют вашим общим фильтрам и паттернам доступа. Если пользователи обычно начинают расследования в области мониторинга, фильтруя по определенному столбцу, например, по имени пода, этот столбец будет часто использоваться в условиях WHERE. Приоритезируйте включение этих столбцов в ваш ключ по сравнению с теми, которые используются реже.
  2. Предпочитайте столбцы, которые помогают исключать большой процент общих строк при фильтрации, тем самым уменьшая объем данных, которые необходимо читать. Названия служб и коды статуса часто являются хорошими кандидатами - в последнем случае только в том случае, если пользователи фильтруют по значениям, которые исключают большую часть строк, например, фильтрация по 200 будет соответствовать большинству строк в большинстве систем, в сравнении с 500 ошибками, которые будут соответствовать небольшой подсетке.
  3. Предпочитайте столбцы, которые, вероятно, будут высоко коррелировать с другими столбцами в таблице. Это будет способствовать обеспечению того, чтобы эти значения также были записаны последовательно, улучшая сжатие.
  4. Операции GROUP BY и ORDER BY для столбцов в упорядоченном ключе могут быть сделаны более эффективными по памяти.

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

Сначала структура

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

Использование карт

Предыдущие примеры показывают использование синтаксиса карт map['key'] для доступа к значениям в столбцах Map(String, String). Кроме того, для фильтрации или выбора этих столбцов доступны специализированные функции ClickHouse map functions.

Например, следующий запрос определяет все уникальные ключи, доступные в столбце LogAttributes, используя mapKeys функцию, за которой следует groupArrayDistinctArray функция (комбинатор).

Избегайте точек

Рекомендуем избегать использования точек в именах колонок Map и можем отказаться от этого использования. Используйте _.

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

Запросы к типам карт медленнее, чем к обычным столбцам - см. "Ускорение запросов". Кроме того, это более синтаксически сложно и может быть неудобно для пользователей. Чтобы решить эту последнюю проблему, мы рекомендуем использовать колонки Alias.

Колонки ALIAS вычисляются во время выполнения запроса и не хранятся в таблице. Поэтому невозможно INSERT значение в колонку этого типа. Используя псевдонимы, мы можем ссылаться на ключи карты и упрощать синтаксис, прозрачно отображая записи карты как обычный столбец. Рассмотрим следующий пример:

У нас есть несколько материализованных колонок и одна колонка ALIAS, RemoteAddr, которая обращается к карте LogAttributes. Теперь мы можем запрашивать значения LogAttributes['remote_addr'] через этот столбец, тем самым упрощая наш запрос, т.е.

Кроме того, добавление ALIAS очень просто через команду ALTER TABLE. Эти колонки становятся немедленно доступными, например,

ALIAS по умолчанию исключены

По умолчанию SELECT * исключает колонки ALIAS. Это поведение можно отключить, установив asterisk_include_alias_columns=1.

Оптимизация типов

Общие лучшие практики ClickHouse по оптимизации типов применимы к использованию ClickHouse.

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

Помимо оптимизации типов, пользователи могут следовать общим best practices для кодеков при оптимизации сжатия для схем наблюдаемости ClickHouse.

В общем, пользователи обнаружат, что кодек ZSTD высоко применим к данным журналов и трассировок. Увеличение значения сжатия с его значения по умолчанию 1 может улучшить сжатие. Однако это следует протестировать, так как более высокие значения увеличивают нагрузку на CPU во время вставки. Обычно мы наблюдаем небольшую выгоду от увеличения этого значения.

Кроме того, временные метки, хотя и выигрывают от дельта-кодирования относительно сжатия, продемонстрировали замедленную производительность запросов, если этот столбец используется в первичном/сортировочном ключе. Мы рекомендуем пользователям оценить соответствующие компромиссы между сжатием и производительностью запросов.

Использование словарей

Словари являются ключевой функцией ClickHouse, обеспечивающей хранение в памяти пар ключ-значение данных из различных внутренних и внешних источников, оптимизированные для запросов с низкой задержкой.

Это полезно в различных сценариях, от обогащения принятых данных на лету без замедления процесса загрузки и улучшения производительности запросов в целом, причем JOIN-ы получают особенно большую выгоду. Хотя JOIN-ы редко нужны в сценариях наблюдаемости, словари все равно могут быть полезны для целей обогащения - как во время вставки, так и во время запроса. Мы предоставляем примеры обоих ниже.

Ускорение JOIN-ов

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

Время вставки против времени запроса

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

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

Мы рекомендуем пользователям ознакомиться с основами словарей. Словари предоставляют таблицу поиска в памяти, из которой значения могут быть извлечены с использованием специализированных специальных функций.

Для простых примеров обогащения см. руководство по словарям здесь. Ниже мы сосредоточимся на общих задачах обогащения для наблюдаемости.

Использование IP словарей

Геообогащение журналов и трассировок с использованием цен и широты на основании IP-адресов - это распространенное требование наблюдаемости. Мы можем достичь этого, используя структурированный словарь ip_trie.

Мы используем общедоступный набор данных DB-IP на уровне города, предоставленный DB-IP.com на условиях лицензии CC BY 4.0.

Из README мы можем видеть, что данные структурированы следующим образом:

Учитывая эту структуру, давайте начнем с того, чтобы заглянуть в данные, используя табличную функцию 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 dictionary structure, чтобы сопоставить наши сетевые префиксы (CIDR-блоки) с координатами и кодами стран. Следующий запрос определяет словарь, используя эту структуру и вышеприведенную таблицу как источник.

Мы можем выбрать строки из словаря и подтвердить, что этот набор данных доступен для запросов:

Периодическое обновление

Словари в ClickHouse периодически обновляются на основе данных в подлежащей таблице и использованного выше условия lifetime. Чтобы обновить наш Geo IP словарь, чтобы отразить последние изменения в наборе данных DB-IP, нам просто нужно вновь вставить данные из удаленной таблицы geoip_url в нашу таблицу geoip с применением преобразований.

Теперь, когда у нас есть загруженные данные Geo IP в нашем словаре ip_trie (удобно также названном ip_trie), мы можем использовать его для геолокации IP. Это можно выполнить с помощью функции dictGet() следующим образом:

Обратите внимание на скорость извлечения здесь. Это позволяет нам обогащать журналы. В этом случае мы выбираем выполнить обогащение во время запроса.

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

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

Обновление периодически

Пользователи, вероятно, захотят, чтобы словарь обогащения ip периодически обновлялся на основе новых данных. Это можно достичь с помощью условия LIFETIME словаря, которое будет периодически перезагружать словарь из подлежащей таблицы. Чтобы обновить подлежащую таблицу, см. "Обновляемые материализованные представления".

Предоставленные выше страны и координаты предлагают возможности визуализации, выходящие за рамки группировки и фильтрации по странам. За вдохновением смотрите "Визуализация геоданных".

Использование словарей Regex (разбор User Agent)

Разбор строк user agent является классической задачей регулярного выражения и общим требованием в наборах данных на основе журналов и трассировок. ClickHouse обеспечивает эффективный разбор user agents с помощью Словарей Дерева Регулярных Выражений.

Словари дереев регулярных выражений определяются в открытом исходном коде ClickHouse с использованием типа источника словаря YAMLRegExpTree, который предоставляет путь к YAML-файлу, содержащему дерево регулярных выражений. Если вы хотите предоставить свой собственный словарь регулярных выражений, детали о необходимой структуре можно найти здесь. Ниже мы сосредотачиваемся на разборе user-agent, используя uap-core и загружаем наш словарь для поддерживаемого формата CSV. Этот подход совместим с OSS и ClickHouse Cloud.

примечание

В приведенных ниже примерах мы используем снимки последних регулярных выражений uap-core для разбора user-agent с июня 2024 года. Последний файл, который иногда обновляется, можно найти здесь. Пользователи могут следовать шагам здесь, чтобы загрузить в CSV-файл, используемый ниже.

Создайте следующие таблицы памяти. Эти таблицы хранят наши регулярные выражения для разбора устройств, браузеров и операционных систем.

Эти таблицы могут быть заполнены из следующих общедоступных CSV-файлов, используя табличную функцию url:

С нашими таблицами памяти, заполненными загрузим наши Словари Регулярных Выражений. Обратите внимание, что мы должны указать ключевые значения как колонки - это будут атрибуты, которые мы можем извлечь из user agent.

С этими загруженными словарями мы можем предоставить пример user-agent и протестировать наши новые возможности извлечения словарей:

Учитывая, что правила для user agents редко меняются, и что словарь требуется обновлять только в ответ на новые браузеры, операционные системы и устройства, имеет смысл выполнять это извлечение во время вставки.

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

Это требует от нас изменить схему для целевой таблицы 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 будет как небольшим, так и актуальным.

Поскольку объединение строк происходит асинхронно, может быть больше одной строки за час, когда пользователь выполняет запрос. Чтобы гарантировать, что все незавершённые строки будут объединены во время выполнения запроса, у нас есть два варианта:

  • Использовать FINAL модификатор в имени таблицы (что мы сделали для запроса подсчёта выше).
  • Агрегировать по ключу сортировки, используемому в нашей конечной таблице, т.е. Timestamp и суммировать метрики.

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

Это ускорило наш запрос с 0.6с до 0.008с - более чем на 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 и получать связанные spans. Хотя это присутствует в ключе сортировки, его позиция в конце означает, что фильтрация будет не такой эффективной и, вероятно, значит, что при извлечении одной трассировки потребуется сканировать значительное количество данных.

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

Представление эффективно обеспечивает наличие минимальной и максимальной временной метки для трассы в таблице otel_traces_trace_id_ts. Эта таблица, упорядоченная по TraceId, позволяет эффективно извлекать эти временные интервалы. Эти диапазоны времени могут, в свою очередь, использоваться при запросе основной таблицы otel_traces. Более конкретно, при извлечении трассы по её id Grafana использует следующий запрос:

CTE здесь определяет минимальную и максимальную временную метку для идентификатора трассы ae9226c78d1d360601e6383928e4d22d, прежде чем использовать это для фильтрации основной таблицы otel_traces для её связанных spans.

Этот же подход можно применить для аналогичных шаблонов доступа. Мы исследуем похожий пример в Моделировании Данных здесь.

Использование проекций

Проекции ClickHouse позволяют пользователям указывать несколько клауз ORDER BY для таблицы.

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

Мы привели пример, где материализованное представление отправляет строки в целевую таблицу с другим ключом сортировки, чем оригинальная таблица, принимающая вставки, для оптимизации поиска по идентификатору трассы.

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

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

Проекции против материализованных представлений

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

Рассмотрим следующий запрос, который фильтрует таблицу otel_logs_v2 по кодам ошибки 500. Это, вероятно, распространённый шаблон доступа для логирования, когда пользователи хотят фильтровать по кодам ошибок:

Используйте Null для оценки производительности

Мы не выводим результаты здесь, используя FORMAT Null. Это заставляет все результаты считываться, но не возвращаться, тем самым предотвращая преждевременное завершение запроса из-за LIMIT. Это нужно только для того, чтобы показать время, затраченное на сканирование всех 10 млн строк.

Приведённый выше запрос требует линейного сканирования с нашим выбранным ключом сортировки (ServiceName, Timestamp). Хотя мы могли бы добавить Status в конец ключа сортировки, улучшая производительность для указанного выше запроса, мы можем также добавить проекцию.

Обратите внимание, что мы сначала должны создать проекцию, а затем материализовать её. Эта последняя команда вызывает сохранение данных дважды на диске в двух различных порядках. Проекцию также можно определить при создании данных, как показано ниже, и она будет автоматически поддерживаться по мере вставки данных.

Важно отметить, что если проекция создается с помощью ALTER, её создание происходит асинхронно, когда команда MATERIALIZE PROJECTION выполняется. Пользователи могут подтвердить ход этой операции с помощью следующего запроса, дожидаясь is_done=1.

Если мы повторим приведённый выше запрос, мы увидим, что производительность значительно улучшилась за счёт дополнительного хранилища (см. "Измерение размера таблицы и сжатие" для информации о том, как это измерить).

В приведённом выше примере мы указываем столбцы, используемые в предыдущем запросе, в проекции. Это будет означать, что только эти указанные столбцы будут храниться на диске в части проекции, упорядоченной по Status. Если, наоборот, мы использовали бы SELECT *, все столбцы были бы сохранены. Хотя это и позволило бы большему количеству запросов (испанованным любым подмножеством столбцов) выиграть от проекции, возникнет дополнительное потребление хранилища. Для измерения дискового пространства и сжатия см. "Измерение размера таблицы и сжатие".

Вторичные / Индексы пропуска данных

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

Стандартные схемы OTel используют вторичные индексы в попытке ускорить доступ к доступу к картам. Хотя мы считаем их вообще неэффективными и не рекомендуем копировать их в вашу пользовательскую схему, индексы пропуска все ещё могут быть полезными.

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

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

Для запросов мониторинга вторичные индексы могут быть полезны, когда пользователи нуждаются в выполнении текстовых поисков. В частности, индексы фильтров Блума на основе ngram и токенов ngrambf_v1 и tokenbf_v1 могут использоваться для ускорения поиска по колонкам типа String с операторами LIKE, IN и hasToken. Важно отметить, что индекс на основе токенов генерирует токены, используя неалфавитные символы в качестве разделителей. Это означает, что только токены (или целые слова) могут быть сопоставлены во время выполнения запроса. Для более тонкого сопоставления можно использовать фильтр Блума N-грамм. Этот метод разбивает строки на N-граммы заданного размера, позволяя выполнять сопоставление подслов.

Чтобы оценить токены, которые будут сгенерированы, и, следовательно, сопоставлены, можно использовать функцию tokens:

Функция ngram предоставляет аналогичные возможности, где размер ngram может быть указан в качестве второго параметра:

Обратные индексы

ClickHouse также имеет экспериментальную поддержку обратных индексов в качестве вторичного индекса. Мы в настоящее время не рекомендуем их для наборов данных журналов, но ожидаем, что они заменят токенизированные фильтры Блума, когда те будут готовы к производству.

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

Здесь нам необходимо сопоставить размер N-граммы 3. Поэтому мы создаем индекс ngrambf_v1.

Индекс ngrambf_v1(3, 10000, 3, 7) принимает четыре параметра. Последний из них (значение 7) представляет собой сид. Остальные представляют собой размер N-граммы (3), значение m (размер фильтра) и количество хеш-функций k (7). k и m требуют настройки и будут зависеть от количества уникальных N-грамм/токенов и вероятности того, что фильтр приведет к ложному отрицанию - что подтверждает отсутствие значения в грануле. Мы рекомендуем эти функции для помощи в установке этих значений.

Если настроить правильно, ускорение может быть значительным:

Пример только

Вышеуказанное предназначено исключительно для иллюстрации. Мы рекомендуем пользователям извлекать структуру из своих журналов при вставке, а не пытаться оптимизировать текстовые поиски, используя токенизированные фильтры Блума. Тем не менее, есть случаи, когда у пользователей есть трассировки стека или другие большие строки, для которых текстовый поиск может быть полезен из-за менее детерминированной структуры.

Некоторые общие рекомендации по использованию фильтров Блума:

Целью фильтра Блума является фильтрация гранул, таким образом, чтобы избежать необходимости загружать все значения для колонки и выполнять линейный просмотр. Клаузула EXPLAIN с параметром indexes=1 может быть использована для определения количества гранул, которые были пропущены. Рассмотрим ответы ниже для исходной таблицы otel_logs_v2 и таблицы otel_logs_bloom с фильтром Блума N-грамм.

Фильтр Блума обычно будет быстрее только в том случае, если он меньше, чем колонка сама по себе. Если он больше, то выгода в производительности, вероятно, будет незначительной. Сравните размер фильтра с колонкой, используя следующие запросы:

В приведенных выше примерах мы можем видеть, что второй индекс фильтра Блума занимает 12MB - почти в 5 раз меньше, чем сжатый размер колонки, которая составляет 56MB.

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

Дополнительные сведения о вторичных индексации пропусков можно найти здесь.

Извлечение из карт

Тип Map широко распространен в схемах OTel. Этот тип требует, чтобы значения и ключи имели один и тот же тип, что достаточно для метаданных, таких как метки Kubernetes. Имейте в виду, что при запросе подключа ключа типа Map загружается весь родительский столбец. Если у карты много ключей, это может привести к значительному штрафу за запрос, поскольку необходимо прочитать больше данных с диска, чем если бы ключ существовал как отдельная колонка.

Если вы часто запрашиваете конкретный ключ, рассмотрите возможность перемещения его в собственный выделенный столбец на корне. Это, как правило, задача, которая выполняется в ответ на общие паттерны доступа и после развертывания и может быть трудно предсказать до начала эксплуатации. См. "Управление изменениями схемы" о том, как изменить вашу схему после развертывания.

Измерение размера таблицы и сжатия

Одной из основных причин, по которой ClickHouse используется для мониторинга, является сжатие.

Помимо значительного снижения затрат на хранение, меньшее количество данных на диске означает меньший ввод-вывод и более быстрые запросы и вставки. Снижение ввода-вывода перевешивает накладные расходы любого алгоритма сжатия относительно CPU. Улучшение сжатия данных должно быть первоочередной задачей при попытке гарантировать, что запросы ClickHouse выполняются быстро.

Подробности о измерении сжатия можно найти здесь.