Temporal: durable workflows вместо самодельных очередей

Что такое Temporal, какие задачи он решает, чем отличается от очередей и saga-оркестраторов и когда стоит его внедрять, а когда достаточно простого решения

В каждом достаточно сложном backend рано или поздно появляется код, который должен надёжно выполнить последовательность шагов: вызвать внешний API, дождаться ответа, обработать результат, при ошибке — повторить или компенсировать. Обычно это реализуется через очереди, cron-задачи, таблицы состояний и ручную логику повторов.

Temporal — это платформа для durable workflows. Она берёт на себя надёжность выполнения: если процесс упал на середине, Temporal восстановит его состояние через replay истории событий и продолжит с логически нужной точки — не с начала и не с последнего checkpoint. На уровне кода это выглядит так, будто выполнение продолжилось с той же строки.

Звучит как магия. На практике — это серьёзный инфраструктурный компонент со своей кривой обучения и операционной стоимостью. Эта статья разберёт, когда Temporal действительно оправдан, а когда проще обойтись без него.

В статье

Проблема: почему самодельные workflow ломаются

Типичная эволюция «просто задачи в очереди»:

  1. Простой воркер: взял задачу, обработал, удалил.
  2. Появились retry — при ошибке задача возвращается в очередь.
  3. Появились зависимые шаги — результат одной задачи нужен для следующей. Добавляется таблица состояний.
  4. Появились таймауты и дедлайны — задача должна выполниться за N минут, иначе эскалация.
  5. Появились компенсации — если шаг 3 упал, нужно откатить шаги 1 и 2.
  6. Появились длинные процессы — workflow может длиться дни или недели (согласование, ожидание внешнего события).

К пункту 4–5 команда обычно уже поддерживает самодельный state machine на таблице workflow_state с полями status, retry_count, last_error, step, data. Этот код хрупкий, плохо тестируется и тяжело меняется.

Что такое Temporal и как он работает

Temporal — это open-source платформа (исходники на GitHub, документация; официальные SDK для Go, TypeScript, Java, Python, .NET, PHP, Ruby). Архитектура:

  • Temporal Server — хранит состояние workflow, управляет таймерами, ретраями и историей выполнения. Требует persistence-store для состояния (PostgreSQL, MySQL или Cassandra) и отдельный visibility-store для поиска и фильтрации workflow; в типовом self-hosted-деплое (в т.ч. в официальном docker-compose) это PostgreSQL + Elasticsearch.
  • Worker — ваш код, подключается к серверу. Выполняет workflow-функции и activity-функции.
  • Workflow — детерминированная функция, описывающая логику процесса. Не делает I/O напрямую.
  • Activity — функция с побочными эффектами (HTTP-запрос, запись в БД, отправка уведомления). Вызывается из workflow.

Ключевой принцип: workflow-код выглядит как обычная последовательная программа, но Temporal гарантирует его durable execution.

Workflow и Activity: разделение логики и побочных эффектов

Workflow:

func ProvisionResource(ctx, request):
    result = CheckAvailability(ctx, request.resource)
    if not result.available:
        return Error("resource unavailable")

    reservation = ReserveResource(ctx, request)

    Sleep(ctx, 24h)                     // ждём подтверждения сутки

    confirmed = WaitForSignal(ctx, "confirmation")
    if not confirmed:
        CancelReservation(ctx, reservation)
        return Error("not confirmed")

    AllocateResource(ctx, reservation)
    return Success()

Код выше — схематичный псевдокод (синтаксис условный, 24h и вызовы упрощены). Но идея за ним реальна: в Temporal Go SDK workflow пишется как обычная функция с последовательным потоком управления — if/else, циклы, вызовы activity через workflow.ExecuteActivity, таймеры через workflow.Sleep, ожидание сигналов, — а не как самодельный state machine. Если worker перезапустится после workflow.Sleep(24h), Temporal восстановит выполнение с той же точки.

Activity — обычные функции с side effects. Temporal автоматически управляет retry, timeout и heartbeat для каждой activity.

Durability: как Temporal восстанавливает состояние

Temporal записывает в базу каждое событие выполнения workflow: старт, вызов activity, результат activity, таймер, сигнал. При перезапуске worker’а Temporal replays историю событий, и workflow-функция «проигрывается» до текущей точки без повторного вызова activity (результаты берутся из истории).

Ограничение: workflow-код должен быть детерминированным. Нельзя использовать time.Now(), rand() или прямой I/O в workflow. Для этого есть activity.

