\chapter{分布式消息网络设计} \section{概述} 现代软件应用程序很少孤立存在,而现在通常的做法是依赖服务或使用远程实体提供的信息。在这种分布式架构中,集成是关键。近年来,消息传递是解决分布式性质挑战的参考解决方案,例如网络不可靠性,生产者和消费者的强烈耦合以及应用的异质性。由于强大的社区以及对标准和整合的共同努力,消息代理如今已成为许多项目和服务的传输层构建块,近年来出现了很多的消息服务,如MQTT,RabbitMQ,RocketMQ, Kafka等. 这些消息框架现在已经发展的非常成熟,但是不太适合本文的使用场景,单纯工控用的PLC通信又十分的僵硬,适合构建信息物理系统(CPS)使用,涉及到大量设备、服务、人和虚拟实体四方消息互动目前来看没有适合的通信方案,在这四方之间消息传递的需求和性能要求皆不同,所以尝试去设计一套新的消息网络去解决数字孪生下的数据困境。 \section{通讯机制} 下图为具体某一集群内的消息节点图,在同一子网下的client节点上线时会进行广播,发现内网节点后进行选举,产生一个子网内核心节点,其余节点连接该节点,核心节点在向上连接,最终链接到核心服务器集群,服务器集群也会选举产生一个核心节点,这个节点为整个网络的核心节点,用来维护节点状态并向下同步状态,整体网络中任意节点下线,皆会触发重新选举或状态更新,核心节点因故障下线时,会导致当前层次子网重新选举,中断服务1s,但是不影响其余层次网络集群功能,最终上线后会同步节点状态,实现整体网络的最终一致性。 对于任意节点来说,其上级节点其实与下级节点一致,在功能逻辑上存在上下级,但是在消息分发上不存在上级,连接的所有节点都是下级或者说同级,会维护一套消息表,记录消息标记和需要发往的目标节点。如节点A刚开始上线时该表为空,当A需要订阅消息时,就广播给周边的节点; B 收到记录下消息类型和目标A以及路径长度1, 在广播给除了A以外的临近节点,同时路径长度加1;每个节点收到同步消息时根据表内数据,没有则添加,有则根据路径长度比较,短则替换目标,长则抛弃。这样,任意节点订阅消息时将会在整个消息网络广播其订阅记录,并在每个节点记录的是最短传播路径。当节点收到消息时,根据消息表内记录,存在标记则转发,不存在则抛弃。 \begin{figure}[h!] \centering \includegraphics[width=0.8\textwidth]{figure/f3.png} \caption{通讯网络} \label{fig-f3} \end{figure} \section{通讯协议} \begin{table} \centering \caption{通讯帧} \begin{tabular}{|l|l|l|l|l|l|l|l|} \hline \multicolumn{4}{|c|}{prefix} & \multicolumn{4}{c|}{count} \\ \hline typ & \multicolumn{3}{c|}{tag} & \multicolumn{4}{c|}{source} \\ \hline \multicolumn{4}{|c|}{target} & \multicolumn{4}{c|}{data} \\ \hline \multicolumn{8}{|c|}{data} \\ \multicolumn{8}{|c|}{....} \\ \multicolumn{8}{|c|}{data} \\ \hline \end{tabular} \label{tab-col} \end{table} \begin{itemize} \item prefix(8bit): 帧前缀,用于判断消息帧的开始 \item count(8bit): 消息长度 \item typ(4bit): 消息类型 \item tag(12bit): 消息标记 \item source(8bit): 消息来源节点 \item target(8bit): 消息目标节点 \item payload: 搭载的数据 \end{itemize} \section{路由算法} 在订阅消息时存在父子订阅需求,对于很多节点来说可能需要订阅的是某个具体的消息,对于某些规则类节点或者虚拟节点来说,订阅的是某个类别的消息,所以设计树状订阅机制,其订阅子树仅会收到子树消息,订阅父节点则会收到下属所有子树消息,这样在进行消息分发时不能简单的通过判断标记想到与否转发,所以采用了rie树算法加速消息分发。 rie树又被称为前缀树、字典树是一种用于快速检索的多叉树结构。字典树把字符串看成字符序列,根据字符串中字符序列的先后顺序构造从上到下的树结构,树结构中的每一条边都对应着一个字符。字典树上存储的字符串被视为从根节点到某个节点之间的一条路径,并在终点节点上做个标记"该节点对应词语的结尾",正因为有终点节点的存在,字典树不仅可以实现简单的存储字符串,还可以实现字符串的映射,只需要将相对应的值悬挂在终点节点上即可。 Trie的核心思想是空间换时间,有如下基本性质: \begin{itemize} \item - 根节点不包含字符,除根节点外每一个节点都只包含一个字符 \item - 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串 \item - 每个节点的所有子节点包含的字符都不相同 \end{itemize} 字典树能够利用字符串中的公共前缀,这样可能会节省内存,利用字符串的公共前缀可以减少查询字符串的时间,能够最大限度的减少无谓的字符串比较,同时在查询的过程中不需要预知待查询字符串的长度,沿着字典树的边进行匹配,查询效率比较高。 \section{分布式选举协议} \begin{figure}[h!] \centering \includegraphics[width=0.8\textwidth]{figure/f2.png} \caption{选举协议} \label{fig-f2} \end{figure} 每个节点上线时如果有现有的消息网络会直接连入现有的消息网络,当大量节点同时上线或者子网间关键节点下线时会触发节点选举,选举产生一位核心节点代表他们和其他集群进行消息通信,并在这些核心节点中再次选举产生一个核心节点,这样所有的节点会自动的根据网络环境分层出消息层,并在消息传递之间保持最小路径 在分布式选举协议中,一个节点任一时刻处于以下三个状态之一: \begin{itemize} \item 从动 \item 候选者 \item 主控 \end{itemize} 如上图所示,可以看出所有节点启动时都是从动状态;在一段时间内如果没有收到来自主控的心跳,从从动切换到候选者,发起选举;如果收到大多数的同意票(含自己的一票)则切换到主控状态;如果发现其他节点比自己更新,则主动切换到从动。 总之,系统中最多只有一个主控节点,如果在一段时间里发现没有主控,则大家通过选举-投票选出主控。主控会不停的给从动发心跳消息,表明自己的存活状态。如果主控节点故障,那么从动节点会转换成候选状态,重新选出主控。 \section{硬实时通信} 在工业控制领域实时(Real Time)是一个核心要求。实时系统是指计算的正确性不仅依赖于逻辑的正确性而且依赖于产生结果的时间,如果系统的时间限制不能得到满足,系统将会产生故障。在工业领域这种故障可能造成灾难性的结果。 在CPU资源调度方面,OS主要提供一个多任务(multitasking)的运行环境,以方便应用的开发。在开发某个应用时首先把工作拆解成多个任务(Task/Thread),每个任务都可以简化成一个简单的无限循环: \begin{lstlisting}[ language={C}, caption={基本调度过程}, label={code-c-sample}, ] void MyTask (void) { while (1) { Wait for an event to occur; Perform task operation; } } \end{lstlisting} 如上面代码所示,任务(Task)都是等待event,然后处理事务。任何一个任务得以运行,都是因为它收到了一个Event,这个Event可能是一个中断、也可能是超时到期、还有可能是其他任务发出的IPC信号,继续追查发出IPC信号的任务最后的源头Event肯定是一个外部设备硬件中断或者是内部的Timer中断。中断引起了Event传递,形成了逐个运行多个任务的链条(Chain)。一个系统内部会存在很多条这种链条。 对实时(Real Time)系统来说,不仅仅要求OS能提供多任务环境,更要求任务能在极短的时间之内响应外部的中断事件。 对于终端节点来说硬实时通讯较好实现,利用时钟定时中断即可强制切换到高级别消息发送,但是终端节点一般作为信息发送和执行节点,不具备逻辑处理功能,在整体通讯延迟受最慢一级也就是逻辑处理层一般也是主机节点影响最大,为处理此问题,对消息进行分级,高优先级消息会先触发中断响应,并切换到执行状态,且不可被抢占。 \section{终端通信} \section{性能分析} 以下性能测试皆使用 2.4 GHz 八核Intel Core i9 作为测试芯片,并进行相关内核性能调优后得到的测试结果。 \begin{figure}[h!] \centering \includegraphics[width=0.8\textwidth]{figure/f4.png} \caption{消息编码速率分布} \label{fig-f4} \end{figure} \begin{figure}[h!] \centering \includegraphics[width=0.8\textwidth]{figure/f5.png} \caption{消息编码速率拟合曲线} \label{fig-f5} \end{figure} \begin{table} \centering \caption{消息编码} \label{tab-three-line-table-example} \begin{tabular}{cccc} \toprule 编码时间 & 下限 & 估值 & 上限 \\ \midrule 回归斜率 & 296.52 ns & 298.26 ns & 300.04 ns \\ 拟合度 & 0.9228055 & 0.9263008 & 0.9225949 \\ 均值 & 295.37 ns & 297.35 ns & 299.41ns \\ 样本标准偏差 & 8.6420 ns & 10.325 ns & 11.813 ns \\ 中值 & 293.45 ns & 295.80 ns & 297.40 ns\\ 绝对中位差 & 6.1489 ns & 8.4741 ns & 11.089 ns\\ \bottomrule \end{tabular} \end{table} 由图4和表格可知,消息分发网络中消息编码时间在300ns左右,设备负载对消息编码速率分布有较大影响. 针对消息分发速率,使用wrk 工具对系统进行压力测试,得到图5 \begin{figure}[h!] \centering \includegraphics[width=0.8\textwidth]{figure/f6.png} \caption{消息分发速率压测结果} \label{fig-f6} \end{figure} 由图6可以看出,消息分发在未绑定任何响应函数的情况下单机可以达到每秒157651条,在未读取数据库情况下消息分发延迟6.33ms。