Task sizing и adaptive planning при FTE
В режиме FTE с retry-policy=TASK промежуточные данные не растворяются в потоке, а материализуются через exchange manager. У этого есть неожиданный побочный эффект: материализованный промежуток можно измерить. А зная фактический размер данных, FTE делает то, чего классический pipelined-Trino не может, — подгоняет размер задач под реальность и переоптимизирует план уже в процессе исполнения.
Этот урок — про две способности, которые FTE получает «бесплатно» из материализации промежутка: task sizing (подбор размера задач) и adaptive planning (адаптивное перепланирование).
Откуда у FTE точные числа
Сначала — ключевое наблюдение, без которого ни task sizing, ни adaptive planning не имеют смысла.
В классическом pipelined-Trino (retry-policy=NONE) данные текут конвейером и нигде не задерживаются. Поэтому в момент планирования у Trino нет фактических размеров промежутка — есть лишь оценки cost-based optimizer по статистике таблиц. А оценки ошибаются: реальная кардинальность join, реальный объём после фильтра могут отличаться от прогноза в разы.
В FTE с retry-policy=TASK всё иначе. Стадия-продьюсер сначала полностью материализует свой вывод в спул через exchange manager, и только потом стадия-консьюмер начинает читать. К моменту старта консьюмера exchange manager точно знает объём промежутка — он лежит в хранилище, его размер измерен, а не угадан.
Факт вместо догадки — вот фундамент обеих способностей этого урока. FTE превращает «надеемся, что данных столько» в «знаем, что данных столько».
Task sizing: подбор размера задач
Task sizing — подбор количества и размера задач под фактический объём данных стадии.
Проблема, которую он решает: фиксированное число задач почти всегда неоптимально. Стадия фиксирована на 100 задач, а данных оказалось мало — 100 задач это overhead планирования и обмена ради крох работы каждой. Данных оказалось много — 100 задач это слишком крупные задачи: каждая жуёт долго, а сбой такой задачи дорого переисполнять.
FTE с retry-policy=TASK решает это так. Зная измеренный объём промежутка, движок назначает столько задач, сколько нужно под этот объём, и каждой задаче даёт примерно целевую порцию работы. Мало данных — мало задач без лишнего overhead. Много данных — больше задач, каждая разумного размера.
Поведением task sizing управляют свойства FTE. Ориентировочные значения по умолчанию:
| Свойство | Дефолт | Смысл |
|---|---|---|
fault-tolerant-execution-standard-split-size | 64MB | Целевой объём данных на один split при FTE |
fault-tolerant-execution-max-task-split-count | 2048 | Максимум splits на одну задачу |
fault-tolerant-execution-task-memory | 5GB | Ориентир памяти на задачу при FTE |
# etc/config.properties — ориентиры task sizing при FTE
fault-tolerant-execution-standard-split-size=64MB
fault-tolerant-execution-max-task-split-count=2048
fault-tolerant-execution-task-memory=5GB
Точные имена и дефолты свойств task sizing меняются между релизами Trino — сверяй их со страницей admin/fault-tolerant-execution для версии 481, а не со старыми гайдами. Идея же стабильна: FTE дробит работу на задачи разумного размера, опираясь на измеренный объём, а не на фиксированное число.
Почему разумный размер задачи так важен именно в FTE? Из-за retry-policy=TASK. Задача — это единица переисполнения при сбое. Слишком большая задача — дорогой ретрай: упала почти доделанная огромная задача, и весь её объём придётся пересчитывать. Слишком мелкая — overhead планирования и спулинга съедает пользу. Task sizing держит задачи в «золотой середине»: достаточно мелкие, чтобы ретрай был дёшев, достаточно крупные, чтобы накладные расходы не доминировали.
Adaptive planning: перепланирование в рантайме
Вторая способность — adaptive planning, адаптивное перепланирование. Если task sizing подгоняет размер задач, то adaptive planning меняет сам план запроса уже в процессе исполнения.
Классический Trino строит распределённый план один раз, до старта, на оценках CBO. Дальше план зафиксирован. Если CBO ошибся в кардинальности — план остаётся плохим до конца запроса.
В FTE работает компонент AdaptivePlanner. Поскольку промежуток между стадиями материализуется и измеряется, после завершения стадии движок видит фактический объём её вывода. Если факт сильно расходится с тем, на чём строился план, AdaptivePlanner переоптимизирует ещё не исполненную часть плана — с учётом реальных чисел, а не первоначальных догадок.
Самый показательный пример — выбор типа join. Из модуля про CBO: broadcast join (build-сторона целиком копируется на каждую ноду) выгоден, когда build-сторона маленькая; partitioned join (обе таблицы перераспределяются по хэшу) нужен, когда она большая. CBO выбирает тип join по оценке размера build-стороны. Если оценка промахнулась — план застрял с неверным типом.
AdaptivePlanner это чинит. Build-сторона материализована — её фактический размер известен. Оказалось, что CBO ждал маленькую build-сторону и выбрал broadcast, а реально она огромная? AdaptivePlanner на лету переключит ещё не исполненный join на partitioned, пока неверный broadcast не положил воркеры по памяти.
Spark AQE: адаптивный query executionПочему adaptive planning доступен именно в FTE
Логичный вопрос: почему AdaptivePlanner работает при FTE, а не в классическом режиме? Ведь идея «исправлять план по факту» полезна всегда.
Ответ — снова в материализации. Чтобы переоптимизировать план по фактическим числам, эти числа нужно иметь. В pipelined-режиме промежуток не материализуется — он течёт мимо, его никто целиком не видит и не измеряет. Фактического размера вывода стадии в классическом Trino попросту не существует как наблюдаемой величины до самого конца запроса. Перепланировать не на чем.
FTE с retry-policy=TASK материализует промежуток ради отказоустойчивости — и этот же материализованный промежуток дарит точные числа. Adaptive planning — это, по сути, бонус: возможность, которая появляется как побочный продукт того, что промежуток и так уже спулен и измерен.
Запомни связку: материализация промежутка в FTE служит сразу трём целям. Первая — отказоустойчивость (упавшую задачу есть откуда переисполнить). Вторая — task sizing (число задач под измеренный объём). Третья — adaptive planning (коррекция плана по фактическим числам). Один архитектурный приём — спулинг промежутка через exchange manager — оплачивает все три способности разом.
Цена и где это уместно
Task sizing и adaptive planning — не бесплатны. Они существуют поверх материализации промежутка, а материализация добавляет латентность: стадия-консьюмер ждёт, пока продьюсер полностью спулится, вместо того чтобы начать на первых же строках. Для коротких интерактивных запросов это плохой размен — там и retry-policy=NONE достаточно.
Зато для долгих batch-запросов выигрыш существенный. Именно у тяжёлых многостадийных запросов оценки CBO ошибаются чаще всего (ошибка кардинальности накапливается через стадии), и именно там цена плохого плана максимальна. FTE с task sizing и adaptive planning превращает «один раз ошиблись в плане — мучаемся весь запрос» в «ошиблись в прогнозе — исправились по факту на следующей стадии».
Попробуй сам
Понаблюдай адаптивность FTE на тестовом Trino с настроенным exchange manager.
- Используй кластер из прошлого урока — с exchange manager на S3/MinIO и
retry-policy=TASK. - Возьми многостадийный запрос с join таблиц, по которым нет собранной статистики (создай таблицы и не запускай
ANALYZE) — так оценки CBO заведомо неточны. Запусти его и снимиEXPLAIN ANALYZE. - Запусти тот же запрос с
retry-policy=NONEи сretry-policy=TASK. В Web UI и в выводеEXPLAIN ANALYZEсравни число задач по стадиям и типы join. Обрати внимание, отличается ли распределение работы между задачами. - Поразмышляй: почему именно отсутствие статистики делает выигрыш adaptive planning особенно заметным, и почему для короткого запроса по
tpch.tinyразница была бы несущественной.
Цель — увидеть, что FTE не просто «ретраит задачи», а ещё и умнее распределяет работу и правит план по фактическим числам.