Из детерминизма вытекают две неочевидные эксплуатационные вещи, о которых стоит знать заранее:

  • Версионирование workflow. Нельзя просто поменять код workflow, у которого есть запущенные экземпляры: при replay новой логики на старой истории Temporal обнаружит несоответствие (non-determinism error). Изменения вводятся через patching/versioning API — это отдельная дисциплина, которую легко недооценить.
  • Continue-As-New. История событий не бесконечна (есть лимит на размер). Долгие и циклические workflow периодически «перезапускают» себя с чистой историей через Continue-As-New, перенося только нужное состояние. Без этого очень длинный процесс упрётся в лимит истории.

Что ещё важно: task queues, signals, child workflows, тестирование

За «линейным кодом с await» стоит несколько понятий, без которых Temporal в проде не собрать:

  • Task queues и маршрутизация воркеров. Workflow и activity привязаны к именованной task queue; воркеры слушают конкретные очереди. Это даёт роутинг (например, GPU-activity — на отдельный пул воркеров), изоляцию и горизонтальное масштабирование — воркеров можно добавлять независимо.
  • Signals и Queries. Signal — асинхронное внешнее событие, доставленное в бегущий workflow (в примере ниже — «подтверждение оператора»); Query — синхронное чтение текущего состояния workflow без изменения истории. Это и есть механизм «человек в цикле» и внешнего наблюдения.
  • Child workflows. Workflow может запускать дочерние — для декомпозиции и независимого версионирования крупных процессов.
  • Activity: таймауты, ретраи, heartbeat. У каждой activity — StartToClose/ScheduleToClose таймауты, политика ретраев (backoff, максимум попыток) и heartbeat для долгих задач (чтобы отличить «зависла» от «ещё работает»). Идемпотентность activity — на вашей стороне (ретрай может вызвать её повторно) — мостик к идемпотентности и outbox.
  • Тестирование. SDK дают test-framework с «промоткой времени»: Sleep(24h) в тесте не ждёт сутки, а перематывается мгновенно; activity мокаются. Это выгодно отличает Temporal от самодельных state machine, которые тестировать больно (мостик к тестированию распределённых системготовится, с 10 сентября).

Workflow-код на реальном SDK (Go) — не псевдокод, а рабочая функция: workflow.ExecuteActivity(ctx, ...).Get(ctx, &res), workflow.Sleep(ctx, 24*time.Hour), workflow.GetSignalChannel(ctx, "confirmation"). Полный пример — в demo-стенде.

Лунная база: координация аварийного протокола через workflow

Аварийный протокол лунной базы — хороший пример для Temporal:

  1. Получить сигнал о разгерметизации.
  2. Оценить критичность (activity: запрос к подсистеме мониторинга).
  3. Если критично — запустить параллельно:
    • зарезервировать аварийный кислород;
    • переключить энергоснабжение;
    • отправить робота.
  4. Дождаться подтверждения всех трёх шагов (с таймаутом 5 минут).
  5. Если таймаут — эскалация на оператора (activity: уведомление + ожидание сигнала).
  6. Если оператор подтвердил — продолжить. Если нет — компенсация.

На самодельной очереди это 200+ строк state machine. В Temporal — линейный код с await и parallel.

Temporal vs очереди, Temporal vs saga

Очереди (RabbitMQ, Kafka) Saga (самодельная) Temporal
Гарантия выполнения retry, DLQ ручной state machine автоматическая durability
Длинные процессы таймеры в коде поле status в БД Sleep(), WaitForSignal()
Компенсации ручная логика ручная логика try/catch в workflow
Параллелизм отдельные задачи сложная координация параллельный fan-out/fan-in
Наблюдаемость логи + метрики query по таблице Temporal UI, history API
Сложность низкая растёт экспоненциально средняя начальная, стабильная

Temporal не заменяет очереди задачготовится, с 17 сентября для простой обработки сообщений — для fire-and-forget джоб с ретраями и DLQ они проще. Он заменяет самодельные workflow-движки: то, что иначе пишется как saga-оркестратор на таблице состояний. Где саму механику «события без распределённой транзакции» проще держать на outbox — Temporal не нужен; где нужна долгая координация с компенсациями и человеком в цикле — нужен. Как это соотносится с остальными событийными паттернами — в матрице решенийготовится, с 8 августа.

Temporal не одинок в своей нише. Он вырос из Uber Cadence (тот по-прежнему развивается), а схожие идеи durable execution предлагают managed-решения — AWS Step Functions, Azure Durable Functions — и open-source Netflix Conductor и Restate. Выбор между ними — это в первую очередь выбор между «свой stateful-компонент с полным контролем» (Temporal/Cadence) и «managed-сервис без своей инфраструктуры, но с привязкой к облаку» (Step Functions, Durable Functions). Есть и managed-Temporal — Temporal Cloud, если свой сервер держать не хочется.

