Clickbait-картинка
Clickbait-картинка

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

Наша статья будет инженерной, а не маркетинговой. Мы увидим, как:

  • Разработчики StarRocks и Doris затачивают дефолты своих движков под бенчмарки, жертвуя стабильностью

  • Три родственных движка — Impala, StarRocks и Doris — с трудом справляются с конкурентной нагрузкой

  • Trino реализует эффективный шедулер запросов, но имеет ряд дефектов, ухудшающих производительность

  • Presto строит хорошие планы запросов, но демонстрирует катастрофически плохую производительность из-за отсутствия одной фичи

Не ждите чудес объективности — по всем канонам жанра в бенчмарке победит наш движок CedrusData (основан на Trino). Но он сделает это заслуженно.

Введение

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

  1. За счет чего ваш движок оказался быстрее?

  2. На какие компромисы вы пошли, чтобы показать высокую скорость?

  3. Насколько сложно конкурентам будет добраться до вас?

Наша компания создает российскую аналитическую lakehouse-платформу CedrusData на основе open-source проектов Iceberg, Trino, Spark и Flink. Высокая скорость обработки данных является нашим важным преимуществом. Но вы не найдете на нашем сайте статей, в которых мы «обгоняем N в M раз», где N — имя конкурирующего продукта, а M — произвольное натуральное число.

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

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

Идея этой статьи родилась в процессе рутинной работы над производительностью CedrusData.

Заявления вендоров

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

Осторожно, цифры

На западе излюбленными мальчиками для бития являются лидирующие облачные платформы. Раз в месяц появляется очередной стартап, который декларирует многократное ускорение и снижение костов относительно Snowflake/Databricks/<ваш вариант>. Пример:

Российский рынок ориентирован на перепродажу open-source решений, разрабатываемых в США или Китае. "Новый" open-source использует для своего продвижения те же тезисы, только мишенями являются лидирующие open-source продукты: Spark, Trino, ClickHouse.

Doris в 40 раз быстрее ClickHouse:

StarRocks многократно быстрее ClickHouse, Druid и Trino:

DuckDB уничтожает Spark:

Не отстают и коммерческие вендоры. Dremio быстрее в 20 раз (быстрее чего?):

Starburst быстрее в 6.3 раза (отдельное спасибо за высокую точность измерений, но все же — быстрее чего?):

Российские вендоры поддерживают западных коллег, добавляя локальный колорит — "медленный" Greenplum.

Если вы думаете, что Doris со своим 40-кратным приростом относительно ClickHouse немного перегибает палку, то подержите мое пиво:

В 2013-2015 годах я разрабатывал движок, который был в 100 (прописью — сто) раз быстрее Hadoop. Его реклама крутилась в YouTube, а пилоты проходили в крупнейших мировых IT компаниях. Слабо переплюнуть?

Бенчмарк

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

Датасет

  • Storage: Yandex Object Store, все настройки по умолчанию. Он работает нестабильно, производительность одного и того же запроса в разные дни может варьировать до 30 процентов. Для исключения данного эффекта все измерения были произведены последовательно в максимально сжатое время с постоянным контролем throughput/latency до S3

  • TPC-DS со scale factor 10000, что соответствует 10 Tb сырых данных

  • Формат хранения: Parquet со сжатием Snappy, итоговый размер порядка 3.5 Tb

  • Табличный формат: Iceberg с компакшеном после записи и собранными NDV

  • Таблицы фактов партиционированы по дате (catalog_returns[cr_returned_date_sk], catalog_sales[cs_sold_date_sk], store_returns[sr_returned_date_sk], store_sales[ss_sold_date_sk], web_returns[wr_returned_date_sk], web_sales[ws_sold_date_sk], inventory[inv_date_sk])

  • Metastore: HMS любой версии 3.x или 4.x (не принципиально для целевого бенчмарка). Хотелось взять наш CedrusData Catalog, но Impala и Doris не поддерживают в должной мере REST-каталоги.

Машина

  • Yandex Compute Cloud, Intel Ice Lake, 32 vCPU, 256 GiB RAM

  • SSD 150 GiB

  • Пропускная способность сетевого канала к S3: ~700 MiB/s

Окружение

  • OS: Ubuntu 22, все настройки по умолчанию

  • JDK:

    • Temurin 24 для Trino (более ранние версии не поддерживаются)

    • Temurin 17 для Apache Doris (более поздние версии не поддерживаются)

    • Temurin 21 (LTS) для остальных продуктов

