高可用冗余服务
概述
高可用 (HA) 指在硬件或软件出现故障时, 系统能在秒级乃至毫秒级内自动接管, 对外表现为持续可用。 DarraRT 高可用有两个层面:
- EtherCAT 冗余 (Ring / Cable 冗余) — 参见 PLC 冗余, 针对网络链路
- 控制器冗余 (本章) — 针对 Service / PLC Runtime 本身, 主备双机或 1+N
本章聚焦控制器冗余: 主备机器通过心跳监测对方健康, 故障时 Standby 激活 I/O 并继承状态, 确保产线不停产。
适用场景
| 场景 | 冗余模型 | 切换时间要求 |
|---|---|---|
| 关键工艺 (化工/半导体) | 1+1 Hot Standby | <1 s |
| 一般产线 | 1+1 Warm Standby | <5 s |
| 大型多机系统 | 1+N Cold Pool | <30 s |
| 测试 / 辅助线 | 无冗余 | — |
冗余模型对比
┌── 1+1 Hot Standby ───────────────┐
│ 主 ←心跳→ 备 │
│ 主: Active I/O, RUN │
│ 备: 同步状态, 不出 I/O │
│ 故障: 备接管 <1s │
│ 场景: 化工/关键工位 │
└────────────────────────────────────┘
┌── 1+1 Warm Standby ──────────────┐
│ 主 ←心跳+状态同步→ 备 │
│ 主: Active I/O │
│ 备: 不运行扫描, 只存状态 │
│ 故障: 备冷启动+载入状态 <5s │
│ 场景: 一般产线 │
└────────────────────────────────────┘
┌── 1+N Cold Pool ─────────────────┐
│ 主 ←广播心跳→ 备池 (N 台) │
│ 一主, N 备冷备 │
│ 故障: 管理器调度备池一台启动 │
│ 场景: 大型系统 / 云化 │
└────────────────────────────────────┘
架构图 (1+1 Hot Standby)
┌── 仲裁网络 ──┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Primary (A) │ │ Secondary (B) │
│ DarraRT Service │◄─10Hz─┤ DarraRT Service │
│ PLC Runtime RUN │ 心跳 │ PLC Runtime HOT │
│ I/O Active │ │ I/O Passive │
└──────┬───────────┘ └──────┬───────────┘
│ │
│ RETAIN/PERSISTENT 状态同步 │
├──────────100Hz───────────┤
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ 双口交换机 / EtherCAT Junction │
│ 两机同时连接, 仅 Active 实际驱动 │
└──────────────────────────────────────────┘
│
▼
EtherCAT 从站网络
前置条件
- 两台物理机硬件一致 (网卡型号、CPU 频率)
- 内核版本、驱动版本一致 (签名驱动来自同一份包)
- 仲裁网络独立 (不走生产 VLAN)
- 有共享存储 (仲裁磁盘) 或第三方仲裁者 (quorum node)
详细步骤
1. 心跳协议
协议: UDP 多播 239.100.0.1:18888, 10 Hz (100 ms 周期), 3 次丢失 (300 ms) 判定宕机。
// Darra.PLC.Service / HA / Heartbeat.cs
public sealed class Heartbeat
{
private const int PeriodMs = 100;
private const int MissThreshold = 3;
public async Task RunAsync(CancellationToken ct)
{
var send = Task.Run(() => SendLoopAsync(ct));
var recv = Task.Run(() => RecvLoopAsync(ct));
await Task.WhenAll(send, recv);
}
private async Task SendLoopAsync(CancellationToken ct)
{
using var udp = new UdpClient();
while (!ct.IsCancellationRequested)
{
var pkt = new HeartbeatPacket {
NodeId = _nodeId,
Role = _currentRole, // Primary / Secondary
SeqNo = ++_seq,
Epoch = _epoch, // 状态版本号
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Health = CollectHealth() // CPU/mem/scan_jitter
};
var bytes = pkt.Serialize();
await udp.SendAsync(bytes, bytes.Length,
new IPEndPoint(IPAddress.Parse("239.100.0.1"), 18888));
await Task.Delay(PeriodMs, ct);
}
}
// RecvLoop: 收到就重置丢失计数, 3 次丢失 → 触发 Failover
}
2. 脑裂防护
两机都认为自己是 Primary = 脑裂, I/O 冲突。三种方案:
| 方案 | 原理 | 适用 |
|---|---|---|
| STONITH | Shoot The Other Node In The Head: 切换时 fencing 关掉对方电源 / 强制重启 | 有 IPMI 的服务器 |
| Quorum | 引入第三个仲裁节点, 2/3 多数决 | 有云 / 第三台机 |
| 仲裁磁盘 | 共享磁盘, 谁先抢到锁谁是主 | 有 SAN / iSCSI |
DarraRT 默认启用 Quorum, 无 quorum 可用时要求用户手动确认切换。
3. 状态同步
需要同步的状态:
| 类别 | 存储 | 同步方式 |
|---|---|---|
| RETAIN 变量 | PLC 内存 | 实时增量 (delta) |
| PERSISTENT 变量 | 磁盘 + 内存 | 每次写入双写 |
| 定时器 / 计数器 | PLC 内存 | 10Hz 全量 |
| 功能块实例变量 | PLC 内存 | 10Hz 全量 |
| 运行时配置 | 磁盘 | 变更事件驱动 |
| OEE / SPC 历史 | 外部数据库 | 数据库自己冗余 |
增量 Delta 同步协议:
主机: OB1 执行完 → 扫描 变量快照 vs 上次同步快照
有差异 → 打包成 DeltaPacket { epoch, seq, changes[] }
走独立 TCP 通道 (9.9.9.1:18001) 发给备机
备机: 接收 DeltaPacket → 校验 epoch 连续性
epoch 跳跃 → 请求全量同步 (StateDump)
否则直接应用到本地镜像内存
同步方式对比:
| 方式 | 延迟 | 带宽 | 一致性 |
|---|---|---|---|
| 主动 Push | 低 | 取决变更频率 | 强 |
| 被动 Pull | 高 | 稳定 | 最终 |
| 增量 Delta | 中 | 省 | 强 (要求 FIFO) |
4. Failover 切换流程
[T0] Primary 心跳丢失 3 次 (300ms)
Secondary 等待 Quorum 确认 (再 +100ms)
[T1] Quorum 确认 → Secondary 触发 Promote
[T2] Secondary STONITH Primary (或标记 fenced)
[T3] Secondary 从 Delta 镜像恢复最后一致状态
- 加载 RETAIN / PERSISTENT
- 重置 Timer / Counter
- FB 实例变量就位
[T4] Secondary 重载 EtherCAT 配置, 激活 I/O
[T5] PLC Runtime RUN
[T6] 发布 NewPrimary 通告, epoch++
总耗时: ~500-800 ms (Hot Standby)
5. 切换后处理
| 动作 | 实施 |
|---|---|
| 通知上位机 | 广播 ha.failover 事件, HMI 自动切后端 |
| 数据对账 | 对历史库做去重 (可能有双写) |
| 原 Primary 恢复后 | 作为新 Secondary 回归, 不自动抢回 |
| 人工复位 | IDE 工具栏"主备切换"可手动强制 |
6. 配置
# <Service>/config/ha.yaml
ha:
enabled: true
role: primary # primary | secondary | auto
node_id: node-a
peer_node_id: node-b
heartbeat:
multicast: 239.100.0.1
port: 18888
interval_ms: 100
miss_threshold: 3
sync:
tcp_port: 18001
mode: delta # delta | full
full_sync_interval_s: 600 # 每 10 分钟强制全量校验
quorum:
type: node # node | disk | none
nodes:
- 10.0.0.10:18080
- 10.0.0.11:18080
fencing:
type: ipmi
peer_ipmi: 10.0.1.2
username: admin
password: ${IPMI_PASS}
failover:
auto: true
min_down_ms: 300
max_wait_ms: 1000
7. 与 EtherCAT 冗余配合
DarraRT 独有设计: 控制器冗余 + EtherCAT Ring 冗余双层叠加。
- 每台控制器都有两块网卡 (A/B) 走 Ring
- 主机的 A/B 连环, 网线断一根 → EtherCAT Ring 冗余补偿, 控制器层不切换
- 控制器整机宕机 → HA 切换到备机, 备机的 A/B 已经预连接 Ring
参考 PLC 冗余 了解 EtherCAT Ring。
参数表
| 参数 | 默认 | 调优 |
|---|---|---|
| 心跳周期 | 100 ms | 10-500 ms, 越小越快但耗 CPU |
| 失效阈值 | 3 | 2-10 次 |
| 同步 TCP 端口 | 18001 | 避开业务端口 |
| 全量校验间隔 | 600 s | 防止 Delta 长期累积错误 |
| Quorum 超时 | 1000 ms | 按网络 RTT 设 |
排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 频繁切换 (flapping) | 心跳网络不稳 | 独立网, 升级交换机 |
| 脑裂 | Quorum 未配置 | 启用 Quorum 或仲裁盘 |
| 切换后状态错 | RETAIN 没同步 | 查变量 Retain 属性 |
| 备机不接管 | 防火墙拦了多播 | 检查 iptables / Windows Firewall |
| Epoch 跳跃频繁 | 大量 PERSISTENT 写 | 改为批量异步 |
高级技巧
- 预热启动: Secondary 在 RUN 态但隔离 I/O, 切换可做到
<100ms - 灰度: 切换前先做 5% 流量双写验证再全切
- 业务无感回切: 原 Primary 恢复后作新 Secondary, 不自动 preempt
- 分片冗余: 一个大 PLC 程序拆多个子系统, 各子系统独立主备
- Chaos 演练: 每月主动拔一次主机网线, 验证切换正常