Вышел CHYT 2.18

Рассказываем про ключевые обновления

Как и версия 2.17, текущая основана на ClickHouse  24.8. В новом релизе мы поработали над процессами чтения и добавили ряд оптимизаций — далее расскажем об основных улучшениях.

Использование min/max-статистик при исполнении запроса

Чтобы лучше разобраться в деталях, кратко рассмотрим, как устроено хранение данных в YT.

В YT таблицы состоят из чанков, которые не монолитны: каждый из них разделён на блоки, в которых и хранятся данные таблицы.

Поскольку речь идёт о CHYT, исходим из того, что таблица хранится в Scan‑формате — то есть данные организованы поколоночно.

Полноэкранное изображение

Для ускорения выполнения запросов чанки содержат не только блоки с данными, но и сопутствующую статистику — например, min/max‑значения.

CHYT уже использовал эти данные для скиппинга: система пропускала чанки, которые не удовлетворяли предикату запроса. В новой версии функционал расширен — теперь статистику можно применять и для вычисления min/max‑выражений над колонками в запросах.

Рассмотрим пример исполнения запроса.

SELECT MIN(colunn1), MAX(column1) FROM `//home/my_table`

Ранее процесс выглядел так: инстанс считывал все данные по запрашиваемой колонке из чанка и локально вычислял агрегаты.

Полноэкранное изображение

Однако наличие min/max‑статистик в чанке позволяет обойтись чтением именно этих данных — без загрузки всей колонки.

Полноэкранное изображение

Эту функциональность мы и добавили в новом релизе.

Важно, что текущая реализация работает лишь в ограниченных сценариях:

  • в запросе нет предиката (при наличии фильтрации всё равно придётся считывать части данных);
  • под SELECT есть только min/max‑выражения.

Несколько показателей из ClickBench. Запрос Q6 (иллюстрирующий описанный выше пример) в новой версии выполняется в 4  раза быстрее по сравнению с предыдущей.

Полноэкранное изображение

Оптимизация работы с колонками низкой кардинальности

Формат хранения YT позволяет выбирать оптимальную кодировку для данных прямо в момент записи.

Если в записываемом чанке присутствует колонка с небольшим количеством уникальных значений, к данным может быть применено словарное кодирование (dictionary encoding). Важно учитывать, что кодирование выполняется на уровне блока — то есть независимо для каждого их них.

Рассмотрим релевантный пример работы dictionary encoding для второй колонки.

Полноэкранное изображение

До текущего релиза CHYT неизменно материализовывал все кодировки в полный набор значений, что оправдано для большинства сценариев. Однако в случае DISTINCT‑запросов или запросов с агрегацией по уникальным значениям это может быть невыгодно.

Мы реализовали механизм, позволяющий в ряде сценариев обходиться без материализации словарей. Это существенно снижает нагрузку на CPU — самый ресурсоёмкий компонент.

Для теста возьмём таблицу из 14  чанков — около 100 млн строк, объём данных примерно 11 GiB. Рассмотрим следующий запрос:

SELECT DISTINCT country_name FROM `//home/test_table`

Колонка country_name содержит всего 14 уникальных значений.

Без оптимизации среднее время трёх запусков — 2,6 секунд, а с ней — 0,85 секунд.

Рассмотрим ещё запрос с фильтрацией:

SELECT DISTINCT country_name FROM `//home/test_table` PREWHERE id > 42

Почему именно PREWHERE — расскажем ниже.

Используемый предикат отфильтровывает примерно половину таблицы.

Без оптимизации среднее время трёх запусков — 8,4 секунд, а с ней — 6,6 секунд.

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

  • запрос является простым DISTINCT/uniq-запросом без использования дополнительных агрегаций и без фильтрации;
  • запрос соответствует условиям из первого пункта и включает простую фильтрацию, которая может быть полностью выполнена на этапе PREWHERE.

В примере запрос включал явный PREWHERE — это позволило не полагаться на move‑to‑prewhere от CH‑оптимизатора. Таким образом, оптимизация не будет применена, если:

  • предикат не может быть перенесён в PREWHERE;
  • PREWHERE в нём не указан явно;
  • CH‑оптимизатор не выполнил move‑to‑prewhere.

Ограничения на использование PREWHERE обусловлены внутренним устройством процесса чтения в CHYT.

Дело в том, что PREWHERE — это единственный этап исполнения, на котором возможно вычисление предикатов и доступны «сырые» данные из YT.

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

Гранулярные чтения

Поскольку CHYT использует «ванильный» движок ClickHouse, можно было бы ожидать сопоставимой производительности с условным ClickHouse, работающим поверх S3. Однако это не так из‑за отличий в архитектуре слоя хранения.