Продукты

Мы также будем использовать DuckDB 1.4.1 для дополнительной оценки эффективности работы ядра целевого продукта. DuckDB работает с Iceberg таблицами через extension httpfs, минуя HMS.

Настройки продуктов

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

    • Apache Doris: fdb, ms, fe, be (cloud mode)

    • Apache Impala: statestored, catalogd, impalad

    • StarRocks: fe, cn

    • CedrusData, Trino, Presto: coordinator, worker

  • Worker-узлу продукта явно выделено 90% памяти машины согласно официальной документации (230.4 GiB)

  • Worker-узлу доступен уровень параллелизма не меньший, чем количество процессоров на сервере

  • Любая работа с дисками отключена (кэши, спилинг)

  • Для всех продуктов сконфигурировано использование всех возможных статистик Iceberg, включая Puffin файлы с NDV. Исключение: Apache Doris не поддерживает Puffin

Тюнинг продуктов

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

Так, считаем допустимым индивидуальный тюнинг, если без него продукт слишком рано перестает справляться с нагрузкой. Звучит кринжово, но забегая вперед скажу — этот пункт будет активирован только для StarRocks, оптимизатор которого не может выбрать оптимальный план. В противном случае до финала смог бы дойти только CedrusData.

Запрос

Мы будем рассматривать запрос TPC-DS Q1. Он читает умеренное количество данных из S3 (~11GiB), хорошо нагружает CPU и память за счет агрегатов с высокой кардинальностью, а также имеет достаточно сложный план с CTE, коррелированным подзапросом и пятью операторами JOIN (с учетом декорреляции). Таким образом, данный запрос позволяет нам оценить работу различных компонентов ядра: оптимизатора, экзекьютора, подсистемы работы с S3.

with customer_total_return as
         (select sr_customer_sk     as ctr_customer_sk
               , sr_store_sk        as ctr_store_sk
               , sum(sr_return_amt) as ctr_total_return
          from store_returns
             , date_dim
          where sr_returned_date_sk = d_date_sk
            and d_year = 2000
          group by sr_customer_sk
                 , sr_store_sk)
select c_customer_id
from customer_total_return ctr1
   , store
   , customer
where ctr1.ctr_total_return > (select avg(ctr_total_return) * 1.2
                               from customer_total_return ctr2
                               where ctr1.ctr_store_sk = ctr2.ctr_store_sk)
  and s_store_sk = ctr1.ctr_store_sk
  and s_state = 'TN'
  and ctr1.ctr_customer_sk = c_customer_sk
order by c_customer_id limit 100

Эксперимент 1: изолированный запуск запроса

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

Изолированный запуск запроса
Изолированный запуск запроса

Presto: не работают runtime-фильтры

Presto показывает наихудший результат с катастрофическими 203 сек на запрос. На первый взгляд, выглядит удивительно, учитывая, что разработчики Meta* (признана экстремистской организацией и запрещена в РФ) уже много лет интегрируют Velox в Presto, рассказывают на VLDB об успешной интеграции HBO (history-based optimizer), и т.д.

Беглый анализ показывает, что оптимизатор выбрал правильный план, структурно схожий с планами CedrusData и затюненного StarRocks (см. ниже). Здесь справа от двух сканов большой таблицы store_returns расположены сканы маленьких измерений date_dim и store, что делает возможным перенос из них runtime-фильтров, которые отфильтровывают 99% записей store_returns (29 млн вместо 2879 млн).

Что такое runtime-фильтр?

Runtime-фильтры — это оптимизация, которая позволяет сформировать дополнительный фильтр к probe-стороне JOIN на основе данных, полученных при обработке build-стороны. Полноценная реализация runtime-фильтров позволяет избегать чтения ненужных данных из стореджа (файлы, row group, page), а так же уменьшает количество строк, которые необходимо обрабатывать вышестоящим операторам. См. нашу статью с подробным рассказом о работе runtime-фильтров в Trino.

Presto не умеет пробрасывать распределенные runtime-фильтры
Presto не умеет пробрасывать распределенные runtime-фильтры

Но реальность жестока. В Presto отсутствуют важнейшая оптимизация — распределенные runtime-фильтры. Из-за этого Presto читает таблицу store_returns целиком и передает все ее записи в вышестоящий Join, на что уходит много времени. Суммарное количество прочитанных данных из S3 — 55.6 GiB. Другие продукты отфильтровывают около 80% записей на этапе чтения из storage (остается ~11-13 GiB), а до Join доходит 1-2% записей.

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