Ландшафт durable execution: аналоги и когда что

Temporal — не единственный способ получить durable execution, и выбор между аналогами — отдельное решение. Сравнить их проще по трём осям: как описывается workflow (обычный код vs конфиг/DSL), хостинг (свой сервер vs managed) и привязка к облаку.

Продукт Модель workflow Хостинг Lock-in Языки Ниша
Temporal code-first (обычный код + await) self-hosted / Cloud нет (OSS) Go, Java, TS, Python, .NET, PHP, Ruby дефолт «своего durable-движка»
Cadence code-first self-hosted нет Go, Java предок Temporal (Uber); экосистема меньше
AWS Step Functions конфиг (ASL — JSON-DSL) + визуал managed (AWS) сильный (AWS) язык-агностично (через Lambda) процессы внутри экосистемы AWS
Azure Durable Functions code-first (orchestrator functions) managed (Azure) сильный (Azure) C#, JS, Python, … durable поверх Azure Functions
Netflix Conductor JSON-DSL workflow self-hosted (OSS) нет язык-агностично (task workers) микросервисная оркестрация по DSL
Restate code-first + durable RPC self-hosted / Cloud нет TS, Java/Kotlin, Go, Python новый, лёгкий рантайм

Как выбирать:

  • Свой durable-движок, много языков, полный контроль, зрелость → Temporal. Cadence — если уже на нём; догонять на новых проектах смысла мало (Temporal активнее).
  • Уже глубоко в AWS, процессы простые/визуальные, не хочется своей инфраструктуры → Step Functions (но ASL — это конфиг, не код, и lock-in). Аналогично Azure → Durable Functions (там как раз code-first, но привязка к Azure Functions).
  • Оркестрация микросервисов по декларативному DSL, а не «код как workflow» → Conductor.
  • Хочется durable execution без тяжёлого сервера, с durable-RPC и лёгким рантаймом → присмотреться к Restate (моложе, но растёт).
  • Общее правило: managed снимает операционную нагрузку ценой lock-in; self-hosted (Temporal/Cadence/Conductor/Restate) даёт контроль и переносимость ценой своего stateful-компонента (см. чеклист ниже).

Когда Temporal стоит внедрять

  • Многошаговые процессы с ретраями, таймаутами и компенсациями;
  • длинные workflow — часы, дни, недели (согласования, ожидание внешних событий);
  • координация между сервисами — вместо хрупких цепочек событий;
  • человек в цикле — workflow ждёт решения оператора;
  • критичная надёжность — процесс не должен потеряться при перезапуске;
  • самодельный state machine уже стал проблемой — слишком сложный, хрупкий, нетестируемый.

Где Temporal избыточен

  • Простая обработка очереди — fire-and-forget задачи, одношаговые воркеры;
  • процесс в одном сервисе и одной транзакции — не нужен внешний координатор;
  • нет инфраструктурной готовности — Temporal Server требует базу данных, мониторинг и операционной поддержки;
  • маленькая команда — кривая обучения и debugging Temporal-workflow не бесплатны;
  • latency-критичные процессы — overhead Temporal (запись истории) не подходит для задач с требованием <10ms.

Checklist: готовы ли вы к Temporal

  1. Есть ли в системе многошаговые процессы, которые сейчас реализованы через таблицы состояний или цепочки очередей?
  2. Сколько строк кода занимает текущий state machine? Если >300 — Temporal, вероятно, упростит.
  3. Есть ли процессы длительностью больше минуты (ожидание ответа, согласование)?
  4. Готова ли инфраструктура к ещё одному stateful-компоненту (Temporal Server + persistence/visibility store + мониторинг)?
  5. Есть ли ресурс на обучение команды (детерминизм workflow, тестирование, debugging)?
  6. Готовы ли вы к версионированию workflow (patching) при изменении логики уже запущенных процессов?
  7. Можно ли начать с одного workflow и оценить ROI, прежде чем мигрировать всё?

Начинайте с одного конкретного болезненного процесса. Не переписывайте всё сразу.

Демо и версии

  • Demo: digital-cookbook/architecture/temporal — Temporal dev-сервер (server start-dev + Web UI) и суб-стенд 00-paradigm на Go: workflow с Sleep, Signal и компенсацией; убить воркер на середине → workflow продолжается с той же точки, activity не переисполняются (durable execution наглядно). Остальные суб-стенды — полная топология с PostgreSQL+Elasticsearch, детерминизм, activities, версионирование через patching, тестирование с промоткой времени, воркеры на Go и Java — в разработке под серию «Temporal: durable execution вглубь».
  • Версии Temporal Server и SDK пиновать (поведение determinism/patching чувствительно к версии).

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии