一、简介:为什么PLC需要容器化?

  • 传统痛点

    • 产线换型时,PLC程序需逐台烧录,停机2小时起。

    • 不同项目依赖冲突(库版本、内核模块),"这台能跑,那台报错"。

    • 现场调试=出差2周,改一行代码重新全量部署。

  • 容器化价值

    • 秒级换型:Docker镜像预装PLC运行时+业务逻辑, docker run 即启动。

    • 环境一致性:开发、测试、产线同一镜像,消除"在我机器正常"。

    • 弹性扩展:产线扩容时,新PLC节点自动拉取镜像注册到SCADA。

    • 回滚无忧:版本化镜像,出问题 docker rollback 秒级恢复。

  • 实时性挑战:容器引入cgroup、namespace开销,需针对性优化才能满足PLC周期任务(通常1-10ms)的确定性。

掌握实时Linux+容器化PLC部署,是工业4.0时代从"项目制"走向"产品化"的核心竞争力。


二、核心概念:6个关键词搞懂容器化实时PLC

关键词一句话PLC场景意义
OCI Runtime容器运行标准,runc/crun实现替换为支持实时调度的crun
cgroup v2统一资源控制接口精确限制CPU/内存,避免PLC任务被饿死
CPU Set绑核隔离PLC容器独占物理核,消除上下文切换抖动
PREEMPT_RT实时内核补丁容器内线程也能获得<50μs调度延迟
Device PluginK8s设备管理扩展将PLC专用IO板卡(EtherCAT、Profinet)暴露给容器
镜像分层基础镜像+运行时+业务逻辑分层构建基础层复用,业务层快速迭代

三、环境准备:10分钟搭好"容器化PLC实验室"

3.1 硬件

组件规格说明
工控机/边缘网关x86_64,4核8GB起建议Intel Atom或ARM64工业板
实时IO模块EtherCAT从站×2用于验证容器内实时通信
网络千兆以太网容器镜像仓库访问

3.2 软件栈

层级组件版本安装命令
宿主机OSUbuntu 22.04 LTS + PREEMPT_RT 5.155.15.71-rt53见下文
容器引擎Docker CE + 实时crun23.0+见下文
PLC运行时CODESYS Runtime Linux3.5.19镜像内嵌
编排(可选)K3s轻量K8sv1.27多节点场景

3.3 一键安装实时内核+Docker(可复制)

#!/bin/bash
# install_rt_docker.sh
set -e

# 1. 安装PREEMPT_RT内核(已编译deb包)
wget https://example.com/linux-image-5.15.71-rt53_1_amd64.deb
sudo dpkg -i linux-image-*.deb
sudo update-grub

# 2. 重启后安装Docker
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

# 3. 安装实时crun(替换默认runc)
wget https://github.com/containers/crun/releases/download/1.8.4/crun-1.8.4-linux-amd64 -O /tmp/crun
sudo mv /tmp/crun /usr/local/bin/crun
sudo chmod +x /usr/local/bin/crun

# 4. 配置Docker使用crun
sudo mkdir -p /etc/docker
cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "default-runtime": "crun",
  "runtimes": {
    "crun": {
      "path": "/usr/local/bin/crun"
    }
  }
}
EOF
sudo systemctl restart docker

echo "安装完成,请重启选择RT内核"

3.4 验证环境

# 检查RT内核
uname -r | grep rt

# 检查Docker+crun
docker info | grep -i runtime
# 应输出: crun

# 测试实时性
docker run --rm -it --cap-add SYS_NICE --cpu-rt-runtime=950000 rt-tests cyclictest -p99 -i100 -d10s

四、应用场景:柔性产线的"分钟级换型"

某汽车零部件厂生产A/B/C三种型号,每种对应不同PLC控制逻辑。传统方式:换型时工程师携带笔记本,逐台连接PLC下载程序,平均耗时4小时。

容器化改造后

  1. 镜像预构建:A/B/C三种型号分别打包为 plc-app:a, plc-app:b, plc-app:c,内含CODESYS Runtime+设备描述文件+工艺参数。

  2. 产线MES下发换型指令"model": "B"

  3. 边缘网关执行

    docker stop plc-app && docker rm plc-app
    docker run -d --name plc-app --privileged \
      --cpuset-cpus="2,3" --cpu-rt-period=1000000 --cpu-rt-runtime=950000 \
      -v /dev:/dev -v plc-data:/data \
      registry.factory.local/plc-app:b
  4. 容器启动后自动:加载EtherCAT主站、扫描从站、同步SCADA注册点表。

  5. 全程耗时:90秒,其中Docker拉取镜像60秒(本地缓存后降至5秒)。

核心价值:换型不再依赖工程师现场,夜班无人化生产成为可能;新产线部署从"2周调试"压缩到"1天镜像验证"。


五、实际案例与步骤:从零构建实时PLC容器

5.1 步骤1:构建基础镜像(CODESYS Runtime)

# Dockerfile.base
FROM ubuntu:22.04

# 安装实时库依赖
RUN apt update && apt install -y \
    libcap2-bin libncurses5-dev libreadline-dev \
    ethercat-master libxml2-dev \
    && rm -rf /var/lib/apt/lists/*

# 安装CODESYS Runtime(假设已下载安装包)
COPY CODESYSControlLin64-3.5.19.deb /tmp/
RUN dpkg -i /tmp/CODESYSControlLin64-3.5.19.deb

# 创建非root用户(安全最佳实践)
RUN useradd -m -s /bin/bash plcuser
USER plcuser
WORKDIR /home/plcuser

# 暴露PLC通信端口
EXPOSE 1217 11740 11741 11743

ENTRYPOINT ["/opt/codesys/bin/codesyscontrol.bin"]

构建:

docker build -f Dockerfile.base -t plc-base:3.5.19 .

5.2 步骤2:构建业务镜像(型号A控制程序)

# Dockerfile.app-a
FROM plc-base:3.5.19

# 复制PLC程序(.app文件)和设备配置
COPY --chown=plcuser:plcuser models/A/ProductionLineA.app /home/plcuser/
COPY --chown=plcuser:plcuser models/A/io_config.xml /home/plcuser/

# 启动时加载指定程序
CMD ["--config=/home/plcuser/io_config.xml", "/home/plcuser/ProductionLineA.app"]

构建:

docker build -f Dockerfile.app-a -t plc-app:a .
docker tag plc-app:a registry.factory.local/plc-app:a
docker push registry.factory.local/plc-app:a

5.3 步骤3:实时运行容器

#!/bin/bash
# run_plc_realtime.sh
# 场景:启动型号A,绑核2-3,实时带宽95%

docker run -d \
  --name plc-app-a \
  --hostname plc-node-01 \
  --privileged \                           # 访问/dev设备
  --cap-add=SYS_NICE \                     # 设置实时优先级
  --cap-add=IPC_LOCK \                     # 锁定内存
  --security-opt seccomp=unconfined \      # 避免系统调用拦截开销
  --cpuset-cpus="2,3" \                    # 独占CPU 2和3
  --cpu-rt-period=1000000 \                # 1秒周期
  --cpu-rt-runtime=950000 \                # 950ms用于实时任务
  --memory=512m \                          # 限制内存
  --memory-swap=512m \                     # 禁用swap
  --shm-size=128m \                        # 共享内存给PLC
  -v /dev:/dev \                           # 直通所有设备(生产环境建议细化)
  -v /sys/bus/pci:/sys/bus/pci \           # PCI设备访问
  -v plc-data:/data \                      # 持久化数据卷
  --restart=unless-stopped \               # 自动重启策略
  registry.factory.local/plc-app:a

5.4 步骤4:验证实时性

# 进入容器
docker exec -it plc-app-a bash

# 查看PLC进程优先级
chrt -p $(pgrep codesyscontrol)
# 应显示: SCHED_FIFO, priority 99

# 运行cyclictest验证容器内延迟
cyclictest -p99 -i100 -d30s -n
# 期望: Max < 50μs

5.5 步骤5:K3s编排(多节点场景)

# plc-deployment.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: plc-app
spec:
  selector:
    matchLabels:
      app: plc
  template:
    metadata:
      labels:
        app: plc
    spec:
      hostNetwork: true          # PLC需要直接网络访问
      containers:
      - name: plc
        image: registry.factory.local/plc-app:a
        resources:
          limits:
            cpu: "2"
            memory: "512Mi"
        securityContext:
          privileged: true
          capabilities:
            add: ["SYS_NICE", "IPC_LOCK"]
        env:
        - name: PLC_MODEL
          valueFrom:
            configMapKeyRef:
              name: plc-config
              key: current_model
      nodeSelector:
        node-type: plc-gateway   # 只调度到PLC网关节点

应用:

kubectl apply -f plc-deployment.yaml
kubectl set env configmap/plc-config current_model=b  # 切换型号

六、常见问题与解答(FAQ)

问题现象解决
容器内cyclictest Max > 200μs未绑核或宿主机负载高检查 --cpuset-cpus,宿主机隔离CPU 0-1给系统,2-3给容器
EtherCAT主站扫描不到从站网卡直通失败使用 --network host 或 SR-IOV 直通物理网卡
PLC程序启动报"权限不足"容器内非root无法访问/dev细化设备映射 -v /dev/EtherCAT0:/dev/EtherCAT0,而非全量/dev
镜像拉取慢影响换型产线网络带宽不足部署本地Harbor仓库+镜像预热,或启用Docker Content Trust增量更新
K3s Pod频繁重启健康检查过于敏感调整livenessProbe: initialDelaySeconds: 60periodSeconds: 30
实时任务被cgroup throttlingCPU配额设置过紧使用 --cpu-rt-runtime 而非 --cpus,确保实时带宽

七、实践建议与最佳实践

7.1 镜像构建

  • 多阶段构建:编译依赖在builder阶段完成,最终镜像仅含运行时+程序。

  • 层缓存优化:将不常变动的CODESYS Runtime放在底层,业务逻辑放在顶层。

7.2 运行时优化

  • CPU拓扑感知:使用 --cpuset-mems 绑定NUMA节点,减少跨节点内存访问。

  • 禁用swap--memory-swap=--memory,防止PLC任务被换出。

  • ** hugepage**:PLC大数据缓冲区使用2MB/1GB大页,减少TLB miss。

7.3 安全加固

# 非特权方案(推荐生产环境)
docker run \
  --device=/dev/EtherCAT0 \
  --device=/dev/gpiochip0 \
  --group-add=$(getent group plc | cut -d: -f3) \
  --security-opt=no-new-privileges \
  plc-app:a

7.4 监控与可观测

  • Prometheus + Grafana:采集容器CPU throttling、内存使用、PLC周期抖动。

  • Loki:聚合PLC应用日志,关联SCADA报警。

7.5 CI/CD集成

# .gitlab-ci.yml
stages: [build, test, deploy]

build:
  script:
    - docker build -t plc-app:$CI_COMMIT_SHA .
    - docker push registry.factory.local/plc-app:$CI_COMMIT_SHA

deploy-staging:
  script:
    - kubectl set image daemonset/plc-app plc=registry.factory.local/plc-app:$CI_COMMIT_SHA
  only: [main]

八、总结:一张脑图带走全部要点

实时Linux PLC容器化
├─ 基础:PREEMPT_RT内核 + crun运行时
├─ 镜像:分层构建(基础Runtime + 业务逻辑)
├─ 运行:绑核 + 实时带宽 + 设备直通
├─ 编排:K3s DaemonSet + ConfigMap换型
├─ 优化:hugepage + 非特权 + 监控
└─ 价值:分钟级换型 + 弹性扩展 + 无人化运维

容器化不是"把PLC装进Docker"那么简单,而是从交付方式到运维模式的全面重构。掌握本文技术栈,你就能:

  • 让产线换型从"4小时"压缩到"90秒"

  • 让新产线部署从"2周出差"变成"1天镜像验证"

  • 让PLC程序像微服务一样版本化、可回滚、可观测

立刻动手构建你的第一个 plc-base 镜像,在实验室跑通cyclictest < 50μs,然后推向产线——工业4.0的容器化浪潮,等你入场!

更多推荐