Введение
Основные понятия ClickHouse
Таблицы относятся к базам данных в ClickHouse. По умолчанию используется база данных
default, но это можно изменить в OpenTelemetry Collector.
Как минимум, вам следует понимать следующие базовые принципы ClickHouse:
Эти понятия лежат в основе производительности ClickHouse. Они определяют, как записываются данные, как они организованы на диске и насколько эффективно ClickHouse может пропускать чтение данных во время выполнения запроса. Любая оптимизация в этом руководстве — будь то материализованные столбцы, индекс пропуска данных, первичные ключи, проекции или materialized view — опирается на эти базовые механизмы.
Перед началом настройки рекомендуется ознакомиться со следующей документацией ClickHouse:
- Создание таблиц в ClickHouse - Простое введение в таблицы.
- Части
- Партиции
- Слияния
- Первичные ключи/индексы
- Как ClickHouse хранит данные: части и гранулы - Более продвинутое руководство о том, как данные организованы и запрашиваются в ClickHouse, с подробным разбором гранул и первичных ключей.
- MergeTree- Расширенное справочное руководство по MergeTree, полезное для команд и внутренних деталей.
Оптимизация 1. Материализуйте часто запрашиваемые атрибуты
LogAttributes, ScopeAttributes и ResourceAttributes и вынести их в столбцы верхнего уровня с помощью материализованных столбцов.
Одной этой оптимизации часто достаточно, чтобы масштабировать развертывания ClickStack до десятков терабайт в день, и применять ее следует прежде, чем переходить к более продвинутым методам тонкой настройки.
Зачем материализовать атрибуты
Map(String, String). Это обеспечивает гибкость, но у запросов к вложенным ключам map есть важная особенность с точки зрения производительности.
При запросе одного ключа из столбца Map ClickHouse приходится читать с диска весь столбец map целиком. Если map содержит много ключей, это приводит к лишним операциям IO и замедляет запросы по сравнению с чтением отдельного столбца.
Материализация часто используемых атрибутов устраняет эти накладные расходы: значение извлекается во время вставки и сохраняется как полноценный столбец.
Материализованные столбцы:
- Вычисляются автоматически во время вставки
- Не могут быть явно заданы в операторах INSERT
- Поддерживают любые выражения ClickHouse
- Позволяют преобразовывать типы из String в более эффективные числовые типы или типы даты
- Позволяют использовать индекс пропуска данных и первичный ключ
- Сокращают объём чтения с диска, избавляя от необходимости читать map целиком
ClickStack автоматически обнаруживает материализованные столбцы, извлечённые из map, и прозрачно использует их при выполнении запросов, даже если пользователи продолжают обращаться к исходному пути атрибута.
Пример
ResourceAttributes:
ResourceAttributes.k8s.pod.name:"checkout-675775c4cc-f2p9c":
В результате получается SQL-предикат, похожий на следующий:
ResourceAttributes для каждой подходящей строки — он может быть очень большим, если Map содержит много ключей.
Если по этому атрибуту часто выполняются запросы, его следует материализовать как столбец верхнего уровня.
Чтобы извлекать имя пода во время вставки, добавьте материализованный столбец:
PodName.
Теперь пользователи могут эффективно выполнять запросы по именам подов, используя синтаксис Lucene, например PodName:"checkout-675775c4cc-f2p9c"
Для вновь вставленных данных это позволяет полностью избежать доступа к Map и значительно сократить I/O.
Однако даже если пользователи продолжают выполнять запросы по исходному пути атрибута, например ResourceAttributes.k8s.pod.name:"checkout-675775c4cc-f2p9c", ClickStack автоматически перепишет запрос внутри системы так, чтобы использовать материализованный столбец PodName, то есть с применением предиката:
По умолчанию материализованные столбцы исключаются из
запросов SELECT *. Это сохраняет инвариант, согласно которому результаты запроса всегда можно снова вставить в таблицу.Материализация исторических данных
system.mutations, например.
is_done не станет равным 1.
Оптимизация 2. Добавление индексов пропуска данных
- Фильтрацию строк с высокой мощностью, таких как TraceId, идентификаторы сеансов, ключи или значения атрибутов
- Фильтрацию по числовым диапазонам, например по длительности span
bloom-фильтры
PodName:"checkout-675775c4cc-f2p9c".
bloom-фильтры наиболее эффективны, когда распределение значений таково, что конкретное значение встречается в относительно небольшом числе частей. Это часто естественным образом происходит в рабочих нагрузках обсервабилити, где такие метаданные, как имена подов, trace ID или идентификаторы сеансов, коррелируют со временем и, следовательно, группируются по ключу сортировки таблицы.
Как и любые индексы пропуска данных, bloom-фильтры следует добавлять выборочно и проверять на реальных шаблонах запросов, чтобы убедиться, что они приносят измеримую пользу — см. “Оценка эффективности индекса пропуска данных.”
Индексы Min-max
SpanAttributes:
Материализация индекса пропуска данных
Материализация индексов пропуска данныхМатериализация индекса пропуска данных обычно является лёгкой и безопасной операцией, особенно для minmax-индексов. Для индексов bloom-фильтра на больших датасетах может быть удобнее выполнять материализацию по одной партиции, чтобы лучше контролировать потребление ресурсов, например:
is_done = 1.
После завершения убедитесь, что данные индекса созданы:
0.01 до 0.05 даёт индекс меньшего размера, который вычисляется быстрее, но ценой менее агрессивного отсечения. Хотя пропускаться будет меньше гранул, общая задержка запроса может снизиться за счёт более быстрой обработки индекса.
Поэтому настройка параметров bloom-фильтра — это оптимизация, зависящая от рабочей нагрузки, и её следует проверять на реальных шаблонах запросов и объёмах данных, близких к продакшн.
Дополнительные сведения об индексах пропуска данных см. в руководстве “Понимание индексов пропуска данных в ClickHouse.”
Оценка эффективности индекса пропуска данных
EXPLAIN indexes = 1, который показывает, сколько частей и гранул отсекается на каждом этапе планирования запроса. В большинстве случаев желательно видеть существенное сокращение числа гранул на этапе Skip — в идеале после того, как первичный ключ уже сузил пространство поиска. Индексы пропуска данных оцениваются после отсечения партиций и отсечения по первичному ключу, поэтому их влияние лучше всего измерять относительно оставшихся частей и гранул.
EXPLAIN подтверждает, происходит ли отсечение, но не гарантирует итогового ускорения. Вычисление индексов пропуска данных требует затрат, особенно если индекс большой. Всегда выполняйте бенчмарк запросов до и после добавления индекса и его материализации, чтобы подтвердить реальное повышение производительности.
Например, рассмотрим индекс пропуска данных с bloom-фильтром по умолчанию для TraceId, включенный в схему Traces по умолчанию:
EXPLAIN indexes = 1, чтобы оценить, насколько это эффективно для селективного запроса:
FORMAT Null, чтобы избежать накладных расходов на сериализацию результата, и отключите кэш условий запроса, чтобы прогоны оставались воспроизводимыми:
use_query_condition_cache гарантирует, что результаты не будут зависеть от кэшированных решений о фильтрации, а установка use_skip_indexes = 0 дает чистую отправную точку для сравнения. Если отсечение данных эффективно, а затраты на вычисление индекса невелики, запрос с индексом должен быть заметно быстрее, как в примере выше.
Когда добавлять индексы пропуска данных
Оптимизация 3. Изменение первичного ключа
Примечание о терминологииВ этом документе термин “ключ сортировки” используется как взаимозаменяемый с термином “первичный ключ”. Строго говоря, в ClickHouse это разные понятия, но в ClickStack они обычно относятся к одним и тем же столбцам, указанным в
ORDER BY таблицы. Подробнее см. документацию ClickHouse о выборе первичного ключа, отличающегося от ключа сортировки.- Логи (
otel_logs) -(ServiceName, TimestampTime, Timestamp) - Трассировки (‘otel_traces) -
(ServiceName, SpanName, toDateTime(Timestamp))
Выбор первичного ключа
Изменение первичного ключа по умолчаниюПервичные ключи по умолчанию достаточны в большинстве случаев. Вносить изменения следует с осторожностью и только при четком понимании шаблонов запросов. Изменение первичного ключа может ухудшить производительность других сценариев, поэтому тестирование обязательно.
- Выбирайте столбцы, соответствующие вашим типичным фильтрам и паттернам доступа. Если вы обычно начинаете расследование в области обсервабилити с фильтрации по конкретному столбцу, например по имени пода, этот столбец будет часто использоваться в секциях
WHERE. Такие столбцы стоит включать в ключ в первую очередь, а не те, которые используются реже. - Предпочитайте столбцы, которые при фильтрации позволяют исключить большую часть строк и тем самым сократить объем читаемых данных. Имена сервисов и коды status часто являются хорошими кандидатами — во втором случае только если вы фильтруете по значениям, исключающим большинство строк; например, фильтрация по кодам 200 в большинстве систем охватит большую часть строк, тогда как ошибки 500 будут соответствовать лишь небольшому подмножеству.
- Предпочитайте столбцы, которые, вероятно, будут сильно коррелировать с другими столбцами таблицы. Это поможет обеспечить их смежное хранение, что улучшит сжатие.
- Операции
GROUP BY(агрегации для диаграмм) иORDER BY(сортировка) по столбцам из ключа сортировки могут быть более эффективны с точки зрения использования памяти.
Изменение первичного ключа
SeverityText расположен перед ServiceName.
1
Создайте новую таблицу
Ключ сортировки и первичный ключОбратите внимание: в примере выше необходимо указать
PRIMARY KEY и ORDER BY.
В ClickStack они почти всегда совпадают.
ORDER BY управляет физической организацией данных, а PRIMARY KEY определяет разреженный индекс.
В редких случаях при очень больших рабочих нагрузках они могут различаться, но большинству пользователей следует держать их согласованными.Дозагрузка существующих данных в новую таблицу в крупных инсталляциях редко оправданна. Затраты на вычислительные ресурсы и IO обычно высоки и не окупаются выигрышем в производительности. Вместо этого дайте старым данным истечь через TTL, а новые данные пусть используют преимущества улучшенного ключа.
SeverityText добавляется как первый столбец первичного ключа. В этом случае новая таблица создаётся для новых данных, а старая сохраняется для исторического анализа.
1
Создайте новую таблицу
Создайте новую таблицу с нужным первичным ключом. Обратите внимание на суффикс_23_01_2025 — замените его на текущую дату. Например:2
Создайте Merge-таблицу
Движок Merge (не путать с MergeTree) сам не хранит данные, но позволяет одновременно читать из любого числа других таблиц.currentDatabase() предполагает, что команда выполняется в правильной базе данных. В противном случае явно укажите имя базы данных.otel_logs.3
Обновите HyperDX, чтобы читать из merge-таблицы
Настройте HyperDX на использованиеotel_logs_merge в качестве таблицы для источника данных журналов.На этом этапе запись по-прежнему идёт в otel_logs с исходным первичным ключом, а чтение выполняется через merge-таблицу. Для пользователей ничего не меняется, и на ингестию это не влияет.4
Поменяйте таблицы местами
Теперь операторEXCHANGE используется для атомарной перестановки имён таблиц otel_logs и otel_logs_23_01_2025.otel_logs с обновлённым первичным ключом. Существующие данные остаются в otel_logs_23_01_2025 и по-прежнему доступны через merge-таблицу. Суффикс указывает дату применения изменения и соответствует самой поздней временной метке в этой таблице.Этот процесс позволяет изменить первичный ключ без прерывания приёма и без какого-либо заметного влияния для пользователей.SeverityNumber, а не SeverityText. Описанный ниже процесс можно повторять столько раз, сколько потребуется при изменении первичного ключа.
1
Создайте новую таблицу
Создайте новую таблицу с нужным первичным ключом. В примере ниже30_01_2025 используется в качестве суффикса, обозначающего дату таблицы. Например:2
Обменяйте таблицы
Теперь операторEXCHANGE используется для атомарной замены имен таблиц otel_logs и otel_logs_30_01_2025.otel_logs с обновленным первичным ключом. Старые данные остаются в otel_logs_30_01_2025 и доступны через merge-таблицу.Избыточные таблицыЕсли настроены политики TTL, что рекомендуется, таблицы со старыми первичными ключами, в которые больше не записываются данные, будут постепенно освобождаться по мере истечения срока хранения данных. Их следует отслеживать и периодически очищать, когда в них больше не остается данных. В настоящее время этот процесс очистки выполняется вручную.
Оптимизация 4. Использование materialized view
Оптимизация 5. Использование проекций
ORDER BY базовой таблицы, что позволяет ClickHouse эффективнее отсекать данные для шаблонов доступа, не соответствующих исходному порядку.
Materialized views могут давать схожий эффект, явно записывая строки в отдельную целевую таблицу с другим ключом сортировки. Главное отличие в том, что проекции поддерживаются ClickHouse автоматически и прозрачно, тогда как materialized views — это явные таблицы, которые ClickStack должен регистрировать и выбирать явно.
Когда запрос обращается к базовой таблице, ClickHouse оценивает базовую структуру и все доступные проекции, анализирует их первичные индексы и выбирает ту структуру, которая позволяет получить корректный результат, прочитав минимальное число гранул. Это решение автоматически принимает анализатор запросов.
Поэтому в ClickStack проекции лучше всего подходят для простого переупорядочивания данных, когда:
- Шаблоны доступа принципиально отличаются от первичного ключа по умолчанию
- Непрактично охватить все сценарии работы одним ключом сортировки
- Вы хотите, чтобы ClickHouse прозрачно выбирал оптимальную физическую структуру
Пример проекций
Используйте подстановочные шаблоныВ примере с проекцией выше используется подстановочный шаблон (
SELECT *). Хотя выбор подмножества столбцов может снизить накладные расходы на запись, это также ограничивает случаи, когда проекцию можно использовать, поскольку подходят только те запросы, которые можно полностью выполнить по этим столбцам. В ClickStack это часто сводит использование проекций к очень узким сценариям. Поэтому обычно рекомендуется использовать подстановочный шаблон, чтобы максимально расширить область применения.Материализация проекции может занять много времени и потребовать значительных ресурсов. Поскольку данные обсервабилити обычно удаляются по TTL, делать это следует только в случае крайней необходимости. В большинстве случаев достаточно, чтобы проекция применялась только к вновь принимаемым данным, оптимизируя наиболее часто запрашиваемые временные диапазоны, например последние 24 часа.
SELECT *), а фильтры запроса хорошо согласуются с ORDER BY проекции.
Запросы с фильтрацией по TraceId (особенно на точное совпадение) и указанием временного диапазона выиграют от приведенной выше проекции. Например:
TraceId или в основном фильтруют по другим измерениям, не являющимся первыми в ключе сортировки проекции, обычно не дают выигрыша (и вместо этого могут читать данные из базовой структуры).
Проекции также могут хранить агрегации (аналогично materialized views). В ClickStack агрегации на основе проекций обычно не рекомендуются, поскольку выбор зависит от анализатора ClickHouse, а их использование сложнее контролировать и предсказать. Вместо этого лучше использовать явные materialized views, которые ClickStack может регистрировать и целенаправленно выбирать на уровне приложения.
Издержки и рекомендации
- Накладные расходы на вставку: Проекция
SELECT *с другим ключом сортировки фактически приводит к двойной записи данных, что увеличивает I/O при записи и может потребовать дополнительных ресурсов CPU и пропускной способности диска для поддержания ингестии. - Используйте экономно: Проекции стоит применять только для действительно разных сценариев доступа, когда второй физический порядок даёт заметное отсечение данных для большой доли запросов — например, если две команды работают с одним и тем же набором данных принципиально по-разному.
- Проверяйте с помощью бенчмарков: Как и при любой оптимизации, сравнивайте реальную задержку запросов и использование ресурсов до и после добавления и материализации проекции.
Облегчённые проекции с _part_offset
Облегчённые проекции — в статусе бета для ClickStackОблегчённые проекции на основе
_part_offset не рекомендуются для рабочих нагрузок ClickStack. Хотя они уменьшают объём хранилища и I/O при записи, они могут приводить к большему числу произвольных обращений при выполнении запросов, а их поведение в продакшне при нагрузках масштаба обсервабилити всё ещё оценивается. Эта рекомендация может измениться по мере развития этой возможности и накопления большего объёма эксплуатационных данных._part_offset в базовой таблице вместо дублирования полных строк. Это может значительно сократить накладные расходы на хранение, а недавние улучшения позволяют выполнять pruning на уровне гранул, из-за чего такие проекции всё больше напоминают настоящие вторичные индексы. См.:
Альтернативы
- Настройте OpenTelemetry Collector так, чтобы он записывал данные в две таблицы с разными ключами
ORDER BY, и создайте отдельные источники ClickStack для каждой таблицы. - Создайте materialized view как конвейер копирования, то есть подключите materialized view к основной таблице так, чтобы она записывала необработанные строки во вторичную таблицу с другим ключом сортировки (это шаблон денормализации или маршрутизации). Создайте источник для этой целевой таблицы. Примеры можно найти здесь.