Ключевое отличие хранилища YT от ClickHouse — размер минимальной порции данных, которую может прочитать система. В ClickHouse такой порцией является гранула, по умолчанию содержащая не более 8192 строк или до 10 МБ данных для «тяжёлых» строк. Гранулы лежат в основе эффективного скиппинга данных и позволяют ClickHouse экономно обращаться к хранилищу.

В хранилище YT основная единица хранения — чанк. Размер чанка обычно составляет от сотен мегабайт до нескольких гигабайт. Внутри него данные дополнительно разбиты на блоки примерно по 16 МБ (для статических таблиц), однако с точки зрения читателя характерный минимальный объём чтения — именно чанк. Число строк в чанке или блоке не фиксировано.

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

Полноэкранное изображение

Продвинутые пользователи, вероятно, знают, что RichYPath позволяет указывать row‑selector — и таким образом читать нужный диапазон строк или ключей (в случае сортированной таблицы). Некоторые применяют этот механизм через CHYT, чтобы ускорить запросы. Однако это неудобно, ведь каждый раз требуется вручную модифицировать запрос, добавляя нужный row‑selector, который ещё нужно вычислить. Кроме того, если использовать CHYT через BI‑инструмент, этот способ становится недоступным. Было бы полезно, если бы CHYT мог автоматически преобразовывать предикат запроса в соответствующий row‑selector — и в результате считывать только необходимые строки.

Поэтому мы добавили такую фичу. Вывод нужного диапазона ключей из предиката происходит в точности как в YT QL, дополнительные детали можно узнать здесь.

Полноэкранное изображение

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

  1. предикат должен задавать условия на префикс ключа сортировки таблицы;
  2. предикат должен состоять только из простых операторов сравнения, IN и BETWEEN;
  3. должны отсутствовать комплексные условия типа (a, b) > (1, 2), то есть допускаются только простые конструкции, такие как a > 1 and b > 2 (это временное ограничение, которое планируется устранить в будущих релизах).

Ещё немного данных из ClickBench. Помимо явных запросов с чтением ключа (как в приведённом выше примере), особого внимания заслуживают запросы Q36–Q41: они выполняют фильтрацию по ключу и нетривиальную агрегацию.

Полноэкранное изображение

Pull mode исполнение

Скорее всего вы знаете, что CHYT исполняет запросы распределённо, если нет, то можно ознакомиться с этим тут. Получив запрос, инстанс берёт на себя роль координатора и в первую очередь формирует задачи чтения — они представляют собой «срезы» таблицы.

Полноэкранное изображение

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

Полноэкранное изображение

Здесь стоит отметить два важных аспекта:

  1. CHYT активно использует кэш прочитанных из системы хранения блоков. Это позволяет сокращать число обращений к узлу системы, где эти блоки расположены, — при выполнении последующих запросов.
  2. Последовательность распределения частей таблицы по инстансам остаётся стабильной между запросами — это повышает cache locality. Проще говоря, при повторном исполнении одного и того же запроса конкретная задача неизменно попадает на тот же инстанс.

Однако есть нюансы:

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

В результате возникает проблема tail latency: весь запрос «блокируется» из‑за медленной работы отдельных инстансов.

Полноэкранное изображение

Поэтому мы вдохновились механизмом Parallel replicas в ClickHouse и изменили процесс распределения задач чтения, реализовав новый подход — input specs pulling.

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

Полноэкранное изображение

Таким образом мы динамически распределяем нагрузку по инстансам клики, но с потерей cache locality.

Как протестировать новые оптимизации

Как включить

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

speclet (для всех запросов клики)

В UI-редактирования спеклета клики в категории «Advanced» в поле «YT config» необходимо прописать следующее:

{
    "settings": {
        "execution": {
            "enable_min_max_optimization": true,
            "enable_distinct_read_optimization": true,
            "enable_read_range_inferring": true,
            "enable_input_specs_pulling": true
        }
    }
}

Опции указаны в порядке их упоминания в этом посте.

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

SETTINGS chyt.execution.<option_name> = {0, 1}

Имена опций можно взять из конфигурационного файла сверху. 0/1 трактуются как false/true.

Как проверить

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

В query_log уже содержатся необходимые данные: нужно обратиться к колонке chyt_runtime_variables в событии QueryFinish (ивент завершения запроса) для записи запроса initial (запроса, инициированного координатором).

SELECT
    initial_query_id,
    query,
    ConvertYson(`chyt_query_runtime_variables`, 'pretty'),
FROM <path_to_query_log>
where type = 'QueryFinish' and is_initial_query
order by query_duration_ms desc

В частности, такие флаги, как:

  • use_min_max_optimization
  • try_optimize_distinct_read
  • use_read_range_inferring
  • use_input_specs_pulling

Оптимизация distinct read в префиксе имеет глагол try, так как итоговый результат её применения определяется в момент чтения. Тем не менее, если в initial-запросе этот флажок равен %false, то оптимизация точно не была применена.

Итог

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

Вышел CHYT 2.18
Войдите, чтобы сохранить пост