IIoT · Temporal · ISA-95
Temporal в промышленном IoT: почему датчики в него не ходят, а платформа без него разваливается
Про пирамиду автоматизации, границу облако/edge и то, за что на самом деле платит durable execution.
Есть вопрос-ловушка, который я слышал в разных формулировках — и на собеседованиях, и в рабочих спорах: «Temporal же медленный — как ты собрался гнать через него данные с датчиков температуры, там же PID-контур?»
Плохой ответ — начать защищать Temporal: тюнить, шардировать, ставить кэш перед persistence. Хороший ответ — «датчики в Temporal не ходят». Чтобы объяснить, почему это не отговорка, а архитектурный принцип, придётся пройти через три вещи:
- как устроена классическая пирамида промышленной автоматизации;
- где в ней проходит граница между облаком и цехом;
- что именно durable execution покупает и почём.
Разберу на примере реальной платформы.
Исходная точка: типичная LoRaWAN-платформа
Вот архитектура, от которой отталкиваемся, — облачная IIoT-платформа на Python-микросервисах:
Данные с LoRaWAN-устройств принимает ChirpStack, телеметрия уходит в Mosquitto, оттуда её разбирает Parser Service и складывает последние значения в Redis. Фронтенды ходят в API через Envoy — транскодер gRPC-Web → gRPC, потому что браузер нативный gRPC не умеет. Realtime вторая версия платформы получает по WebSocket напрямую из NATS. Все потоки записи сходятся в DB Writer Service — единую точку записи в Postgres.
Архитектура работает, но в ней семь-восемь сервисов, три брокера сообщений и — что важнее — она показывает только транспорт данных. Процессов на ней нет. А процессы есть всегда: провижининг устройства, эскалация алерта, раскатка прошивки на парк. В таких системах они обычно размазаны по крон-джобам и статусным колонкам в таблицах, и именно там живёт самая болезненная сложность.
Прежде чем перекраивать это под durable execution, нужно понять, на каком уровне всё это вообще находится. Для этого — пирамида.
Пирамида автоматизации: пять уровней, пять временных масштабов
Промышленная автоматизация десятилетиями строится по слоистой модели, формализованной в стандарте ISA-95. Смысл пирамиды не в бюрократии, а в физике: каждый уровень живёт на своём временном масштабе, и вычисления должны стоять настолько близко к процессу, насколько быстрым должен быть отклик.
Внизу, на L0, — сама физика: термопары, датчики движения, клапаны, приводы. Над ними, на L1, — ПЛК и промышленные контроллеры. Здесь крутятся контуры регулирования, включая тот самый PID: цикл 10–100 миллисекунд, жёсткий детерминизм, никакой сети до облака. L2 — это SCADA и операторские панели: визуализация в реальном времени, выставление уставок, аларм; масштаб — секунды. L3 — MES, управление производственными процессами: минуты и часы. L4 — ERP и бизнес-планирование: дни.
Ключевое следствие: чем ниже уровень, тем короче цикл и тем ближе к железу должны стоять вычисления. PID-регулятор по температуре не может жить в облаке не потому, что облако «плохое», а потому что сеть до облака даёт сотни миллисекунд джиттера, а регулятору нужен стабильный цикл в десятки миллисекунд. Скорость света плюс TCP плюс брокер — это физика, её не чинит ни один вендор.
Где проходит граница облако/edge
Облачная IIoT-платформа — та, что на первой схеме, — это L2/L3 в облачном исполнении. Она не управляет процессом напрямую. Она делает две вещи:
- смотрит на телеметрию, которая поднимается снизу;
- спускает вниз уставки.
Разделение труда выглядит так: облако говорит «держи 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):
Горячий путь идёт мимо 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 попадает то, у чего есть сюжет — начало, ожидание, развязка. У показания датчика сюжета нет: пришло, записалось, через десять секунд пришло следующее.
Разница видна на числах. Через ingest идёт поток телеметрии — порядка 5000 сообщений в секунду, и почти все они (≈99,9 %) просто пишутся в Redis и Postgres и на этом заканчиваются. Но иногда в этом потоке случается не рядовое показание, а событие — момент, когда в системе что-то поменялось:
- устройство замолчало на десять минут;
- температура вышла за порог;
- устройство впервые вышло на связь после провижининга;
- оператор запустил раскатку прошивки.
Таких событий не тысячи в секунду, а десятки в час. Вот они и уходят сигналами в воркфлоу — всё остальное проходит мимо Temporal.
Три типовых сюжета, чтобы было предметно.
AlertEscalationWorkflow. Сценарий:
- порог пересечён → создать алерт, уведомить дежурного;
- ждать подтверждения 15 минут;
- нет подтверждения → эскалация на второго дежурного, снова ждать;
- всё ещё тишина → SMS начальнику смены;
- температура вернулась в норму → сигнал в воркфлоу, алерт автозакрывается.
Без 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 на каждое значимое событие:
- устройство создано → ждём join → ждём первый uplink → активно;
- замолчало → таймаут → offline → алерт;
- вернулось на связь → снова активно.
Вся state machine устройства — это код одного воркфлоу с видимой историей в Temporal UI, а не enum-колонка, которую мучают пять сервисов наперегонки.
FirmwareRolloutWorkflow. «Раскатай v2.3 на 400 устройств»:
- батч из 20 → downlink каждому;
- ждать подтверждений с таймаутом;
- посчитать процент отказов: больше 5 % → стоп и откат, иначе следующий батч.
Процесс живёт часы или дни (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, и пирамиду, по которой промышленная автоматизация живёт последние сорок лет.