На стенде интегратора Docker поднимает runtime за две минуты: тот же проект, тот же цикл 20 мс, Modbus с эмулятора, HMI в браузере. Заказчик видит и говорит: «На линии поставим так же, на промышленный ПК, без отдельного контроллера». На третьей смене после «некритичного» обновления хоста контейнер стартует, цикл плавает, USB-адаптер RS-485 переезжает на другой tty, NTP скачет, retain читается со слоя, который перезаписали. Смена видит нормы на экране и одновременно ручной режим на агрегате. Стенд врал не в логике программы - он врал в окружении.
Разбираем контейнер (Docker, Podman и аналоги) с softPLC-runtime на промышленном ПК: чем живой объект отличается от стенда, что делают обновления хоста, NTP, изоляция CPU, проброс USB и Ethernet, и когда оставляют обычный аппаратный контроллер. Не повторяем спор «виртуальный ПЛК вообще против шкафа» - здесь не про идею softPLC, а про упаковку runtime в контейнер на IPC и про то, где эта упаковка разваливается в эксплуатации. Не курс по Dockerfile и не сравнение оркестраторов.
Короткий ответ
Контейнер с runtime ПЛК на IPC работает там, где цикл не жёсткий, I/O идёт по сети без капризного USB-passthrough, хост заморожен по ядру и пакетам, NTP один на весь контур, CPU изолирован, а смена имеет регламент отката образа не хуже замены блока контроллера.
На стенде обычно нет антивируса, нет чужих контейнеров, нет патча ядра раз в месяц, нет оператора, который перетыкает кабель, нет температуры шкафа 48 °C и нет требования «не перезагружать до конца партии». Именно это ломает пуск: джиттер, потеря tty, съехавшие MAC/IP после обновления docker0, overlayfs вместо ожидаемого retain, рассинхрон журналов.
Обычный контроллер оставляют на контурах с предсказуемым циклом у механизма, на ПАЗ/SIL, в шкафу без ИТ-сопровождения и там, где отказ должен лечиться заменой изделия, а не разбором cgroups. Runtime в контейнере - CODESYS, MasterSCADA 4D или иной совместимый IEC 61131-3 - не делает IPC контроллером сам по себе; это тот же проект на чужом планировщике. Среды равнозначны как заказ runtime, но ни одна не отменяет дисциплины хоста.
Что именно упаковывают в контейнер
На практике в образ кладут не «ПЛК», а процесс runtime, библиотеки, иногда веб-HMI, иногда шлюз. Хост - Linux на IPC: ядро, сеть, USB, диски, NTP, журналы. Контейнер видит то, что ему пробросили: PID и CPU через cgroups, файлы через volume, порты через NAT или host network, устройства через --device или privileged.
Путаница начинается со слова «изолирован». Изоляция удобна для доставки версии. Она не даёт детерминизма цикла. Планировщик хоста, прерывания сети, сборка мусора соседнего контейнера, trim SSD, обновление containerd - всё это снаружи образа и на стенде часто выключено.
Podman rootless на объекте выглядит безопаснее для ИБ и больнее для realtime: лишние слои, другие права на устройства, сюрпризы с портами ниже 1024. Docker с privileged «чтобы завелось» зачёркивает смысл контейнера как границы. Нужен минимум прав: конкретные устройства, конкретные порты, read-only корень, volume только под retain, логи и лицензии.
Лицензия runtime привязана к железу, MAC, ключу, файлу. Контейнер, который каждый раз получает новый MAC на bridge, «теряет» лицензию после рестарта. Это стенд не показывает, если лицензия trial или ключ лежит в том же каталоге, что и проект.
Почему стенд врёт
Стенд честен по прикладной логике и лжив по эксплуатации. На столе IPC один, контейнеров два, эмулятор I/O отвечает мгновенно, инженер сидит рядом и не ставит Windows Update на хост, потому что хост - Linux «под проект». Сеть - один коммутатор без шторма камер и без сканера ИБ.
На линии тот же IPC делит шкаф с управляемым коммутатором, питанием, иногда с камерой «для охраны», иногда с чужим ПО «поставьте ещё сбор логов». ИБ требует агента. IT требует патч. Энергетики требуют не трогать до плановой остановки. Смена требует, чтобы после мигания света всё поднялось само в правильном порядке: NTP, сеть, runtime, потом поле.
Типовые расхождения стенд / смена:
нагрузка CPU и IRQ, которой не было при эмуляторе; USB-последовательный адаптер, который перечисляется иначе после питания; время хоста, которое сначала «как получилось», потом прыгает на часы; обновление ядра, после которого не грузится модуль realtime или драйвер NIC; volume, который на стенде bind-mount, а в «боевом» compose - named volume и другой uid; сеть host vs bridge: на стенде host, на объекте bridge «для безопасности» и другой latency; второй контейнер (MQTT, Grafana, «маленький Python»), которого «не было в объёме работ».
Пока эти пункты не входят в программу FAT на целевом IPC с целевым образом хоста, стенд доказывает только то, что программа компилируется.
Обновления хоста: тихий убийца цикла
Контейнер обещает: обновили приложение, хост не трогаем. На объекте хост всё равно трогают. CVE в ядре, docker, openssl, агент резервного копирования. После патча «сервисы поднялись», а runtime нет: cgroup v1/v2, iptables/nftables, переименовался интерфейс, SELinux подрезал device.
Регламент, без которого контейнерный ПЛК нельзя сдавать:
заморозка версии ядра и runtime контейнера на период между плановыми остановами; стенд-клон хоста, на котором патч накатывают раньше линии; запрет автообновления; окно отката: старый kernel в grub, старый образ контейнера в registry на площадке, не «в облаке подрядчика»; проверка цикла и I/O после каждого патча, не только docker ps.
ИБ-служба услышит «не патчим» враждебно. Компромисс - патч по регламенту ОТ, как замена прошивки контроллера: план, окно, акт, не ночной silent update.
Антивирус и EDR на хосте с realtime - отдельный разговор. Сканер, который находит «новый» бинарник runtime после обновления образа и начинает его хешировать на лету, даёт пики CPU. На стенде агента нет, на объекте «так политика». Либо исключение каталогов с измеримым влиянием на джиттер, либо runtime не в контейнере на этом хосте.
NTP, журналы и фантомные события
IPC с контейнерами любит жить с часами VM и с hwclock «потом». Runtime пишет метки, MQTT/OPC шлюз пишет свои, SCADA - третьи. После патча chrony стартует позже контейнера: первые минуты после питания метки в прошлом или в будущем. Смена видит аварию «раньше пуска».
Правило то же, что для любого контура, только жёстче: хост синхронизируется до старта runtime. В systemd это не After=docker.service, а явное ожидание синхронизации (chrony wait, или хотя бы лимит шага) и запись в журнал «время зафиксировано». Контейнеру лучше отдавать тот же clock, не вводить второй NTP внутри образа «для удобства».
Retain и файлы проекта с метками времени разъезжаются, если volume на NFS с другим часовым поясом. Для ПЛК это экзотика, для «IPC в серверной» - нет. FAT должен включать отключение питания на 30 секунд и на 10 минут: что поднялось, какой порядок, какие метки в первом цикле.
CPU isolation, realtime и сосед по сокету
SoftPLC на Linux RT имеет смысл, когда процесс прибит к ядрам, IRQ сети уведены, изолированные CPU не отданы Docker «всем подряд». Контейнер по умолчанию делится планировщиком с whoever. cpu_shares - не realtime. Нужны cpuset, иногда isolcpus/nohz_full на хосте, и запрет другим контейнерам садиться на те же ядра.
Privileged realtime в контейнере требует CAP_SYS_NICE, иногда CAP_IPC_LOCK, mlock, корректный chrt. Документация runtime это описывает для «голого» Linux. Слой Docker добавляет свои дефолты: CFS quota, которая внезапно режет процесс в конце квоты и даёт пик цикла раз в период. На стенде квоты нет (unlimited). На объекте compose из шаблона IT квоту ставит «чтобы не сожрал сервер».
Измеряют не средний цикл, а максимум за час под нагрузкой: опрос, архив, веб-HMI, рестарт соседнего контейнера, docker logs. Если 99-й перцентиль вылезает за уставку технологии - либо снимают соседей, либо уходят на аппаратный ПЛК.
CODESYS и MasterSCADA 4D как runtime на одном и том же IPC подчиняются одним законам хоста. Выбор среды не лечит джиттер Docker. Он лечит только то, как написан проект и как лицензируется.
USB и Ethernet passthrough: где стенд особенно лжёт
USB-RS-485 на стенде - один адаптер, всегда /dev/ttyUSB0. На объекте после грозы и перетыкания появляется ttyUSB1, udev не успел, контейнер стартовал с неправильным портом, опрос «жив», ответы - чужого устройства или тишина. Симлинки udev по serial id обязательны; в контейнер пробрасывают стабильное имя, не номер. И даже тогда USB в шкафу с помехами - слабое место против встроенного порта IPC или против Ethernet-шлюза.
Ethernet passthrough (macvlan, sriov, USB-NIC в контейнер) ломается на обновлении cni-плагина, на конфликте MAC с сетью завода, на STP, который видит «новый» хост. Host network проще для цикла и хуже для ИБ. Компромисс часто такой: runtime в host network или с выделенным физическим NIC, шлюзы и дашборды - в bridge.
Нельзя пробрасывать «все USB» privileged, потому что на стенде так быстрее. На объекте в тот же IPC втыкают сервисную флешку - и runtime видит новый диск, иногда перемонтирует volume, иногда просто подвисает udev.
Полевая связь лучше уезжает в Ethernet: удалённые I/O, Modbus TCP, OPC. Тогда контейнеру не нужен USB. Это возвращает к архитектуре: контейнерный runtime на IPC хорошо живёт как шлюз и как некритичная логика с сетевым I/O. Плохо - как замена корзины модулей на проводе.
Сетевые привычки те же, что в промышленных Ethernet-сетях: кольцо, шторм, качество линка. Контейнерный NAT не должен стоять на пути цикла до частотника.
Диск, overlay, retain и «куда делся проект»
Слои образа read-only - плюс для воспроизводимости и минус, если наладчик пишет конфиг внутрь контейнера. После recreate конфиг исчезает. На стенде контейнер не пересоздают неделями. На объекте «пересоздали, чтобы очистить» - и пропали привязки.
Retain, рецепты, лицензии, журналы - только на volume вне слоя. Права uid внутри контейнера должны совпадать с хостом. Rootless Podman здесь регулярно подкладывает 100000+ mapping: runtime не может писать retain, стартует «с нуля», клапаны в безопасное, смена в панике.
Диск IPC в шкафу - часто eMMC или маленький SSD. Журнал Docker и json-file логов без ротации забивает раздел. Runtime при этом «не виноват»: диск 100%, цикл растёт, потом read-only filesystem. На стенде логи смотрят глазами и не копят.
Бэкап: снимок volume плюс тег образа плюс версия хоста. Версионирование проекта ПЛК не заменяется тегом Docker. В Git - исходник. На объекте должен быть акт: какой тег образа, какой commit проекта, какой checksum volume retain. Иначе «откатили контейнер» откатывает бинарник, но не состояние.
Когда контейнер на IPC уместен
Сценарии, где это уже не игрушка:
FAT и постоянный стенд с тем же compose, что на линии, для регресса; агрегация, MQTT/OPC-шлюз, расчёт KPI, не цикл 5 мс; некритичный контур учёта, вентиляции склада, мониторинга энергии; пилотный алгоритм рядом с основным аппаратным ПЛК, с возможностью выключить контейнер рубильником; тиражирование одинаковых узлов, где ценность - одинаковый образ, а не экономия на контроллере.
Даже здесь хост - изделие: промышленный IPC, паспорт, температура, питание 24 В, watchdog железа, который перезапускает хост, если процесс умер. Watchdog Docker healthcheck не заменяет аппаратный.
Когда оставляют обычный контроллер
Аппаратный ПЛК остаётся правильным выбором, когда отказ и джиттер стоят партии или безопасности.
Жёсткий цикл у привода и локальная корзина I/O. Контур, который должен пережить смерть серверной и жить в шкафу у насоса. Функциональная безопасность: контейнер на общем IPC не становится SIL «потому что runtime умеет». Эксплуатация без Linux-администратора: замена блока по ЗИП понятнее, чем podman rollback в три часа. Среда, где IT обязана патчить хост чаще, чем технология допускает останов. Проект, который уже на пределе технического долга legacy: перенос в контейнер не лечит кривую логику, только добавляет слой.
Первый старт среды на железе контроллера и документация интерфейсов - отдельная дисциплина; для CODESYS она собрана в маршруте первого запуска. Тот же проект в контейнере на IPC требует ещё паспорт хоста. MasterSCADA 4D на том же IPC - тот же паспорт хоста, другая лицензия точек, не «более высокий уровень».
Таблица: фактор на объекте, контейнер и проверка до пуска
| Фактор на объекте | Контейнер ок | Контейнер не ок | Что проверить до пуска |
|---|---|---|---|
| Требуемый цикл и джиттер | >50–100 мс, запас CPU | Жёсткие 1–20 мс у механизма | Максимум цикла 1 ч под нагрузкой, соседи по CPU |
| I/O | Ethernet, стабильный NIC | USB-RS-485 «как получится» | Имена устройств по id, рестарт питания, обмен после udev |
| Обновления хоста | Заморозка + стенд-клон | Автопатч, EDR без исключений | Сценарий патча на клоне, откат kernel, повтор FAT цикла |
| NTP | Хост синхронен до старта runtime | Часы VM, второй NTP в образе | Отключение питания, метки первых событий |
| Сеть контейнера | Выделенный NIC / осмысленный host | NAT на пути цикла, чужой bridge | Latency, MAC/лицензия после recreate |
| Retain и лицензии | Volume + стабильный uid | Запись в слой образа | Recreate контейнера, проверка retain и лицензии |
| Соседи на IPC | Запрещены на ядрах runtime | Grafana, агенты, «ещё Python» | Суммарный CPU/IRQ, рестарт соседа во время цикла |
| Эксплуатация | Регламент образа и отката | «Разберётся подрядчик удалённо» | Инструкция смены, ЗИП IPC, кто имеет право docker |
| ПАЗ / SIL | Нет, рядом отдельный контур | Runtime в контейнере как ПАЗ | Граница ответственности в ТЗ |
| Температура шкафа | IPC в паспорте, запас | Офисный ПК в шкафу | Тепловой режим, троттлинг CPU |
Приёмка: стенд имеет право врать, FAT - нет
FAT и SAT для контейнерного runtime должны включать хост как изделие. Не только сценарии технолога.
Полный freeze хоста: версия ядра, docker/podman, образа, compose, checksum volume. Рестарт питания шкафа, не docker restart. Убийство процесса runtime без убийства хоста: watchdog, время восстановления, состояние выходов. Рестарт соседнего контейнера и docker pull запрещённый на линии - попытка, которая должна не пройти без окна. Смена USB/кабеля Ethernet и возврат. Патч на клоне с протоколом джиттера до/после. Откат на предыдущий тег образа силами того, кто будет дежурить, не автора Dockerfile.
Если подрядчик говорит «на стенде же работало» без этих пунктов - он сдаёт демо, не систему.
Типовые ошибки
Сдать стендовый compose как боевой. На линии появляются квоты CPU, bridge и агент ИБ.
Privileged и --network host «пока наладка». Наладка заканчивается, флаги остаются, граница контейнера фиктивна.
ttyUSB0 в конфиге. После первого силового цикла опрос смотрит не туда.
Retain в слое образа. Recreate обнуляет контур.
Автообновление хоста. Ядро уехало - realtime и NIC не поднялись.
Второй контейнер «он же почти ничего не ест». Ест IRQ и диск в худший момент партии.
Нет образа на площадке. Интернет на ОТ закрыли, откатить нечем.
Путать среды. «CODESYS - это ПЛК, MasterSCADA - это SCADA, в контейнер кладём только нижний уровень». На IPC обе среды - равноправные runtime проекта; в контейнер кладут выбранный runtime, а не «уровень».
Вопросы при работе
Docker или Podman?
Важно не логотип, а версия, политика rootless, registry на площадке и то, кто умеет откатывать в смену. Для realtime чаще проще предсказуемый Docker/cri с замороженными пакетами, чем «что поставил IT». Выбор фиксируют в паспорте хоста.
Можно ли оркестрировать Kubernetes?
Для цикла ПЛК на среднем заводе обычно нет: лишние демоны, лишние рестарты, лишние руки. Compose/systemd хватает. K8s оставляют IT-контуру, не такту клапана.
Что делать, если лицензия слетает после рестарта?
Искать, что изменилось: MAC, machine-id, путь volume, uid. Стабилизировать идентификатор хоста до пуска, не «привязать заново каждую смену».
Смена имеет право делать docker restart?
Только по карточке аварии, как reset контроллера: с записью в журнале, с пониманием retain и времени цикла на подъёме. Без карточки - звонок тому, кто в акте SAT.
Когда остановить эксперимент и поставить ПЛК?
Когда джиттер не лечится изоляцией ядер, когда USB остаётся единственным полем, когда патч-политика IT несовместима с окном технологии, когда нет человека на откат образа.
На практике
Если контур должен жить в шкафу как изделие с экраном, корзиной модулей и заменой блока по ЗИП, промышленный контроллер с Linux RT остаётся прямой альтернативой IPC с контейнером. В линейке СТАБУР runtime заказывают как CODESYS 3.5 или MasterSCADA 4D на одном железе - без иерархии «ПЛК против SCADA». Контейнер на чужом IPC эту развилку не упрощает: он добавляет слой хоста, который нужно паспортизовать отдельно.
Обсуждение