Impala: неоптимальный план, медленная работа с S3, ненужный loopback

Impala показывает наихудший результат среди тех движков, чьи результаты можно назвать адекватными: 54 секунды при чтении 12 GiB из S3. На то есть две причины.

Во-первых, Impala не может построить оптимальный план запроса:

  1. В процессе декорреляции оптимизатор создает LEFT SEMI JOIN, которые мешает "переезду" таблицы store вправо. Из-за этого не удается построить эффективный runtime-фильтр для одного из тяжелых сканов store_returns, и вышестоящему JOIN приходится обрабатывать 550 млн строк вместо 29 млн

  2. Оптимизатор выбирает неправильный порядок одного из JOIN. Таблица customer оказывается правым входом (build-сторона), из-за чего приходится строить hash-таблицу из 65 млн строк. В оптимальном плане hash-таблица была бы построена из другого входа (24 млн строк)

Impala выбирает неоптимальную стратегию JOIN
Impala выбирает неоптимальную стратегию JOIN

Проблема оптимизации запросов в Impala не является неожиданной. Это legacy-продукт, многие компоненты которого имеют устаревшую архитектуру. Так, оптимизатор Impala является единственным среди всех тестируемых продуктов, который использует монолитную архитектуру с AST-подобным внутренним представлением. Этот дизайн соответствует оптимизаторам 90-х годов и не позволяет реализовать многие актуальные техники планирования сложных запросов. Все современные оптимизаторы являются модульными и используют реляционную алгебру для описания внутреннего представления. Сотрудники Cloudera пытаются создать новый оптимизатор на основе Apache Calcite, но пока что их успехи достаточно скромные — новый оптимизатор не готов к эксплуатации.

Во-вторых, среди всех испытуемых движков Impala демонстрирует наименьшую скорость работы с S3. Объективно это проявляется в низкой скорости чтения данных: примерно 450 MiB/s, против 600-700 MiB/s у других движков.

Реальная причина такого поведения — предмет отдельного анализа. Это может быть связано с использованием устаревших библиотек Hadoop для работы с S3, неоптимальным уровнем параллелизма чтений (например, дробление TCP-пакетов из-за слишком большого количества конкурентных HTTP-запросов), иными факторами.

В-третьих, worker Impala без нужды отправляет пакеты на самого себя через loopback. См. IMPALA-8659. Аналогичная проблема присутствует в Trino, см. обсуждение ниже. Не исключено, что именно использование loopback снижает скорость работы с S3 из-за возникновения дополнительной конкуренции за ресурсы.

Trino: неоптимальный план, ненужный loopback

Запрос в Trino отрабатывает 47 секунд, зачитывая из S3 12 GiB. Это на порядок лучше Presto, но ощутимо хуже CedrusData, Doris и StarRocks. Виноваты две недоработки.

Во-первых, Trino без особой необходимости пропускает локальные данные через loopback интерфейс. Проблема аналогична Impala.

Все распределенные движки реализуют оператор Exchange (синоним — shuffle), который перераспределяет данные между узлами по мере прокачки строк через операторы. При выполнении Exchange адресатом части пересылаемых worker-ом пакетов будет является этот же worker. Все испытуемые движки, кроме Impala, Trino и выбывшего Presto, определяют такие пакеты и просто передают их между потоками. Trino и Impala же отправляют их в сетевой интерфейс. Операционные системы хорошо оптимизируют передачу данных через loopback, но накладные расходы остаются, и это существенно ухудшает результат.

Исправление этого недочета в Trino требует пару дней рабочего времени квалифицированного инженера, после чего целевой запрос начнет отрабатывать примерно за 35-40 секунд. В нашем форке Trino данная проблема отсутствует.

При выполнении Exchange Trino передает пакеты данных внутри одного узла через сетевой интерфейс
При выполнении Exchange Trino передает пакеты данных внутри одного узла через сетевой интерфейс

ВАЖНО! Мы наблюдаем этот эффект, потому что тестируем движки локально. В распределенном эксперименте его влияние будет существенно снижено.

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

Детальное описание проблемы для сильных духом

