MAATRIX / Блог / Temporal на сервере: частые ошибки и решения

Temporal на сервере: частые ошибки и решения

MAATRIX

Temporal обещает надёжные длительные workflow «из коробки» — retry, таймауты, версионирование логики уже встроены в движок. На практике первое знакомство с self-hosted кластером почти всегда сопровождается набором одних и тех же граблей: воркер не подхватывает задачи, история workflow растёт до неприличных размеров, а после деплоя новой версии кода падают уже запущенные процессы. Собрал самые частые проблемы Temporal на собственном сервере и рабочие способы их решения — без прикрас и без «просто перезапустите».

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Как устроен Temporal и откуда берутся проблемы

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

Temporal-кластер состоит из четырёх ролей (Frontend, History, Matching, Worker service), которые в self-hosted инсталляции обычно запускаются одним бинарником temporal-server с разными наборами включённых сервисов, плюс persistence-слой — Cassandra, PostgreSQL или MySQL для истории событий, и опционально Elasticsearch для visibility (поиск workflow по атрибутам). Отдельно, вне кластера, живут ваши воркеры — процессы на Go/Java/Python/TypeScript, которые подключаются к серверу, забирают задачи из task queue и выполняют код workflow и activity.

Ключевая идея, которую нужно держать в голове: сервер Temporal не выполняет ваш код. Он только хранит историю событий и раздаёт задачи воркерам через task queue. Если воркер упал, отключился от сети или не слушает нужную очередь — workflow просто зависает в ожидании, сервер при этом «здоров» и не покажет ошибку. Отсюда добрая половина тикетов вида «temporal не работает», хотя по факту не работает воркер.

Минимальный self-hosted стек на Ubuntu через docker-compose:

services:
  postgresql:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: temporal
      POSTGRES_USER: temporal
    volumes:
      - pgdata:/var/lib/postgresql/data

  temporal:
    image: temporalio/auto-setup:1.24.2
    depends_on:
      - postgresql
    environment:
      - DB=postgres12
      - DB_PORT=5432
      - POSTGRES_USER=temporal
      - POSTGRES_PWD=temporal
      - POSTGRES_SEEDS=postgresql
    ports:
      - "7233:7233"

  temporal-ui:
    image: temporalio/ui:2.31.2
    environment:
      - TEMPORAL_ADDRESS=temporal:7233
    ports:
      - "8080:8080"

volumes:
  pgdata:

Для боевой нагрузки образ auto-setup подходит только для старта — на каждом рестарте он прогоняет миграции схемы, что в кластере с несколькими нодами создаёт гонки. В проде стоит разносить temporal-server и шаги миграции (temporal-sql-tool) по отдельности.

Ошибка: воркер не забирает задачи (workflow висит в статусе Running)

Самая частая жалоба: workflow запущен, в UI виден статус Running, но ни один шаг не выполняется — история не растёт часами. Причины по убыванию частоты:

  1. Воркер слушает не ту task queue. Имя очереди в client.ExecuteWorkflow должно точно совпадать со строкой, на которую подписан воркер через worker.New(client, "my-task-queue", ...). Регистр важен, опечатка в одном символе — и задачи копятся в очереди, которую никто не слушает.
  2. Воркер не может подключиться к frontend-у. Проверьте логи воркера на context deadline exceeded при подключении к 7233 — частая причина в self-hosted окружении: firewall режет порт, или воркер обращается по внутреннему hostname, который не резолвится с той машины, где он запущен.
  3. Namespace не совпадает. По умолчанию всё крутится в default, но если где-то явно указан другой namespace, а воркер поднят с default — задачи никогда не встретятся.
  4. Sticky-очередь протухла. После обновления кода воркера старый sticky task queue может ссылаться на уже недоступный процесс — сервер подождёт StickyScheduleToStartTimeout (по умолчанию 5 секунд) и переотправит задачу в обычную очередь, но если и там пусто — тупик тот же.

Диагностика через temporal CLI:

temporal workflow describe --workflow-id order-12345
temporal task-queue describe --task-queue my-task-queue

Во втором выводе смотрите на поле Pollers — если список пуст, к очереди в данный момент не подключён ни один воркер, и это прямое подтверждение проблемы №1 или №2.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Ошибка: workflow падает после деплоя новой версии кода (non-determinism)

