2. Temporal. Архитектура и концепции
«Архитектура программной системы должна делать две вещи: решать поставленную бизнес-задачу сегодня и гарантировать, что завтра система не рассыплется на куски, когда упадет половина серверов.»
В предыдущей статье 1. Что такое workflow orchestration мы сформулировали фундаментальный вызов распределенных транзакций и познакомились с концепцией Durable Execution — иллюзией непрерывного выполнения кода, которая не разрушается даже при аппаратных сбоях.
Теперь мы переходим к детальному изучению Temporal — индустриального стандарта оркестрации современных распределенных систем, созданного авторами Cadence (Максимом Фатеевым и Самаром Аббасом) в недрах Uber и развивающегося как мощная open-source платформа на языке Go.
Чтобы писать высокопроизводительный, надежный код на Temporal и не совершать фатальных архитектурных ошибок, инженер обязан понимать механику работы платформы: где исполняются инструкции, как взаимодействуют узлы кластера и почему рантайм гарантирует сохранность состояния при падении дата-центров.
Разделение Control Plane и Data Plane
Самое частое и опасное заблуждение инженеров, впервые открывающих документацию: «Сервер Temporal исполняет скомпилированный Go-код наших бизнес-процессов».
Это категорически не так. В архитектуре Temporal проведена строгая демаркационная линия между плоскостью управления (Control Plane) и плоскостью данных (Data Plane).
flowchart TB
subgraph ControlPlane["Control Plane: Кластер Temporal (Кластерные сервисы на Go)"]
direction TB
F["Frontend Service<br>(gRPC API Gateway, Auth, Rate Limiter)"]
M["Matching Service<br>(Task Queues, Matchmaking, Long Polling)"]
H["History Service<br>(State Machines, Event Sourcing, Sharding)"]
W_SYS["Internal Worker Service<br>(Системные воркфлоу, архивация)"]
DB["(Durable Storage<br>PostgreSQL / Cassandra / MySQL)"]
F <--> M
F <--> H
H <--> DB
H <--> M
W_SYS <--> F
end
subgraph DataPlane["Data Plane: Ваша инфраструктура (Go Workers)"]
direction LR
AppClient["API Gateway / Backend<br>(Temporal Client SDK)"]
W1["Workflow Worker Pods<br>(Детерминированная оркестрация)"]
W2["Activity Worker Pods<br>(I/O, SQL, внешние API)"]
end
AppClient -->|"1. StartWorkflowExecution (gRPC)"| F
W1 <-->|"2. PollWorkflowTaskQueue (Long Poll)"| F
W2 <-->|"3. PollActivityTaskQueue (Long Poll)"| F
classDef cp fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef dp fill:#0f172a,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class ControlPlane cp;
class DataPlane dp;
1. Temporal Cluster (Control Plane)
Это серверный кластер, который вы разворачиваете в Kubernetes (через Helm/Operator) или арендуете как управляемый сервис (Temporal Cloud). Кластер ничего не знает о вашей бизнес-логике: он не запускает ваши бинарные файлы, не парсит структуры предметной области и не имеет доступа к вашим базам данных.
Его задачи — надежный учет состояния конечных автоматов, синхронизация таймеров и распределение задач через очереди. Кластер состоит из четырех микросервисов:
- Frontend Service: Публичный шлюз. Принимает все входящие gRPC-запросы от клиентов и воркеров. Отвечает за TLS-терминацию, аутентификацию (mTLS/JWT), валидацию неймспейсов (Namespaces), распределенный Rate Limiting и маршрутизацию запросов к внутренним сервисам.
- History Service: «Мозг» и транзакционное ядро Temporal. Он управляет состоянием каждого запущенного Workflow через распределенный конечный автомат (State Machine). Каждое действие Workflow шардируется по хэшу от
WorkflowID(по умолчанию от 512 до нескольких тысяч шардов) и атомарно записывается в персистентную СУБД (PostgreSQL, Cassandra, MySQL) в виде журнала событий (Event History). - Matching Service: Высокопроизводительный распределенный диспетчер очередей задач (Task Queues). Он сводит воедино задачи, ожидающие исполнения (поступившие от History Service), со свободными воркерами, ожидающими работу по открытым каналам Long Polling.
- Worker Service: Вспомогательный внутренний сервис для служебных нужд самого кластера (репликация данных между регионами, архивация старой истории в S3/GCS, очистка удаленных неймспейсов).
2. Workers (Data Plane)
Это ваши собственные Go-приложения. Вы компилируете их стандартным go build, упаковываете в Docker-образы и масштабируете горизонтально с помощью HPA в Kubernetes.
Воркер не слушает входящие сетевые порты (ему не нужен открытый Ingress). При старте он инициализирует temporal.Client, подключается к Frontend Service по gRPC и открывает долгоживущее соединение, заявляя:
«Я воркер неймспейса 'billing', слушаю Task Queue 'orders-v1' и зарегистрировал у себя Workflow 'ProcessOrder' и Activity 'ChargeCreditCard'. Жду задач!».
Фундаментальные абстракции: Workflow и Activity
Чтобы движок Event Sourcing мог надежно воспроизводить состояние при сбоях, код строго разделяется на две категории ответственности.
classDiagram
class WorkflowDefinition {
<<Orchestrator>>
+Deterministic logic
+State Machine & Control Flow
+workflow.Sleep()
+workflow.Go()
+workflow.ExecuteActivity()
-NO I/O, NO time.Now()
}
class ActivityDefinition {
<<Worker>>
+Non-deterministic logic
+External HTTP / gRPC
+Database SQL Read/Write
+RetryPolicy & Timeouts
+Heartbeat reporting
}
WorkflowDefinition ..> ActivityDefinition : invokes via Task Queue
1. Workflow (Оркестратор)
Workflow — это детерминированная функция-дирижер. Она задает логику и правила перехода между фазами бизнес-процесса: циклы, ветвления if/else, реакцию на таймеры и внешние сигналы.
Главное правило: Код Workflow обязан быть на 100% детерминированным.
Если передать в функцию Workflow те же аргументы и скормить ей тот же журнал событий из истории, она обязана выполнить ровно ту же последовательность шагов и остановиться в той же точке.
Из-за этого в теле Workflow СТРОГО ЗАПРЕЩЕНО:
- Выполнять любой сетевой I/O (обращаться по HTTP, вызывать сторонние микросервисы, слать SQL-запросы).
- Читать файлы с локального диска или опрашивать переменные окружения, способные измениться во времени.
- Использовать системное время через
time.Now()(при воспроизведении истории время будет разным!). - Генерировать псевдослучайные числа (
rand/crypto/rand). - Использовать стандартные горутины
go func()(планировщик Go недетерминирован и переключает контексты в непредсказуемом порядке). - Итерироваться по структуре
mapс помощью циклаrange(в Go порядок обхода ключей в мапе намеренно рандомизируется при каждом запуске программы!).
2. Activity (Рабочая лошадка)
Activity — это место для всей реальной, «грязной» работы. Внутри функции Activity разрешено абсолютно всё, что нужно для взаимодействия с физическим миром: сетевые вызовы к внешним сервисам, SQL-транзакции, чтение S3, парсинг файлов, генерация криптографических ключей.
- Activity выполняется в пуле воркеров и может завершаться сетевыми сбоями или падениями процесса.
- Temporal автоматически повторяет выполнение упавшей Activity в соответствии с настроенной политикой
RetryPolicy. - Результат работы (или финальная неисправимая ошибка) фиксируется в History Service и возвращается в вызывающий Workflow как результат выполнения функции.
Анатомия вызова (Mechanical Sympathy)
Что происходит на уровне сети, сокетов и базы данных, когда воркер исполняет одну строку кода:
workflow.ExecuteActivity(ctx, SendEmail, args)?
sequenceDiagram
autonumber
participant W as Workflow Worker
participant Front as Frontend Service
participant Hist as History Service
participant DB as Persistence DB
participant Match as Matching Service
participant ActW as Activity Worker
W->>Front: gRPC: RespondWorkflowTaskCompleted + ScheduleActivityTaskCommand
Front->>Hist: Process Workflow Task Decisions
Hist->>DB: BEGIN TX: Append History (ActivityTaskScheduled)
DB-->>Hist: COMMIT (fsync to disk)
Hist->>Match: Add Activity Task to Queue
ActW->>Front: gRPC: PollActivityTaskQueue (Long Poll)
Front->>Match: Get Pending Task
Match-->>ActW: Return Task: SendEmail(args)
Note over ActW: Исполнение SendEmail (HTTP API / SMTP)
ActW->>Front: gRPC: RespondActivityTaskCompleted(result)
Front->>Hist: Record Activity Result
Hist->>DB: BEGIN TX: Append History (ActivityTaskCompleted)
DB-->>Hist: COMMIT (fsync to disk)
Hist->>Match: Schedule Workflow Task to wake up Workflow
W->>Front: gRPC: PollWorkflowTaskQueue (Long Poll)
Front->>Match: Fetch Workflow Task
Match-->>W: Return Task (with Activity result)
Note over W: Workflow разблокирован и продолжает выполнение
Детальный разбор шагов:
- Workflow Worker: Вызов
workflow.ExecuteActivityне делает прямой вызов по сети к сервису email. Вместо этого воркер приостанавливает корутину Workflow и формирует командуScheduleActivityTaskCommand. - gRPC: Команда отправляется на Frontend Service в теле ответа
RespondWorkflowTaskCompleted. - History Service & DB: Сервер открывает транзакцию в БД, записывает событие
ActivityTaskScheduledи фиксирует ее на диске (fsync). - Matching Service: Задача помещается в память Matching Service в указанную очередь (
Task Queue). - Activity Worker: Свободный Activity-воркер забирает задачу по висящему gRPC-запросу
PollActivityTaskQueue. - Выполнение: Воркер вызывает функцию
SendEmail, совершая реальный сетевой вызов к внешнему почтовому сервису. - Завершение: Воркер отправляет результат в кластер через gRPC-вызов
RespondActivityTaskCompleted. - Фиксация результата: History Service открывает транзакцию и записывает в журнал событие
ActivityTaskCompleted. - Пробуждение: Matching Service создает новую задачу
WorkflowTaskдля воркера оркестрации, который просыпается и передает распакованный результат выполнения в переменную Go.
Один вызов Activity генерирует минимум 4 сетевых gRPC-раундтрипа и 2 дисковые транзакции в СУБД кластера. Именно поэтому проектировать системы на Temporal нужно крупными блоками бизнес-логики, избегая превращения мелких примитивных выражений в отдельные Activity.
Идиоматичный пример на Go
Рассмотрим production-ready структуру сервиса онбординга пользователя с настройкой таймаутов, повторов и клиентской инициализацией:
package onboarding
import (
"context"
"fmt"
"net/http"
"time"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
)
// ---------------------------------------------------------------------
// 1. Описание Activity (Недетерминированный рабочий код)
// ---------------------------------------------------------------------
type EmailService struct {
client *http.Client
}
func (s *EmailService) SendWelcomeEmailActivity(ctx context.Context, email string) error {
logger := activity.GetLogger(ctx)
logger.Info("Отправка приветственного письма", "email", email)
// Реальный поход во внешний мир (SMTP / REST API)
if email == "" {
// Non-retryable ошибка: ретраить бесполезно, email невалиден
return temporal.NewNonRetryableApplicationError(
"email cannot be empty",
"INVALID_ARGUMENT",
nil,
)
}
// Имитация успешной отправки
return nil
}
// ---------------------------------------------------------------------
// 2. Описание Workflow (Детерминированный дирижер процесса)
// ---------------------------------------------------------------------
const TaskQueueName = "onboarding-tasks"
func UserOnboardingWorkflow(ctx workflow.Context, email string) (string, error) {
logger := workflow.GetLogger(ctx)
logger.Info("Старт онбординга пользователя", "email", email)
// Настройка политик вызова Activity
ao := workflow.ActivityOptions{
// Время на выполнение одной попытки
StartToCloseTimeout: 10 * time.Second,
// Общее время со всеми повторами
ScheduleToCloseTimeout: 1 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: 1 * time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 15 * time.Second,
MaximumAttempts: 5,
},
}
ctx = workflow.WithActivityOptions(ctx, ao)
// 1. Вызов Activity отправки письма
var emailService *EmailService
err := workflow.ExecuteActivity(ctx, emailService.SendWelcomeEmailActivity, email).Get(ctx, nil)
if err != nil {
logger.Error("Не удалось отправить приветственный email", "error", err)
return "", fmt.Errorf("failed to send welcome email: %w", err)
}
// 2. Долгоживущая пауза на 3 дня (Durable Sleep)
// Сервер освобождает ресурсы, таймер тикает в кластере Temporal
logger.Info("Ожидание 3 дней перед отправкой обучающих материалов")
err = workflow.Sleep(ctx, 3*24*time.Hour)
if err != nil {
return "", err
}
logger.Info("Онбординг успешно завершен")
return "Onboarding Completed", nil
}
// ---------------------------------------------------------------------
// 3. Запуск Worker-процесса
// ---------------------------------------------------------------------
func RunWorker() error {
// Подключение к Frontend-сервису кластера Temporal
c, err := client.Dial(client.Options{
HostPort: "localhost:7233",
Namespace: "default",
})
if err != nil {
return fmt.Errorf("unable to create Temporal client: %w", err)
}
defer c.Close()
// Инициализация воркера для заданной Task Queue
w := worker.New(c, TaskQueueName, worker.Options{
MaxConcurrentActivityExecutionSize: 100,
MaxConcurrentWorkflowTaskExecutionSize: 100,
})
emailSvc := &EmailService{client: http.DefaultClient}
// Регистрация доступных для исполнения сущностей
w.RegisterWorkflow(UserOnboardingWorkflow)
w.RegisterActivity(emailSvc.SendWelcomeEmailActivity)
// Блокирующий запуск прослушивания очереди
return w.Run(worker.InterruptCh())
}
Подводные камни и вопросы с собеседований
Итог
- Temporal реализует четкое разделение между Control Plane (кластер сервисов на Go, управляющий историей и очередями) и Data Plane (ваши воркеры с бизнес-кодом).
- Кластер Temporal состоит из четырех сервисов: Frontend (gRPC API), History (конечные автоматы и персистентность), Matching (диспетчеризация очередей) и Worker (фоновые задачи кластера).
- Общение между воркерами и кластером построено на gRPC Long Polling, обеспечивающем естественное противодавление (Backpressure) и нулевые задержки без избыточной нагрузки на сеть.
- Workflow — детерминированный алгоритмический дирижер, где запрещен сетевой/дисковый ввод-вывод.
- Activity — изолированный исполнитель прикладной логики, защищенный механизмами автоматических повторов (
RetryPolicy) и таймаутов. - Надежность требует вычислительных ресурсов: каждый шаг оркестрации фиксируется транзакцией на диске хранилища истории, поэтому Temporal применяется для оркестрации ответственных бизнес-транзакций.
В следующей лекции мы разберем главный механизм «бессмертия» функций — как именно рантайм выполняет воспроизведение истории и обеспечивает детерминизм: 3. Durable execution.