Планировщик Trino умеет делать pushdown агрегаций под Join. Это важная и полезная оптимизация. Теперь следите за руками:

  1. Trino разворачивает корреллированный подзапрос в LEFT JOIN. Это так называемая декорреляция — важнейшая оптимизация, которую делают все современные распределенные движки, что бы не выполнять кореллированные подзапросы через NESTED LOOP JOIN. См. фундаментальный paper по этой теме от "немцев" Unnesting Arbitrary Queries

  2. Далее Trino пытается пропушить агрегат внутрь LEFT JOIN. Но чтобы сохранить эквивалентность результатов, он добавляет CROSS JOIN c одной строкой, которая целиком состоит из значений NULL. Это немного взрывает мозг, но в этом есть смысл. Мы должны вернуть корректные значения агрегатов для тех групп, у которых все значения агрегируемых колонок оказались NULL. В случае LEFT JOIN такое может произойти, когда с правой стороны не оказалось подходящих записей. Но разные функции агрегации по-разному обрабатывают NULL. Например, SUM(NULL) == NULL, но COUNT(NULL) == 0. Так как Trino поддерживает UDAF (user-defined aggregation function), то оптимизатор в общем случае не знает, каков будет результат применения UDAF(NULL). Поэтому он добавляет CROSS JOIN с одной специальной строкой из NULL значений, из которой гарантированно можно вывести правильный агрегат

  3. На более поздних этапах добавленный компенсирующий CROSS JOIN (а если быть точным — специальная проекция над ним) не дает оптимизатору превратить LEFT JOIN в INNER JOIN, хотя в данном запросе такая трансформация возможна

  4. Из-за этого оптимизатор не может перенести таблицу store в правую часть плана через границу LEFT JOIN

  5. Из-за этого мы не можем применить runtime-фильтр из таблицы store для одной из таблиц store_returns, что существенно увеличивает количество данных внутри одного из пайплайнов.

Упущенная возможность применить runtime-фильтр в Trino
Упущенная возможность применить runtime-фильтр в Trino

Мы как профессиональные разработчики движков сталкиваемся с такими проблемами постоянно. Вы удивитесь, но решение данной проблемы — это буквально несколько десятков строк кода. Совместно с первой оптимизацией это могло бы дать Trino время выполнения порядка 30 секунд, и он бы максимально приблизился к CedrusData и Doris. Но как говорится, если бы у бабушки ...

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

StarRocks и Doris: меняем память на скорость

DuckDB, StarRocks и Doris читают в два раза меньше данных из S3, чем CedrusData, Impala и Trino. Значит, где-то срабатывает кэширование. Так как мы явно отключили дисковый кэш у тестируемых движков, остается один вариант — дедупликация CTE (common table expression).

Планы SQL-запросов могут иметь повторяющиеся части, которые либо следуют из синтаксиса запроса (CTE — common table expression), либо появляются неявно в процессе оптимизации. Дедупликация повторяющихся подзапросов может существенно ускорить выполнение запроса, за счет уменьшения общего количества вычислений и чтений.

Большинство популярных аналитических систем реализуют дедупликацию на основе идей Greenplum, описанных в пэпере Optimization of Common Table Expressions in MPP Database Systems. В этом подходе мы "нарезаем" план по границам CTE, и далее выполняем отдельные подзапросы в соответствии с их топологической сортировкой в рамках всего плана. По окончании выполнения отдельного CTE мы материализуем его результат. Без полной материалиации может возникнуть deadlock. После этого результат CTE может быть использован в других частях плана.

План Doris и StarRocks с материализацией CTE
План Doris и StarRocks с материализацией CTE

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

  • Trino не поддерживает материализацию CTE

  • CedrusData и Presto материализуют CTE в облачном сторедже. Материализация CTE по умолчанию выключена. Мы считаем, что это правильный подход для масштабной промышленной аналитики: сначала пусть заработает, потом ускорим

  • DuckDB материализует CTE в памяти. Материализация CTE по умолчанию включена. Это хорошее решение конкретно для DuckDB, потому что основной паттерн его использования — вычисления на локальной машине, к которой у текущего пользователя есть эксклюзивный доступ

  • StarRocks и Doris материализуют CTE в памяти, но могут также спилить на локалный диск (контроль потребления дискового пространства отсутствует) или в облачный сторедж. Материализация CTE по умолчанию включена

Автоматическое включение материализации CTE в памяти в многопользовательском аналитическом движке — это типичный пример подгона дефолтов системы под хорошие цифры:

  • Материализация может ухудшать качество плана. Так, ни Doris, ни StarRocks не умеют пробрасывать динамические фильтры через границы CTE. Как следствие, план выше неоптимален: таблица store_returns порождает 558 млн строк, после джоина и агрегации они превращаются в 442 млн строк, которые нужно хранить в памяти. Если бы был использован runtime-фильтр, в памяти пришлось бы хранить всего 23 млн строк

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

CedrusData: просто работает

Мы потратили много сил на это. План оптимален, памяти потребляем по-минимуму, никакие спорные оптимизации, вроде материализации CTE, не используем.

Ну и повезло немного. Современные оптимизаторы без доли "фарта" — никуда. Вечер в продакшн, эксплуатанты!

DuckDB: вне конкуренции при локальном исполнении

15.9 секунд при чтении из S3. 10 секунд при включении in-memory кэша для S3.

Ложка дегтя: DuckDB не может обработать некоторые запросы TPC-DS, которые читают соизмеримое количество данных из S3. Совладать со small data не так просто, как кажется. Мы покажем эти примеры в следующих статьях.

Эксперимент 2: отключаем материализацию CTE

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

Но мы хотим протестировать конкурентное выполнение запросов, и StarRocks c Doris просто не смогут продолжить соревнование, если мы не отключим данный функционал (из-за неработающих для CTE runtime-фильтров).

Проверим, как повлияет отключение материализации CTE на результаты (DuckDB и Presto исключены).

Время выполнения запроса при отключенной материализации CTE
Время выполнения запроса при отключенной материализации CTE

StarRocks и Doris: неоптимальный порядок Join

Doris стал медленнее в полтора раза. StarRocks потерял порядка 10%. В этом прогоне у обоих движков получается план, похожий на план Trino, где runtime-фильтры не могут быть полностью использованы, потому что таблица store не "уехала" вправо. Отсюда долгая работа отдельного оператора JOIN и увеличение длительности запроса.

Неоптимальный план Doris и StarRocks после отключения материализации CTE
Неоптимальный план Doris и StarRocks после отключения материализации CTE

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

А в StarRocks к выбору неоптимального плана приводят ... расширенные статистики Iceberg (NDV в Puffin)! Мы выяснили это экспериментально: если запустить запрос с параметром enable_iceberg_column_statistics = false, то оптимизатор выберет оптимальный план, аналогичный плану CedrusData, и время выполнения уменьшится с 36 до 24 секунд.

Ухудшение плана при увеличении количества доступных статистик свидетельствует о недостаточной зрелости оптимизатора. Не удивлюсь, если перемудрили с эвристическими формулами расчета кардинальностей в попытке заточить движок под конкретные TPC-DS запросы. Я тоже так делал раньше, пока жизнь не отучила.

На длинной дистанции опытные разработчики аналитических систем начинают склоняться к более консервативным стратегиям выполнения запросов, потому что бизнесу нужна сначала предсказуемость, а потом уже скорость. За это пользователи любят такие системы, как Oracle, Teradata, а так же семейство продуктов Presto/Trino: они придерживаются стратегии "сначала сделаем стабильно, потом ускорим". К этому же пониманию со временем приходят и инженеры, эксплуатирующие такие системы.

Эксперимент 3: конкурентная нагрузка

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

Целевой запрос читает до 12 GiB данных из S3. Коэффициент сжатия Parquet + Snappy — один к трем. Целевой датасет должен занимать порядка 36 GiB в памяти. Плюс накладные расходы на хэш-агрегаты и хэш-таблицы. Можем грубо прикинуть, что в худшем случае движкам потребуется порядка 70 GiB RAM на выполнение этого запроса. Но так как все испытуемые имеют pipelined модель выполнения, и не требуют одномоментной материализации сканов, то разумно ожидать потребления не более 40-50 GiB в пике. В норме — меньше.

На нашей машине 32 vCPU и 256 GiB памяти. Воркеру движка выделено 90% RAM, то есть 230 GiB. Ограничения на потребление памяти отдельным запросом отсутствуют.

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

Четыре конкурентных запроса

Смогут, но не все. Запускаем четыре одинаковых запроса TPC-DS Q1.

Продукт

Время работы изолированного запроса, сек

Среднее время запроса при одновременном запуске, сек

Количество успешно завершенных запросов

DuckDB

15.9

62.3

4/4

CedrusData

21.7

69.9

4/4

Trino

46.7

161.9

4/4

Doris, материализация CTE

21.1

188.2

4/4

Doris, без материализации CTE

31.3

141.4

4/4

StarRocks, материализация CTE

0/4

StarRocks, без материализации CTE

1/4

StarRocks, без материализации CTE, без детальных статистик Iceberg*

24.1

90.5

4/4

Impala

53.9

289.6

4/4

  • Результаты DuckDB приведены как дополнительный референс для проверки адекватности цифр

  • CedrusData и Trino демонстрируют "линейную масштабируемость latency" по мере роста нагрузки, и отсутствие патологической конкуренции за ресурсы

  • StarRocks c материализацией CTE просто не работает. Привет дефолтам

  • StarRocks без материализации CTE тоже не работает. Успешно завершился лишь один запрос из четырех. Это худший результат сред среди родственных движков с аналогичным неоптимальным планом (Impala и Doris)

  • Doris и Impala справились, но мы наблюдаем "отрицательную масштабируемость latency" — время работы запросов выросло непропорционально. Особенно для Doris cо включенной материализацией CTE. Это свидетельствует о том, что движки не умеют грамотно разделять нагрузку между конкуретными запросами, что приводит к избыточной конкуренции за ресурсы. Мы остановимся подробнее на этом вопросе далее

Для StarRocks мы провели дополнительный эксперимент, в котором отключили чтение NDV Iceberg. По сути мы вытягиваем StarRocks, но не от хорошей жизни: на следующем этапе мы потеряем всех, кроме Trino и CedrusData. Так что без тюнингованного StarRocks было бы совсем неинтересно. Для других выбывших движков мы не смогли быстро найти схожие хаки (уверен, они есть).

Среднее время выполнения запроса при одновременном выполнении четырех запросов
Среднее время выполнения запроса при одновременном выполнении четырех запросов

Восемь конкурентных запросов

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

Продукт

Время работы изолированного запроса, сек

Среднее время запроса при одновременном запуске, сек

Количество успешно завершенных запросов

CedrusData

21.7

115.1

8/8

Trino

46.7

293.0

8/8

Doris, материализация CTE

4/8

Doris, без материализации CTE

4/8

StarRocks, без материализации CTE, без детальных статистик Iceberg*

24.1

183.5

8/8

Impala

5/8

  • CedrusData и затюненный StarRocks продолжают "линейно масштабировать latency" и успешно выполняют все запросы

  • Trino начинает отставать. Сказывается неоптимальность плана (из-за неправильного порядка JOIN не работает важный runtime-фильтр), из-за чего начинает поддавливать GC

  • История Impala и Doris здесь заканчивается. Движки потребляют очень много памяти, запросы падают. Однако, хочется отметить, что падают именно запросы, а не узлы. Это уже хорошо.

На этом этапе мы можем увидеть новый интересный эффект — как распределяется время запросов?

Шедулеры Presto, Trino и CedrusData имеют общие корни и умеют квантовать процессорное время, приоритезируя более новые запросы и деприоритезируя старые. Изначально это было сделано из предположения, что:

  • Если запрос работает долго ("старый"), то это вероятно тяжелый ETL, который можно немного притормозить

  • Если запрос еще не успел потребить много ресурсов ("новый"), то это вероятно пользовательский ad-hoc, который нужно выполнить как можно быстрее.

Более подробно с мотивацией выбора этого алгоритма вы можете ознакомиться в оригинальном пэпере Presto (параграф IV.F).

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

Шедулер CedrusData эффективно квантует процессорное время между запросами
Шедулер CedrusData эффективно квантует процессорное время между запросами

А вот так выглядит распределение времени выполнения запросов в движках семейства Impala. Мы используем в качестве примера затюнингованный StarRocks, но Impala и Doris ведут себя аналогично.

Шедулер StarRocks бессистемно выполняет все запросы одновременно
Шедулер StarRocks бессистемно выполняет все запросы одновременно

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

Шестнадцать конкурентных запросов

К сожалению, Trino не смог преодолеть эту отметку: не хватает памяти из-за неоптимального плана. Без должного тюнинга настроек JVM на этом этапе вы поймаете падение узла. После тюнинга — узел вновь станет стабильным, но часть запросов будет падать, как и у семейства движков Impala.

Последние из могикан:

Среднее время выполнения запроса в зависимости от конкурентной нагрузки (меньше — лучше)
Среднее время выполнения запроса в зависимости от конкурентной нагрузки (меньше — лучше)

Просто картинка распределения времени работы запросов в CedrusData:

Распределение времени выполнения запросов в CedrusData
Распределение времени выполнения запросов в CedrusData

На данном этапе на руку CedrusData начинает играть наше новое ядро, написанное на Rust с использованием библиотек Tokio, Arrow и DataFusion — CedrusData Oxide. И как ни странно, дело вновь не в "Java тормозит". Конкретно в этом запросе Oxide выполняет только сканы и фильтры, а все тяжелые агрегаты и Join все еще считает Java. Но наше новое ядро грамотно отделяет IO от CPU операций, за счет чего мы избегаем блокировок потоков и можем независимо управлять уровнем нагрузки на сеть и CPU. Мы расскажем про Oxide в одной из следующих статей.

Выводы

Мы посмотрели под микроскопом на работу движков на примере одного запроса. Никаких финальных выводов относительно конкретного продукта на основе сухого бенчмарка сделать нельзя. Все они более-менее работают, кто-то лучше, кто-то хуже. Можно посидеть и вытянуть в победители любого из них (пожалуй, кроме Impala — там нужно переписывать оптимизатор). Тюнингом, фиксами, как угодно. Суть не в этом.

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

  • Ни у одного современного движка нет подавляющего преимущества. В мире просто не существует таких ноу-хау, которые могли бы дать одному продукту безусловное кратное ускорение относительно другого. Все тезисы вида "мы быстрее в 5-10-20-40 раз" — это очковтирательство. Команда, которая приоритезирует красивые слайды, всегда будет создавать посредственный нестабильный код, разваливающийся в продакшене

  • У всех продуктов есть большое количество огрехов, которые устранимы, но сильно роняют производительность в моменте. Это нормально. Буквально на одном запросе мы увидели, что у Impala слабый оптимизатор, StarRocks косячит со статистиками, Doris умирает под нагрузкой, а Trino гоняет ненужный трафик. Про CedrusData я напишу отдельный пост, где расскажу, как мы постоянно теряем производительность то тут, то там. Но потом находим — это не страшно

  • Избегайте устаревших движков — они устарели, потому что за ними нет команды, которую бы волновали ваши проблемы. Команда Presto работает на внутреннего потребителя — компанию Meta* (признана экстремистской организацией и запрещена в РФ). А Cloudera давно поставила крест на Impala — нет сообщества, нет документации. В обоих продуктах нет активной работы над актуальными техническими проблемами, поэтому они будут все больше отставать, и тянуть вас за собой

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

Удачной эксплуатации!

Комментарии (6)


  1. WaitEvent
    16.10.2025 16:18

    это не серьезно, mysql на таких объемах всех уделает. это же бигдата движки с упором на кластер. наверника на кластере расклад будет другим.


    1. yuriygavrilov
      16.10.2025 16:18

      Еще скажите, что это реклама Cedrus :) и забыли про Postgres или еще какой нибудь gpudb


    1. mentin
      16.10.2025 16:18

      Я бы так не упрощал. Тут представлен DuckDB который тоже для локальной машины. Главное отличие от MySql, эти работают с внешними данными, а не своим форматом как MySql. Нет индексов, статистик и прочего.


  1. EvgeniyRasyuk
    16.10.2025 16:18

    clickhouse нет в обзоре


  1. EvgeniyRasyuk
    16.10.2025 16:18

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


  1. Dm1tr1ch
    16.10.2025 16:18

    Спасибо автору за статью, есть несколько интересных моментов.

    StarRocks прекрасен как MPP система, которая работает на множестве серверов. Так же тут сравнивается только часть StarRocks, которая конкурирует с Trino, но совсем не учитывается свой движок хранения, который в комбинации со множеством серверов и использованием для DWH очень неплохи для своего круга задач.

    На счёт минусов работы с Iceberg, то StarRocks сейчас тратит множество усилий на улучшая в этом компоненте и в версии 4+ уже поправил часть проблем, что указаны в этой статье. Что говорит об очень быстрой реакции разработчиков на проблемы.

    Например:

    Optimized metadata file parsing for Iceberg statistics to avoid repetitive parsing. #59955

    Optimized COUNT/MIN/MAX queries against Iceberg metadata by efficiently skipping over data file scans, significantly improving aggregation query performance on large partitioned tables and reducing resource consumption. #60385