Temporal перевыполняет (replay) историю workflow при каждом восстановлении состояния — это и есть механизм durability. Replay должен дать тот же самый набор команд, что и оригинальное выполнение. Если вы поменяли код workflow — добавили условие, убрали шаг, изменили порядок вызовов activity — старые запущенные инстансы при следующем replay упадут с NonDeterministicWorkflowError.

Типичные источники недетерминизма: прямые вызовы time.Now()/rand.Int() внутри тела workflow вместо workflow.Now(ctx)/workflow.SideEffect; изменение порядка workflow.ExecuteActivity без версионирования; итерация по map без сортировки ключей (в Go порядок map недетерминирован); условная логика, зависящая от внешнего состояния на момент выполнения, а не сохранённого в истории.

Решение — workflow.GetVersion для безопасной эволюции кода:

v := workflow.GetVersion(ctx, "add-shipping-step", workflow.DefaultVersion, 1)
if v == workflow.DefaultVersion {
    // старая ветка — для workflow, начатых до изменения
} else {
    err = workflow.ExecuteActivity(ctx, ShippingActivity, order).Get(ctx, nil)
}

Старые (in-flight) workflow при replay попадут в DefaultVersion и продолжат идти по прежней логике, новые — получат маркер версии 1. Без этого механизма любое изменение кода workflow с уже запущенными инстансами — почти гарантированный инцидент. Практический совет: activity менять безопасно (детерминизм там не нужен), а перед мерджем изменений в сам workflow прогоняйте replay-тесты на сохранённых историях реальных production-workflow — это ловит большую часть таких ошибок до деплоя.

Ошибка: раздутая история workflow (Event History too large)

Если workflow живёт долго (циклы согласований, saga на недели) и вызывает много activity, история событий растёт линейно и упирается в лимит — сервер начинает предупреждать на 10 000 событий и жёстко блокирует продолжение на 50 000 (HistorySizeLimitError).

Основной штатный механизм — Continue-As-New: workflow сам завершает себя и немедленно стартует новый инстанс с тем же workflow ID, но чистой историей, передавая нужное состояние как input:

if workflow.GetInfo(ctx).GetContinueAsNewSuggested() {
    return workflow.NewContinueAsNewError(ctx, MyWorkflow, state)
}

Флаг ContinueAsNewSuggested сервер сам выставляет, когда история приближается к порогу — проверяйте его в каждой итерации длинного цикла (обычно в connector-workflow, слушающих сигналы годами). Ещё два практических правила: не плодите activity там, где хватит одной с батчингом (частая ошибка — вызывать по одной activity на каждую запись при обработке тысяч элементов), и используйте Local Activity вместо обычных для коротких операций (до нескольких секунд) — они не создают отдельных событий в истории и пишутся компактнее.

Persistence и сайзинг: PostgreSQL vs Cassandra

Для self-hosted кластера чаще всего выбирают между PostgreSQL и Cassandra. Для старта и для нагрузок до нескольких сотен workflow в секунду PostgreSQL — более простой и предсказуемый вариант: один процесс, знакомые инструменты бэкапа, меньше операционной сложности, чем у Cassandra-кластера.

КритерийPostgreSQLCassandra
Операционная сложностьНизкая, один инстанс + репликаВысокая, нужен кворум узлов
МасштабированиеОграничено (шардирование вручную)Изначально распределённая
Рекомендуется дляСтарт, средняя нагрузка, < 1000 workflow/секВысоконагруженные enterprise-кластеры

Отдельный совет по тюнингу PostgreSQL под Temporal: таблицы executions, history_node, history_tree получают интенсивную нагрузку на запись — вынесите WAL на отдельный диск (лучше NVMe), увеличьте max_connections и не забывайте про autovacuum — на history_node с активным Continue-As-New накапливается много мёртвых строк.

Пример без auto-setup, шаги миграции вручную:

temporal-sql-tool --plugin postgres12 --ep 127.0.0.1 -p 5432 \
  -u temporal --pw 'secret' create-database -db temporal
temporal-sql-tool --plugin postgres12 --ep 127.0.0.1 -p 5432 \
  -u temporal --pw 'secret' -db temporal setup-schema -v 0.0
