Retry policies: NONE, QUERY, TASK — когда какой
В прошлом уроке мы установили идею FTE: материализовать промежуток, чтобы пережить сбой воркера. Но «пережить сбой» можно на разном уровне — повторить запрос целиком или повторить только упавшую задачу. За этот выбор отвечает одно свойство — retry-policy. У него три значения: NONE, QUERY, TASK. Они образуют шкалу «нет отказоустойчивости -> грубая -> тонкая», и каждое следующее значение мощнее, но требовательнее.
Разберём все три: что именно ретраит каждое, какие у него требования и накладные расходы, и по какому профилю нагрузки выбирать.
retry-policy=NONE — дефолт без отказоустойчивости
retry-policy=NONE — значение по умолчанию. Это классический stateless-Trino из прошлого урока: pipelined-исполнение, нет материализации промежутка, сбой любого воркера роняет весь запрос, восстановления нет.
# etc/config.properties — дефолт, можно не указывать
retry-policy=NONE
NONE — правильный выбор для интерактивных кластеров: дашборды, ad-hoc аналитика, короткие запросы. Запрос длится секунды, шанс наткнуться на сбой мал, а отсутствие накладных расходов FTE означает максимальную скорость. Платой за надёжность здесь была бы материализация промежутка на каждом запросе — для коротких запросов это невыгодный размен.
retry-policy=QUERY — ретрай всего запроса целиком
retry-policy=QUERY — первый уровень отказоустойчивости. Если во время исполнения происходит инфраструктурный сбой, Trino повторяет весь запрос целиком — заново, с самого начала.
# etc/config.properties
retry-policy=QUERY
Звучит грубо — «упало, начали с нуля». Но при QUERY ретрай прозрачен для клиента. Без FTE упавший запрос — это ошибка в лицо пользователю. С retry-policy=QUERY пользователь о сбое и ретрае может вообще не узнать: координатор сам перезапускает запрос, а клиент просто получает результат чуть позже.
Чтобы ретрай был прозрачным, координатор не должен отдать клиенту частичный результат, который потом окажется неверным после перезапуска. Поэтому координатор буферизует результат запроса, прежде чем начать его отдавать. Если запрос упал до того, как результат начал уходить клиенту, — буфер сбрасывается, запрос ретраится, клиент ничего не заметил. Размер этого буфера задаёт свойство exchange.deduplication-buffer-size, по умолчанию 32MB.
У retry-policy=QUERY есть естественное ограничение. Повторять с нуля дёшево для коротких запросов и дорого для длинных. Если запрос идёт 5 секунд — перезапуск стоит ещё 5 секунд, мелочь. Если запрос идёт 3 часа и падает на 2:59 — QUERY начнёт все 3 часа заново. Поэтому QUERY хорош для кластеров с множеством мелких запросов: каждый отдельный ретрай дёшев, а отказоустойчивость есть.
retry-policy=TASK — ретрай отдельной задачи
retry-policy=TASK — тонкий уровень отказоустойчивости. Вместо перезапуска всего запроса Trino ретраит только упавшую задачу (task) — ту единицу работы, что исполнялась на сбойном воркере.
Вспомни таксономию из модуля про распределённое исполнение: запрос дробится на стадии, стадия — на задачи, задача исполняется на конкретном воркере. При retry-policy=TASK, когда воркер падает, переисполняется не весь запрос, а лишь его задачи. Их назначают на другие, живые воркеры, и общий прогресс запроса сохраняется.
Именно ради этого нужна материализация промежутка из прошлого урока. Чтобы переисполнить одну задачу на другом воркере, её входные данные должны быть доступны — а значит, промежуточные результаты предыдущих стадий обязаны быть спулены в надёжное хранилище.
Spark: устойчивость через RDD-lineage и материализацию shuffle Поэтому:
retry-policy=TASK ТРЕБУЕТ настроенного exchange manager. Exchange manager — компонент, который спулит промежуточные данные обмена во внешнее хранилище (filesystem, S3, GCS); ему посвящён следующий урок. Без exchange manager retry-policy=TASK работать не может: переисполнять задачу попросту неоткуда — её входные данные негде взять. retry-policy=QUERY и NONE exchange manager не требуют.
# etc/config.properties
retry-policy=TASK
# плюс обязательно настроенный exchange-manager.properties — см. следующий урок
retry-policy=TASK рекомендуется для больших batch-запросов — долгих ETL, перестроек таблиц, тяжёлых отчётов. Здесь его преимущество максимально: четырёхчасовой запрос при сбое воркера переисполнит лишь несколько задач, а не все 4 часа.
Цена за тонкость — повышенная латентность для коротких запросов. Материализация промежутка через exchange manager стоит I/O всегда, даже когда никакой сбой не случился. Для долгого batch это незаметная добавка; для запроса на полсекунды спулинг промежутка может сам по себе превысить полезное время. Поэтому TASK — для batch, а не для интерактива.
Сводная таблица: три политики
| Свойство | NONE | QUERY | TASK |
|---|---|---|---|
| Что ретраит при сбое | ничего, запрос падает | весь запрос с нуля | только упавшие задачи |
| Требует exchange manager | нет | нет | да |
| Накладные расходы | нулевые | буфер дедупликации (32MB) | спулинг промежутка всегда |
| Цена сбоя | потеря всего запроса | повтор всего запроса | переисполнение нескольких задач |
| Оптимален для | интерактив, дашборды | кластеры с множеством мелких запросов | большие долгие batch-запросы |
| Латентность коротких запросов | минимальная | минимальная | повышенная |
Параметры ретраев
Сколько раз и с какими паузами повторять — задаётся отдельными свойствами. Дефолтные значения:
| Свойство | Дефолт | Смысл |
|---|---|---|
query-retry-attempts | 4 | Сколько раз повторять весь запрос (для QUERY) |
task-retry-attempts-per-task | 4 | Сколько раз повторять одну задачу (для TASK) |
retry-initial-delay | 10s | Пауза перед первым ретраем |
retry-max-delay | 1m | Максимальная пауза между ретраями |
retry-delay-scale-factor | 2.0 | Множитель роста паузы (экспоненциальный backoff) |
Паузы растут по схеме экспоненциального backoff: первая пауза retry-initial-delay (10s), каждая следующая умножается на retry-delay-scale-factor (2.0) — 10s, 20s, 40s, — пока не упрётся в retry-max-delay (1m). Backoff не даёт ретраям молотить кластер вплотную: если сбой вызван временной перегрузкой, пауза даёт системе шанс прийти в себя, прежде чем повторить.
Лимит попыток (query-retry-attempts / task-retry-attempts-per-task) защищает от бесконечного цикла. Если запрос или задача упали 4 раза подряд, сбой почти наверняка не случайный — продолжать ретраить бессмысленно, запрос завершается ошибкой.
Точные имена и дефолты FTE-свойств между релизами Trino меняются — список в admin/fault-tolerant-execution для версии 481 не обязан совпадать со старыми статьями. Перед настройкой продакшен-кластера сверяй имена и значения со страницей FTE той версии, что разворачиваешь.
Как выбрать политику
Алгоритм выбора простой и опирается на профиль нагрузки кластера:
- Кластер интерактивный — дашборды, ad-hoc, короткие запросы? ->
NONE. Накладные расходы FTE не окупятся, перезапуск редкого упавшего запроса дёшев. - Много мелких и средних запросов, нужна отказоустойчивость без сложной инфраструктуры? ->
QUERY. Прозрачный ретрай, не требует exchange manager, повтор короткого запроса дёшев. - Большие долгие batch-запросы — ETL, перестройки, тяжёлые отчёты? ->
TASK. Переисполнение задач вместо всего запроса критично, повышенная латентность коротких запросов не важна — их тут и нет. Готовь exchange manager.
Главный водораздел — длина запросов. Короткие живут с NONE или QUERY; длинные batch требуют TASK. А поскольку TASK добавляет латентность коротким запросам, смешивать интерактив и batch на одном кластере с TASK — плохая идея; об этом — в уроке про эксплуатацию.
Попробуй сам
Сравни поведение NONE и QUERY на тестовом Trino.
- Подними кластер с координатором и 2-3 воркерами. Оставь дефолтный
retry-policy=NONE. Запусти минутный join поtpch.sf100, во время исполнения убей один воркер (docker kill) — запрос упадёт с ошибкой. - Теперь задай в
etc/config.propertiesкоординатораretry-policy=QUERY, перезапусти координатор. Повтори: запусти тот же запрос, убей воркер во время исполнения. Сравни поведение — запрос должен пережить сбой и завершиться успешно (если воркеров осталось достаточно). - Подумай: почему
retry-policy=TASKты не можешь включить тем же одним свойством, в отличие отQUERY? Чего ему не хватает и какой урок это закрывает?
Цель — увидеть разницу между «запрос упал» (NONE) и «запрос пережил сбой прозрачно для клиента» (QUERY), и понять, что TASK требует дополнительной инфраструктуры.