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

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

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

  • Выбор первичного ключа - В схемах по умолчанию используется 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 Collector, как описано здесь. В большинстве случаев пользователи обнаружат, что ClickHouse значительно более эффективно расходует ресурсы и быстрее, чем процессоры сборщика. Основной недостаток выполнения всей обработки событий в SQL заключается в связывании вашего решения с ClickHouse. Например, пользователи могут пожелать отправить обработанные журналы в альтернативные пункты назначения от OTel сборщика, например, S3.

Материализованные колонки

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

Нагрузка

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

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

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

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

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

примечание

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

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

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

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

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

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

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

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

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

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

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

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

Условия

Обратите внимание на использование условий выше для извлечения SeverityText и SeverityNumber. Эти функции чрезвычайно полезны для формирования сложных условий и проверки, если значения установлены в картах - мы наивно предполагаем, что все ключи существуют в LogAttributes. Мы рекомендуем пользователям ознакомиться с ними - они ваши помощники в разборе журналов, наряду с функциями для работы с null значениями!

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

Выбранные здесь типы основаны на оптимизациях, обсуждаемых в "Оптимизация типов".

примечание

Обратите внимание, как мы радикально изменили нашу схему. На самом деле, пользователи, вероятно, также захотят сохранить колонки Trace, а также колонку ResourceAttributes (которая обычно содержит метаданные Kubernetes). Grafana может использовать колонки трассировки для предоставления функциональности связывания между журналами и трассировками - см. "Использование Grafana".

Ниже мы создаем материализованное представление otel_logs_mv, которое выполняет вышеуказанный выбор для таблицы otel_logs и отправляет результаты в otel_logs_v2.

Вышеизложенное визуализируется ниже:

NEEDS ALT

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

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

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

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

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

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

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

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

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

Псевдонимы по умолчанию исключены

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

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

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

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

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

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

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

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

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

NEEDS ALT

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

Ускорение соединений

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Использование регулярных выражений с помощью словарей (Парсинг User Agent)

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

Словари дерева регулярных выражений определяются в ClickHouse open-source с использованием типа источника словаря 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.6s до 0.008s - более чем в 75 раз!

примечание

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

Более сложный пример

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

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

Чтобы сохранить подсчет кардинальности для инкрементального обновления, требуется AggregatingMergeTree.

Чтобы ClickHouse знал, что агрегированные состояния будут храниться, мы определяем колонку UniqueUsers как тип AggregateFunction, указывая функцию источника частичных состояний (uniq) и тип исходного столбца (IPv4). Как и в случае с SummingMergeTree, строки с одинаковым значением ключа ORDER BY будут объединены (Hour в данном примере).

Связанное материализованное представление использует ранее указанный запрос:

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

После того как данные будут повторно загружены, через перезапуск Collector, мы можем подтвердить наличие 113 строк в таблице unique_visitors_per_hour.

Наш конечный запрос должен использовать суффикс Merge для наших функций (так как столбцы хранят частичные агрегированные состояния):

Обратите внимание, что мы используем GROUP BY здесь вместо использования FINAL.

Использование материализованных представлений (инкрементальные) для быстрого поиска

Пользователи должны учитывать свои модели доступа при выборе ключа сортировки ClickHouse с колонками, которые часто используются в фильтрах и агрегатных выражениях. Это может быть ограничительным в случае наблюдаемости, когда пользователи имеют более разнообразные модели доступа, которые нельзя охватить в одном наборе колонок. Это лучше всего иллюстрируется примером, встроенным в стандартные схемы OTel. Рассмотрим стандартную схему для трасс:

Эта схема оптимизирована для фильтрации по ServiceName, SpanName и Timestamp. В трассировке пользователям также необходима возможность делать запросы по конкретному TraceId и извлекать связанные спаны трассы. Хотя это присутствует в ключе сортировки, его расположение в конце означает, что фильтрация не будет такой эффективной, и, вероятно, потребуется сканировать значительное количество данных при извлечении одной трассы.

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

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

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

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

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

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

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

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

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

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

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

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

NEEDS ALT

Рассмотрим следующий запрос, который фильтрует нашу таблицу 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.

Здесь нам нужно совмещать по размеру ngram 3. Поэтому мы создаем индекс ngrambf_v1.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сведения о том, как измерять сжатие, можно найти здесь.