temporal-sql-tool --plugin postgres12 --ep 127.0.0.1 -p 5432 \
  -u temporal --pw 'secret' -db temporal \
  update-schema -d ./schema/postgresql/v12/temporal/versioned

Для сайзинга под старт достаточно 2 vCPU / 4 ГБ RAM на сервер Temporal и столько же на PostgreSQL; для сотен workflow/сек закладывайте 4-8 vCPU и 8-16 ГБ RAM на каждый компонент отдельно, с NVMe под WAL базы. Это ориентировочные цифры для планирования, а не результат бенчмарка — точные требования зависят от размера ваших workflow-историй и частоты вызовов activity, так что закладывайте запас и проверяйте под своей нагрузкой.

Настройка воркеров и мониторинг кластера

Воркер — обычный процесс, которому применимы все стандартные практики системного администрирования. Ключевые параметры worker.Options (Go SDK), которые чаще всего просто не настраивают, оставляя дефолты:

w := worker.New(c, "orders-task-queue", worker.Options{
    MaxConcurrentActivityExecutionSize:     50,
    MaxConcurrentWorkflowTaskExecutionSize: 50,
    MaxConcurrentActivityTaskPollers:       4,
    MaxConcurrentWorkflowTaskPollers:       4,
})

Дефолты консервативны и рассчитаны на «не уронить сервер новичку» — на выделенном сервере с 8+ ядрами они почти всегда занижены. Поднимайте постепенно, ориентируясь на метрику temporal_worker_task_slots_available. Отдельно — таймауты activity: без явного StartToCloseTimeout зависшая activity (например, HTTP-запрос без собственного таймаута) будет висеть неопределённо долго, это обязательный параметр, который легко забыть:

ao := workflow.ActivityOptions{
    StartToCloseTimeout: 30 * time.Second,
    RetryPolicy: &temporal.RetryPolicy{
        InitialInterval:    time.Second,
        BackoffCoefficient: 2.0,
        MaximumAttempts:    5,
    },
}

Для мониторинга сервер отдаёт Prometheus-метрики на :9090/metrics. Три главные: service_errors (ошибки gRPC-сервисов кластера), persistence_latency (задержки к базе — первый признак, что PostgreSQL не справляется), task_schedule_to_start_latency (время от постановки задачи до её взятия воркером — рост почти всегда означает нехватку или недоступность воркеров). Связку Grafana и Prometheus проще всего поднять рядом на том же сервере — официальный репозиторий temporalio/dashboards даёт готовые панели, достаточно импортировать по ID.

Быстрые команды для ручной диагностики:

# зависшие workflow старше часа
temporal workflow list --query "ExecutionStatus='Running' AND StartTime < '2026-08-30T00:00:00Z'"

# принудительный откат до конкретной точки истории
temporal workflow reset --workflow-id order-12345 --type FirstWorkflowTask

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему workflow завис, а в логах сервера нет ошибок?

Скорее всего, воркер не подключён к нужной task queue или упал. Сервер Temporal не выполняет код сам — он только хранит историю и ждёт воркера. Проверьте temporal task-queue describe — пустой список Pollers подтверждает диагноз.

Можно ли просто перезапустить воркер, чтобы решить NonDeterministicWorkflowError?

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

Сколько workflow может обработать один self-hosted сервер Temporal?

Зависит от размера истории, персистентного слоя и ресурсов воркеров — единого числа нет. На PostgreSQL при разумной настройке кластер спокойно тянет десятки-сотни workflow в секунду; для большего смотрите в сторону Cassandra и горизонтального масштабирования сервисов.

Нужен ли Elasticsearch обязательно?

Нет, базовый SQL-based visibility работает без него и достаточен для небольших объёмов и простых фильтров по статусу/времени. Elasticsearch нужен, когда требуется поиск по кастомным search attributes на больших объёмах workflow.

Как безопасно откатить деплой, если новая версия workflow уже сломала часть инстансов?

Откатите код воркера на предыдущую версию — уже запущенные workflow продолжат идти по старой логике при следующем decision task. Проблема возникает, только если между версиями успели запуститься новые workflow, начавшие писать историю по новой ветке — тогда потребуется workflow reset до точки перед проблемным решением.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →