IIoT · Temporal · ISA-95

Temporal в промышленном IoT: почему датчики в него не ходят, а платформа без него разваливается

Про пирамиду автоматизации, границу облако/edge и то, за что на самом деле платит durable execution.

Есть вопрос-ловушка, который я слышал в разных формулировках — и на собеседованиях, и в рабочих спорах: «Temporal же медленный — как ты собрался гнать через него данные с датчиков температуры, там же PID-контур?»

Плохой ответ — начать защищать Temporal: тюнить, шардировать, ставить кэш перед persistence. Хороший ответ — «датчики в Temporal не ходят». Чтобы объяснить, почему это не отговорка, а архитектурный принцип, придётся пройти через три вещи:

Разберу на примере реальной платформы.

Исходная точка: типичная LoRaWAN-платформа

Вот архитектура, от которой отталкиваемся, — облачная IIoT-платформа на Python-микросервисах:

ChirpStack App Server · Network Server Frontends MQTT Mosquitto gRPC-Web Envoy WS NATS gRPC PYTHON BACKENDS EVENT PARSER API KAFKA Redis last value JetStream DB Writer Service Postgres
Исходная платформа: семь-восемь сервисов, три брокера, вся сложность — в транспорте данных.

Данные с LoRaWAN-устройств принимает ChirpStack, телеметрия уходит в Mosquitto, оттуда её разбирает Parser Service и складывает последние значения в Redis. Фронтенды ходят в API через Envoy — транскодер gRPC-Web → gRPC, потому что браузер нативный gRPC не умеет. Realtime вторая версия платформы получает по WebSocket напрямую из NATS. Все потоки записи сходятся в DB Writer Service — единую точку записи в Postgres.

Архитектура работает, но в ней семь-восемь сервисов, три брокера сообщений и — что важнее — она показывает только транспорт данных. Процессов на ней нет. А процессы есть всегда: провижининг устройства, эскалация алерта, раскатка прошивки на парк. В таких системах они обычно размазаны по крон-джобам и статусным колонкам в таблицах, и именно там живёт самая болезненная сложность.

Прежде чем перекраивать это под durable execution, нужно понять, на каком уровне всё это вообще находится. Для этого — пирамида.

Пирамида автоматизации: пять уровней, пять временных масштабов

Промышленная автоматизация десятилетиями строится по слоистой модели, формализованной в стандарте ISA-95. Смысл пирамиды не в бюрократии, а в физике: каждый уровень живёт на своём временном масштабе, и вычисления должны стоять настолько близко к процессу, насколько быстрым должен быть отклик.

L4 · ERP планирование · дни L3 · MES / процессы Temporal-воркфлоу · минуты–дни L2 · SCADA / мониторинг ingest → Redis → SSE · секунды ГРАНИЦА ОБЛАКО / EDGE L1 · ПЛК / PID цикл 10–100 мс · детерминизм L0 · датчики и актуаторы физика процесса уставка телеметрия чем ниже уровень — тем короче цикл и тем ближе к железу стоят вычисления
ISA-95: оранжевым — то, что съезжает в облако (L2–L3), красным — real-time edge (L0–L1), который облака не видит.

Внизу, на L0, — сама физика: термопары, датчики движения, клапаны, приводы. Над ними, на L1, — ПЛК и промышленные контроллеры. Здесь крутятся контуры регулирования, включая тот самый PID: цикл 10–100 миллисекунд, жёсткий детерминизм, никакой сети до облака. L2 — это SCADA и операторские панели: визуализация в реальном времени, выставление уставок, аларм; масштаб — секунды. L3 — MES, управление производственными процессами: минуты и часы. L4 — ERP и бизнес-планирование: дни.

Ключевое следствие: чем ниже уровень, тем короче цикл и тем ближе к железу должны стоять вычисления. PID-регулятор по температуре не может жить в облаке не потому, что облако «плохое», а потому что сеть до облака даёт сотни миллисекунд джиттера, а регулятору нужен стабильный цикл в десятки миллисекунд. Скорость света плюс TCP плюс брокер — это физика, её не чинит ни один вендор.

Где проходит граница облако/edge

Облачная IIoT-платформа — та, что на первой схеме, — это L2/L3 в облачном исполнении. Она не управляет процессом напрямую. Она делает две вещи:

Граница облако / edge в промышленной автоматизации Сверху облачная платформа уровней L2 и L3: мониторинг за секунды и Temporal-воркфлоу на минуты и дни. Снизу цех уровней L0 и L1 с PID-контуром за десятки миллисекунд. Между ними граница: вниз идут уставки, вверх телеметрия. При потере связи цех работает автономно. ОБЛАКО · L2–L3 · SUPERVISORY online Мониторинг L2 · секунды INGEST REDIS SSE → UI TIC-204 · 64.8 °C last value · горячий путь мимо Temporal Процессы · Temporal L3 · минуты–дни DeviceLifecycleWorkflow running AlertEscalationWorkflow await ack · 12m FirmwareRolloutWorkflow batch 3/20 ГРАНИЦА ОБЛАКО / EDGE уставка «держи 65 °C» редко · секунды ок телеметрия 64.8 °C · uplink поток · не команды EDGE · ЦЕХ · L0–L1 · REAL-TIME ПЛК PID · SP 65.0 °C Клапан актуатор · L0 Датчик PV 64.8 °C · L0 управление физика процесса измерение 10–100 мс Автономность связь с облаком пропала — контур продолжает работать, контроллер держит последнюю уставку. Облако не в контуре. L0–L1: детерминизм, источник истины — датчик L2–L3: durability, источник истины — история
Облако — supervisory-уровень: уставки вниз, телеметрия вверх. PID-контур замкнут локально и облака не видит.

Разделение труда выглядит так: облако говорит «держи 65 °C», и этой команде можно лететь хоть секунды — уставка меняется редко, и от её задержки процесс не разваливается. А вот удержание этих 65 °C — реакция на каждое отклонение — происходит локально на контроллере, в замкнутом контуре, который облако вообще не видит. Если связь с облаком пропала, цех продолжает работать: контроллер держит последнюю уставку.

Отсюда и ответ на вопрос-ловушку. «Быстро ли Temporal реагирует на датчик движения» — вопрос про смешение слоёв. Данные с частотой контура регулирования не должны покидать L1 в принципе — ни в Temporal, ни в Kafka, ни куда-либо ещё. LoRaWAN, кстати, сам по себе делает вопрос бессмысленным: downlink с латентностью в секунды и ограничения duty cycle — это транспорт для телеметрии и редких команд, а не для замкнутых контуров.

Три контура вместо одного: куда встаёт Temporal

Внутри самой облачной платформы то же слоение повторяется ещё раз. Есть три контура с разными требованиями, и Temporal подходит ровно для одного из них.

Первый — контур регулирования, миллисекунды. Как мы выяснили, его в облаке нет вообще, он остался на ПЛК.

Второй — горячий путь данных, миллисекунды-десятки миллисекунд внутри платформы. Ингест телеметрии, кэш последних значений, live-поток в интерфейс оператора. Здесь Temporal противопоказан: каждый переход воркфлоу — это запись истории в персистентное хранилище, что принципиально не миллисекунды и ограниченный throughput на кластер. Заводить workflow на каждое из тысяч сообщений в секунду — расстрел кластера и счёт за него.

Третий — процессный контур, секунды-дни. Эскалация алерта, провижининг устройства, раскатка прошивки. Здесь латентность Temporal (даже 200 мс на переход) не значит ничего, потому что сам процесс ждёт минуты и часы. Зато durability значит всё: процесс, который живёт три дня, обязан пережить деплой, падение воркера и рестарт базы.

Формула, которую стоит запомнить: Temporal — это supervisory-уровень. Не data plane и тем более не control plane.

Есть и контринтуитивный момент, который отличает человека, трогавшего АСУ ТП, от человека, читавшего про неё: на нижних уровнях durability не просто не нужна — она вредна. Перезагрузившийся ПЛК не «восстанавливает историю событий», он читает текущую температуру с датчика и продолжает регулировать. Его источник истины — сама физика, она никуда не девалась. Тащить event sourcing на L1 — значит не понимать, где там состояние. А вот у процесса «раскатать прошивку на 400 устройств» физического источника истины нет: если платформа забыла, на каком батче остановилась, — она забыла навсегда. Поэтому durability покупается там, где состояние существует только в софте.

Целевая архитектура

С учётом трёх контуров платформа пересобирается так (стек: TypeScript, Fastify, Temporal, Postgres, Redis):

ChirpStack → Mosquitto LoRaWAN · MQTT ~5000 msg/s Ingest consumer parse · batch · детект событий Redis last value Postgres batch insert signalWithStart · редко TEMPORAL DeviceLifecycleWorkflow per device AlertEscalationWorkflow FirmwareRolloutWorkflow Worker activities запись · нотификации · ChirpStack API API service Fastify · REST + SSE query / signal read Redis pub/sub → SSE HTTPS + SSE Frontends без Envoy
Три сервиса вместо семи: ingest, api, temporal-worker. Горячий путь — мимо Temporal.

Горячий путь идёт мимо Temporal: тонкий ingest-consumer слушает Mosquitto, парсит, пишет last value в Redis и батчами складывает сырьё в Postgres. Realtime в интерфейс — Redis pub/sub плюс SSE через Fastify, что заодно убивает Envoy вместе с gRPC-Web-транскодированием.

Сервисов вместо семи-восьми остаётся три: ingest, api, temporal-worker. DB Writer Service смиряется как отдельный сервис — его смысл (надёжная доставка в базу с ретраями) — это activity с retry policy, а не процесс с очередью и отдельным мониторингом. Event Service смиряется по той же причине: цепочка «событие → обработка → реакция → запись» — это буквально определение воркфлоу.

Что тогда ходит в Temporal

Ходят события и процессы, а не данные. Правило: в Temporal попадает то, у чего есть сюжет — начало, ожидание, развязка. У показания датчика сюжета нет: пришло, записалось, через десять секунд пришло следующее.

телеметрия · ~5000 msg/s Ingest consumer парсинг + детект смены состояния 99,9% · данные Redis + Postgres пришло → записал → забыл десятки в час · события События offline · порог · join · команда signalWithStart Temporal · воркфлоу
В Temporal ходят смены состояния, не поток. Разница — четыре порядка по объёму.

Разница видна на числах. Через ingest идёт поток телеметрии — порядка 5000 сообщений в секунду, и почти все они (≈99,9 %) просто пишутся в Redis и Postgres и на этом заканчиваются. Но иногда в этом потоке случается не рядовое показание, а событие — момент, когда в системе что-то поменялось:

Таких событий не тысячи в секунду, а десятки в час. Вот они и уходят сигналами в воркфлоу — всё остальное проходит мимо Temporal.

Три типовых сюжета, чтобы было предметно.

AlertEscalationWorkflow. Сценарий:

Без Temporal это таблица alerts со статусами, крон каждую минуту («кому пора эскалировать») и идемпотентность руками. С Temporal — линейный код:

export async function alertEscalationWorkflow(alert: AlertInput) {
  await notifyOnDuty(alert);

  let acked = false;
  let resolved = false;
  setHandler(ackSignal, () => { acked = true; });
  setHandler(resolvedSignal, () => { resolved = true; });

  const ackedInTime = await condition(() => acked || resolved, '15m');
  if (resolved) return closeAlert(alert, 'auto-resolved');
  if (!ackedInTime) {
    await notifySecondLine(alert);
    const secondAck = await condition(() => acked || resolved, '15m');
    if (!secondAck && !resolved) await smsShiftSupervisor(alert);
  }

  await condition(() => resolved);
  await closeAlert(alert, 'resolved');
}

Состояние («ждём подтверждения от второй линии уже 12 минут») живёт в самом воркфлоу и переживает деплой, падение воркера и рестарт базы. В крон-версии это состояние пришлось бы реконструировать из таблицы при каждом цикле.

DeviceLifecycleWorkflow — один долгоживущий воркфлоу на устройство, signalWithStart на каждое значимое событие:

Вся state machine устройства — это код одного воркфлоу с видимой историей в Temporal UI, а не enum-колонка, которую мучают пять сервисов наперегонки.

FirmwareRolloutWorkflow. «Раскатай v2.3 на 400 устройств»:

Процесс живёт часы или дни (LoRaWAN-устройства просыпаются редко), переживает что угодно и виден в UI с полной историей. Ручной аналог — очереди плюс таблица rollout_progress плюс молитва.

Тест для любой сущности предельно простой: есть ли у неё «ждать»? Ждать подтверждения, ждать таймаута, ждать батча, ждать человека. Есть — воркфлоу. Нет («пришло-записал-забыл») — обычный consumer и база. Показание датчика никогда ничего не ждёт — поэтому оно и не ходит в Temporal.

На что платим

Durable execution — это не бесплатная магия. Temporal продаёт одну гарантию: процесс доживёт до конца через любые падения, — и платит за неё персистентностью каждого шага. Кто понимает, за что платит, тот понимает, где покупать. Уставка «держи 65 °C» стоит того, чтобы пережить деплой. Каждое из пяти тысяч показаний в секунду — не стоит: у него нет процесса, который надо доводить до конца.

Инфраструктурная цена — сам Temporal-кластер. В минимальном варианте это temporalio/auto-setup в Docker Compose рядом с managed-базой, и если Temporal уже стоит для других задач, процессный слой платформы получает почти бесплатно.

Ещё один вариант — многохостовый. Сервисы Temporal (frontend, history, matching) разносятся по разным узлам, воркеры живут отдельно и подключаются к кластеру по сети, а персистентное хранилище — managed-Postgres или Cassandra — тоже отдельный сетевой узел, связанный с Temporal сетевыми соединениями. Тогда история воркфлоу переживает падение любого хоста, а пропускная способность растёт добавлением воркеров и шардов истории, а не вертикальным ростом одной машины.

Итог

Пирамида автоматизации пережила и волну «загоним всё в облако», и откат к edge computing — потому что она про физику, а не про моду: вычисления стоят настолько близко к процессу, насколько короткий у него цикл. Облачная IIoT-платформа — это L2/L3, supervisory-уровень: телеметрия вверх, уставки вниз, PID остаётся на контроллере и облака не видит.

Внутри платформы то же слоение: горячий путь данных (ingest → Redis → SSE) живёт мимо Temporal, а Temporal забирает то, что раньше было размазано по кронам и статусным колонкам, — процессы с ожиданием. Вопрос «а не медленный ли Temporal для датчиков» после этого отвечается одной фразой: датчики в Temporal не ходят, Temporal — supervisory. И это не отговорка, а самый содержательный ответ из возможных: он показывает, что ты понимаешь и цену durable execution, и пирамиду, по которой промышленная автоматизация живёт последние сорок лет.