Stateless-дизайн: конкурентность и отсутствие fault tolerance по умолчанию
Этот урок завершает модуль про MPP-архитектуру, и он связывает воедино всё, что мы разобрали: shared-nothing, координатор, воркеры, discovery, протокол. Все эти решения объединяет один принцип — stateless-дизайн. Воркеры Trino практически не хранят состояния, и это не мелкая деталь, а фундаментальный выбор, который определяет два важнейших свойства Trino: его выдающуюся конкурентность и отсутствие fault tolerance по умолчанию.
Эти два свойства — две стороны одной медали. Понять их связь критически важно. Без этого fault tolerance кажется случайным недостатком («почему такой мощный движок просто падает при сбое узла?»), а на самом деле это осознанная плата за конкретное преимущество. Этот урок объясняет компромисс — и почему он для Trino правильный.
Что значит stateless
Начнём с термина. Система или компонент stateless (без состояния), если он не хранит постоянного состояния между операциями. Каждая операция самодостаточна; компонент не накапливает в себе ничего, что нужно было бы беречь и переносить.
Применительно к воркеру Trino это значит вот что. Воркер не владеет данными — данные живут во внешних источниках (вспомните разделение storage и compute из модуля 1). Воркер не хранит на диске ничего постоянного. Всё, что у воркера есть в каждый момент времени, — это промежуточные данные текущих запросов в его оперативной памяти. Запрос завершился — эти данные исчезли. Между запросами воркер «пуст»: он не несёт никакого уникального состояния, которое отличало бы его от другого воркера.
Сравним с СУБД. PostgreSQL — глубоко stateful система: она владеет данными на диске, хранит индексы, буферный кэш, состояние транзакций, write-ahead log. Узел PostgreSQL уникален — на нём лежат конкретные данные, и потерять его — значит потерять их. Воркер Trino — противоположность: терять в нём, кроме промежуточных данных текущих запросов, нечего.
PostgreSQL: страницы и heapСторона выгоды: высокая конкурентность
Из stateless-дизайна напрямую вытекает первое большое преимущество — способность кластера обслуживать сотни одновременных запросов от множества пользователей. Разберём, почему именно stateless это даёт.
Воркеры взаимозаменяемы. Раз воркеры не несут уникального состояния, они неотличимы друг от друга. Любую задачу можно отдать любому воркеру. Координатору не нужно искать «тот самый» воркер с нужными данными — нужных данных ни у кого нет, данные приходят из источника. Это даёт планировщику полную свободу раскладывать работу множества запросов по всему пулу воркеров максимально равномерно.
Работа дробится на мелкие части. MPP-модель режет каждый запрос на множество мелких задач. Когда в кластер приходят сотни запросов одновременно, все они превращаются в общее море мелких задач, и кластер раскладывает это море по воркерам и их ядрам. Запросы не ждут друг друга целиком — они перемешиваются на уровне мелких задач.
Узлы легко добавлять и убирать. Раз воркер stateless, ввод и вывод воркера — мгновенная операция (это мы видели в уроке про discovery). Под рост числа пользователей кластер просто расширяют воркерами, под спад — сужают.
Именно поэтому Trino так хорош для интерактивной аналитики из модуля 1: десятки аналитиков и множество BI-дашбордов могут долбить кластер запросами одновременно, и он это выдерживает. Это прямая заслуга stateless-дизайна.
Сторона платы: нет fault tolerance по умолчанию
Теперь обратная сторона той же медали. У того же самого stateless-дизайна есть цена: по умолчанию у Trino нет fault tolerance — отказоустойчивости.
Логика проста и неумолима. Воркер во время исполнения держит промежуточные данные запроса в своей оперативной памяти. Эти данные нигде не продублированы и никуда не сохранены — в этом и суть stateless. Что произойдёт, если воркер упадёт посреди запроса — машина выключилась, процесс умер, сеть пропала?
Промежуточные данные, которые были в памяти этого воркера, теряются безвозвратно. Часть работы запроса исчезла, и восстановить её неоткуда — копии нет. У координатора в этой ситуации нет хорошего выхода: продолжить запрос нельзя, кусок результата пропал. Поэтому по умолчанию координатор завершает весь запрос целиком с ошибкой. Один упавший воркер роняет весь запрос.
Сравните со Spark из модуля 1. Spark в классической пакетной модели материализует промежуточные результаты — сохраняет их, — и потому может повторить упавший кусок работы, не начиная всё заново. Это его встроенная отказоустойчивость. Но за неё Spark платит накладными расходами на материализацию. Trino делает противоположный выбор: не материализует, гонит данные потоком через память ради скорости и конкурентности — и потому по умолчанию не может пережить сбой узла.
Это самое частое разочарование новичков: «Trino — мощный распределённый движок, почему он просто падает, если один воркер умер?». Ответ: это не баг и не недоработка, а прямое следствие stateless-дизайна, который и даёт Trino его скорость и конкурентность. Отказоустойчивость и отсутствие материализации — взаимоисключающие по умолчанию; Trino по умолчанию выбрал второе.
Почему компромисс для Trino правильный
Может показаться, что отсутствие fault tolerance — это слабость. Но для основного назначения Trino выбор правильный, и вот рассуждение.
Trino создан в первую очередь для интерактивных запросов — коротких, на секунды. Сбой воркера — событие сравнительно редкое. Если короткий интерактивный запрос изредка падает из-за сбоя узла, цена невелика: его просто перезапускают, и через секунды есть результат. Платить же за страховку от этого редкого события постоянными накладными расходами на материализацию каждого промежуточного результата — невыгодно: это замедлило бы вообще все запросы. Для интерактивной нагрузки «быстро и иногда перезапустить» лучше, чем «всегда чуть медленнее, но никогда не перезапускать».
Картина меняется для длинных запросов. Если запрос идёт не секунды, а часы, вероятность словить сбой узла за время исполнения становится заметной, а потерять часы работы — уже дорого. Для таких случаев Trino предлагает fault-tolerant execution (FTE) — отдельный, опциональный режим. В нём Trino начинает спулить промежуточные данные обмена (сохранять их во внешнее хранилище), и тогда при сбое воркера потерянную часть можно переиграть, не роняя весь запрос. FTE — это сознательное переключение компромисса: вы доплачиваете накладными расходами и получаете отказоустойчивость. По умолчанию FTE выключен; включают его осознанно под длинную batch-нагрузку. Этому режиму посвящён отдельный модуль курса.
| Аспект | Trino по умолчанию | Trino с FTE |
|---|---|---|
| Промежуточные данные | Только в памяти воркеров | Спулятся во внешнее хранилище |
| Сбой воркера в запросе | Весь запрос падает | Потерянную часть переигрывают |
| Накладные расходы | Минимальные | Выше (спулинг обмена) |
| Для чего подходит | Интерактивные короткие запросы | Длинные batch-запросы |
Запомните формулу этого модуля. Stateless-дизайн -> воркеры взаимозаменяемы и работа дробится на мелкие части -> высокая конкурентность (сотни запросов). Та же stateless-природа -> промежуточные данные только в памяти -> сбой воркера роняет запрос -> нет fault tolerance по умолчанию. Одно решение, два следствия. FTE — опциональный способ доплатить за отказоустойчивость, когда она нужна.
Попробуй сам
Если у вас поднят многоузловой кластер из заданий к прошлым урокам, проведите наглядный эксперимент. Запустите достаточно тяжёлый запрос к tpch.sf100 (большой масштаб, чтобы запрос шёл хотя бы несколько секунд) и, пока он исполняется, в другом терминале остановите один из контейнеров-воркеров командой docker stop. Посмотрите, что произойдёт с запросом в CLI: он должен завершиться с ошибкой. Это и есть отсутствие fault tolerance по умолчанию, увиденное вживую.
Затем подумайте над двумя сценариями и запишите ответ для каждого: (1) кластер обслуживает 200 аналитиков с короткими дашбордными запросами; (2) кластер по ночам гоняет один тяжёлый четырёхчасовой batch-запрос. Для какого из сценариев поведение по умолчанию приемлемо, а для какого стоит включить fault-tolerant execution, и почему? Обоснуйте через компромисс «накладные расходы против цены потери работы».