仓储控制系统(WCS)软件可靠性设计
仓储控制系统要求高可靠性
仓储控制系统(WCS)是企业物流输送系统的关键核心,不论是生产线边库还是销售成品库,业务中断都将带来直接经济损失,因此其可靠性(Reliability)要求极高。
可靠性(Reliability)定义为软件在规定时间内和特定的环境中无错运行的概率,可靠性是工业自动化控制软件系统排在首位的非功能性需求。可靠性主要包含如下 3 个方面:
成熟:经过严格的测试,不存在缺陷,故障率很低;
容错:难免发生故障的时候,仍能维持规定级别的能力;
易恢复:难免发生故障后,具备自动恢复或者快速恢复的能力。
更直接的说,要求 WCS 能够 7*24 小时长期无故障运行!软件本身的成熟度要依靠软件工程、软件测试以及项目实践的积累,但在设计的前期就要充分考虑容错性和易恢复性这两个至关重要的软件需求。那么哪些情况会经常导致 WCS 中断工作呢?比如断电、网络中断、PLC 端故障报警、作业加入队列失败等等。
每设备独立线程故障隔离
仓储控制系统调度多种自动化设备进行作业,如堆垛机、RGV 等,最直接的设计思路是每设备由单独的线程独立控制,做到故障隔离,一旦某一台设备出现硬件、机械故障,不至于影响其他设备正常工作。
每种设备有不同的指令格式、控制逻辑和状态监控数据,实现如上图所示的设备驱动逻辑简单清晰。而 WCS 负责接受外部系统的作业任务,负责协调、分配任务给这些设备驱动(Driver)线程,每个设备驱动线程再将作业任务翻译为指令发送给 PLC,并实时监控设备状态、任务执行状态,通过事件的方式上报 WCS 以便协调处理。
这种多线程模型不仅使得你有可能最优化的使用设备,提高设备利用率,而且可以做到设备间故障隔离,不会因为单台设备导致整个系统瘫痪。
每设备独立线程持续作业
WCS 一旦启动,就会启动所有设备控制线程,这些独立的设备控制线程必须永不停歇的工作。然而,很有可能设备报警、网络连接中断或者当 WCS 将作业分配给设备驱动线程时失败、甚至线程出现不可预测的异常等导致线程异常退出。
如何保证设备驱动线程在各种异常情况下仍能正常工作?
分析一下出库作业的场景:
WCS 收到一个出库任务,要确保接收成功,应将任务成功序列化后(例如保存至数据库)才返回给外部系统成功的消息;
WCS 查询各设备的工作状态和作业队列,选择最优的设备组合来执行出库任务,分配作业任务时为每个设备创建独立的作业,同样应将作业序列化成功后再交给设备驱动线程执行;
设备驱动线程持有一个作业队列,当查询到设备空闲,从队列种取出一个作业,翻译为 PLC 理解的指令数据(字节数组),通过 Socket 等各种通讯方式发送给 PLC。若发生网络断开,必须有断线重连机制。对发送的指令数据需有校验确实成功的机制,因为就算工业以太网也有可能发生丢包的现象;
设备驱动线程不断轮询 PLC 中的作业执行状态,记录设备执行作业任务的全部过程记录,完成后重复 3;
当作业队列中没有任务时,尝试检查数据库中是否有等待的作业任务,若有则载入至作业队列,这个设计是为了加强可靠性;
作业任务序列化后再传递
可以看出,在作业任务传递的过程,必须在序列化成功的前提下再执行线程间的消息传递,若序列化和传递消息采用了原子性事物,则 5 中检查数据库中是否有等待的作业任务可以省略。
用伪代码描述如下:
将作业任务存储至数据库的过程可以做到高可靠性,要么成功要么失败,失败后通过重试(Retry)机制确保成功。
网络断线重连机制
工业场景中使用高可靠性的工业以太网,底层通讯通常都是有线连接,采用 TCP 协议。但我们在实际项目中曾经遇到各种情况导致网络中断。例如网口松了、工厂断电、下班后关了 PLC 电柜但没关服务器,都会导致 WCS 中设备驱动线程的 Socket 连接被断开。
网络被断开后,必须能自动重连,使得 WCS 具备自恢复性。要实现自动重连机制,关键的是能检测出 Socket 被远端断开了,可是很多人并不了解如何实现。例如顶顶大名的西门子 PLC 通讯的开源程序库s7netplus的源代码中检测是否连接(IsConnected)的函数是这样的(2020.6.14):
很明显 TODO 写了注释,它并没有真正实现。TcpClient.Connected 属性是获取截止到最后一次 I/O 操作时的 Client 套接字的连接状态,若连接被远端断开,这个属性并不会动态改变而是一直为 True。特别是如果使用了 NetworkStream.Read 方法读取数据时在远端关闭连接时会抛异常(IOException),而且会将线程阻塞(完蛋的节奏)。
如果不想陷入阻塞状态,用Socket.Poll的方法检测 Socket 是否连接,但是这种实现方法也不能检测出远端断开的情况(没用的节奏)。
正如 s7netplus 项目源代码中注释中提到的,检测 Socket 是否真正处于连接的状态,要通过发送一个空的字节来实现,具体实现可参考这篇博客。这种方法经现场调试是可行的,据说.Net Core 已经修正了这个问题,尚待验证。
数据发送后作读回校验
给 PLC 发送指令数据前,总是要检查网络是否处于连接状态,发送失败后需有重试机制。
数据发送至 PLC 后,仅仅通过 Socket 返回的成功状态就断定发送成功了不是 100%可靠的。为了防止数据丢包,应读回后作比较,确保发送的数据 PLC 端完整收到了。
线程重启恢复现场
不论何种情形,线程重启应从 PLC 和数据库读回设备和作业状态,恢复现场。
例如:你重启设备驱动线程时,堆垛机干完了最后一个作业任务,此时 StackerDriver 因为重启没有读取到作业完成的信号。但是,如果在线程启动时重新载入作业队列,从设备读取最后的现场数据和状态,仍然能补救处理作业的完成逻辑(比如写入数据库 job.status=FINISHED),然后就能自动取出下一个作业,继续执行。
多线程引入的复杂性
每设备独立线程带来故障隔离的益处,但也使得 WCS 成为复杂的多线程系统,引入了复杂性,代码编写、现场调试都更困难。
首先要保证每个独立的设备驱动线程始终处于工作中,若出现异常退出,WCS 需能自动恢复。因此要求设备驱动线程具备状态查询、健康检查接口,WCS 可监控每个设备驱动线程的状态。
设备驱动中的状态、作业队列以及当前作业数据等需支持多线程访问,因为一方面 WCS 分配作业任务时会从上至下改变设备驱动线程中的数据,另一方面设备驱动线程轮询 PLC 后会修改当前作业的执行状态。
例如,作业队列在一个单线程的环境下,你可能用一个 List 数据结构就实现的很好,可是当这个 List 一边要添加作业(Add)一边要取出(Remove)的时候,你就要通过各种多线程锁机制实现你的设备驱动(Driver)了。
不同的设备有可能底层使用同一块 PLC 来控制,访问 PLC 数据使用的是同一个 IP 地址,端口相同的情况下,多个线程同时收发是不允许的,这时就需要同步锁。
某些情形需要与外部系统集成,通过数据库共享表的方式,定时查询是否有新任务,通常会采用定时器,而定时器是另一个单独的线程,一不小心就会因为多线程问题出现很奇怪的异常和软件缺陷。
不论是.Net 还是 Java,虽然都有很好的异步编程框架,但是要小心。
服务器容灾措施
此外,参照软件系统可靠性设计,通常服务器应有双机热备等容灾措施,以防硬件损坏后快速恢复系统。
版权声明: 本文为 InfoQ 作者【阿喜伯】的原创文章。
原文链接:【http://xie.infoq.cn/article/a56709a199303a1cdbee1123d】。文章转载请联系作者。
评论