何 锋,曾 文,王秉钧
酒泉卫星发射中心,甘肃 酒泉 732750
近年来随着国际信息安全形势问题的日益突出,各国都纷纷立足于开源软件平台,设计开发自主、可控的软件系统。致力于开发跨平台面向对象的Qt平台自诞生以来,经过开源社区的共同努力,已集成了各式各样的图形组件和丰富的基础库,实现了对Windows、IOS、Android、Unix Like 等操作系统的支持,已成为跨平台自主软件首选的开发工具。OpenMP 即Open Multiprocessing[1],是基于共享内存的一种便携式和可扩展的并行编程模型,包含编译制导语句、运行时函数库和环境变量三部分,由一组支持C++/Fortran 的应用程序接口(API)构成,不仅支持细粒度的for 循环展开并行,也支持SPMD[2]模式的线程级别并行。在目前的共享内存并行编程标准中,OpenMP是使用最为广泛和便捷的开发标准之一,Qt平台也提供了对OpenMP线程库的支持。
实时测控数据存储系统就是要把各类测控设备如光学、雷达、遥测、计算机等产生的实时测控数据存储起来,供事后数据分析使用。由于实时测控数据处理的复杂性,实时测控数据存储系统不但要应对多设备、高并发性、大数据量的数据特点,还要对异常数据有相应的处理策略,做到不缺不漏,实现高可靠性和强容错性。随着计算机硬件的不断发展,目前存储服务器大多都是单机多核环境[3],但是传统的串行程序逻辑不能充分利用多核的优势,计算负载大多集中在一个处理核心上,导致计算资源利用率低,同时也难以发挥存储系统后台RAID(Redundant Array of Independent Disks)阵列并行读写的优势[4],存储效率低,难以满足日益增长的强实时、大流量和可扩展的实时测控数据存储需求。需要设计实现一个规模可扩展、实时性高、并行能力强的并行实时测控数据存储系统。
实时测控数据处理系统为了保证系统的强实时性,应对一对多的传输要求,大多采用UDP组播协议,来减轻网络压力[5]。本文设计的系统基于Qt 图形界面使用OpenMP 实现SPMD 线程级并行。不同于事后数据处理每一个并行线程的数据是已知的持久化数据,实时测控系统并行线程的负载将对应每一个UDP Socket接收到的网络实时测控数据。
如图1所示,并行实时测控数据存储系统框架包含四个模块:进程初始化模块、线程初始化模块、数据解析模块、数据持久化模块。
进程初始化模块负责整个系统的任务初始化。创建的任务进程P,申请并初始化用于各并行线程间进行通信的共享缓冲区。该共享缓冲区可以被各个并行子线程访问,实现线程间数据通信。同时读取任务配置,根据配置度量系统的静态负载,并根据静态负载平衡算法进行并行任务划分。
线程初始化模块负责初始化使用OpenMP 制导语句Fork 出的各并行子线程。考虑到存储系统人机交互需要,本框架使用Qt 图形界面专门设置一个GUI(Graphical User Interface)线程T0来负责处理图形界面,完成内存占用率、多核CPU 的使用率,硬盘使用率等系统资源的监视、异常数据的显示、刷新和人机交互任务。T1至Tn是数据存储线程,具体个数由进程初始化模块中任务负载的划分情况来决定。每个数据存储线程在Fork 之后第一步会根据网络配置创建接收数据的socket套接字,并创建一个循环队列缓冲区用于缓存实时数据,借助于队列先进先出的特性保证网络接收数据的时序性。
图1 并行实时测控数据存储系统框架
数据解析模块负责UDP数据包的解析工作。为了及时发现测控数据的有效性,存储系统还需要对数据包做基本的异常性校验工作。各个数据存储线程响应并接收网络数据后,解析应用层协议包头并进行校验,如果为异常数据则首先写入共享通信缓冲区,再根据持久化的要求进行持久化。若为正常数据则直接根据持久化需求进行持久化操作。GUI线程T0会根据线程的同步策略去访问共享缓冲区的异常数据并在GUI 界面更新,为用户决策提供依据。
数据持久化模块负责数据的持久存储工作,目前存储系统大多使用RAID 阵列作为后台存储器。用户端会在虚拟磁盘控制器的指导下,根据控制指令完成数据库文件的读写工作。多个并行线程可以同时在不同的磁盘上进行读写,发挥RAID 阵列并行I/O 的优势。并行I/O 工作主要由RAID 的虚拟磁盘控制器完成,本文不再深入讨论。
基于Qt 和OpenMP 的并行实时测控数据存储系统要具备高可扩展性和强实时性,首先要解决任务划分问题。将网络数据按照流量大小均衡分割为一个个并行子域,然后交给各个存储线程并行地解析处理,防止单个线程负载过重引起的延迟甚至是数据丢失。其次是线程间通信的同步问题,本系统的线程间同步任务主要是T0和某个存储线程之间的共享缓存通信同步。某一时刻点至多关系到两个线程,而OpenMP本身提供的线程同步机制粒度过大[6],在处理本系统通信同步时容易导致全局线程阻塞产生较大延迟,需要设计一个细粒度的线程间同步算法。最后要利用Qt基于消息和槽机制的良好人机交互型特性,还需要将Qt 的特有的事件循环机制和OpenMP 线程有效结合,在OpenMP 线程中实现事件处理循环。
并行程序的性能本身受各线程之间的同步影响较大。如果单个线程的负载过重就会使其运行速度下降,导致和其同步线程在同步点长时间阻塞等待,进而使整个并行程序的运行速度大打折扣,降低并行效率。如果单个线程相比其他线程负载过轻,该线程就会过快地完成计算任务而在同步点等待,CPU 核长时间空闲,浪费计算资源。考虑到动态迁移带来的额外时间开销,本系统采用静态负载均衡思想来解决系统的任务划分问题。
根据系统框架图所示,抽象的数据存储过程模型如图2所示。测控设备将自身处理产生的测控数据以UDP数据报的方式发送进入网络,存储系统根据预先约定的网络地址和端口创建socket并响应数据接收,将数据放入缓冲队列,然后继续响应下一个网络数据包。处理模块从循环缓冲队列取出一包数据进行校验、存盘等解析工作。如果将测控设备作为顾客源,存储系统作为服务机构,该过程可以用排队系统[7-8]来表示。
图2 数据存储过程的抽象模型
排队系统包含三个要素:输入过程、排队规则、服务机构。根据网络设备基本特点本系统的输入过程和服务过程均可以使用负指数分布模型来刻画,由于使用循环队列缓冲区,有先进先出的特性,因此服务规则可以对应先来先服务(First Come First Served,FCFS)原则。这是一个典型M/M/1模型的排队系统[9-10]。
假设某一个测控设备的发送速率为λ,网络的传送过程没有数据丢包(网络可靠性不在本系统考虑范围),系统的服务时间即处理时间为Ts。本文主要关心的几个参数包括:
缓冲区长度Nq,排队系统进入稳态后,系统内长期驻留的数据包的个数,它确定在保证丢失率为可接收的数值范围内,应为每个线程分配的缓冲队列大小;输入速率λ,它是线程负载的直接度量,跟缓冲区大小有直接关系,必须确保系统能处理完数据,不产生丢失;处理时间Ts,在保证基本功能的前提下,服务时间越短越好。
M/M/1系统在稳态时平均队列的长度满足式(1)[11]:
其中,ρ为服务台的使用率,是平均到达率与平均服务率之比,即在相同时区内顾客到达的平均数与被服务的平均数之比,通过网络速率λ和服务速率μ定义,如式(2):
将式(2)带回式(1),就得到了本文关心的三个要素之间的制约关系,如式(3)所示:
其中,系统的处理时间Ts是可以预知的变量,每个线程要接收的网络数据速率λ和缓冲区队列的长度是可变变量。
考虑到计算核和内存的代价,本系统的静态负载均衡方法的主要步骤如下所示:
(1)预置一个缓冲队列的大小Nq0由式(3)确定每一个线程能承受的负载流量大小λ0。
(2)根据λ0和总流量M计算出需要的计算核个数,向上取整。
(3)比较C和服务器本来可用的CPU 总核数CN。若C≤CN,则划分方案可行,开始执行任务;若C>CN,则计算核数不足转到步骤(4)。
(4)在Nq0基础上增加整数增量D,即以Nq0+D的大小再次回到步骤(1),继续试探直至找到可行的划分方案为止。
基于排队论的静态负载均衡算法可以兼顾计算核心数、缓冲队列长度和网络流量三个要素的影响,在满足系统实时性、可靠性的前提下寻找到最适合目前服务器能力的任务静态划分方案,既能保证并行效率,又能提高资源利用率。
在本系统中,存储线程需要显示数据时必须先将数据写入对应共享通信缓冲区,然后由T0即GUI 线程读取共享数据,并在界面更新。在使用单缓冲区时,必须要实现共享缓冲区的“写后读”和“读后写”的同步才能保证读取数据的正确性[12],因此同步算法本身的实时性也是系统实时性能的瓶颈。OpenMP 本身提供的多线程同步方法为显示栅栏同步[13],这将会强制所有线程在共享缓冲区读写时同步等待,但本系统中存储线程之间并不需要协作,每次同步只有GUI线程和某一个存储线程,使用粗粒度的栅栏同步将会大大地降低系统的实时性。本文基于互斥锁设计了细粒度同步算法来实现本系统的共享缓冲区同步任务。如算法1 所示,以T0和存储线程Tm的同步为例,其主要步骤包括:
(1)主进程P初始化共享缓冲区和互斥锁队列。根据存储线程总数threads,主进程为每一个存储线程申请一块用于共享通信的缓冲区buffer[i],其中i为线程编号,0 <i<threads。同时为其定义两把互斥锁,W[i]用于“写后读”同步(下文简称写锁),保证每个共享缓冲区在写入数据后才能被读取;R[i]用于“读后写”同步(下文简称读锁),保证每个共享缓冲区的数据被读取结束后才能再次擦写。在任务开始时缓冲区可以用于写但是不能读,因此将所有线程的写锁初始为开锁状态,而所有读锁调用加锁操作Lock(W[i])初始化为加锁状态。
(2)存储线程Tm向共享缓冲区写数据。线程Tm需要显示接收的异常数据时,先申请自身对应的共享缓冲区buffer[m]的“读后写”即读锁R[m],若加锁成功表明该缓冲区内上一次的显示数据已经读取结束并允许重复擦写,那么就将本次的显示数据写入,并将本共享缓冲区的“写后读”即写锁W[m]解锁,允许GUI线程T0读取共享缓冲区的显示数据。否则就进入阻塞状态,等待T0完成读取操作释放缓冲区的读锁。
(3)GUI 线程T0从共享缓冲区读取数据并更新显示。由于各个存储线程的共享通信缓冲区相互独立,数据到达时间也相互独立。因此本文使用非阻塞加锁方法[14]Try_Lock()和循环轮询的方式逐个试探加锁。若加锁成功则读取显示数据,否则就跳过该线程的共享通信过程,对下一个线程的共享通信缓冲区进行试探加锁,如此反复,直至完成所有存储线程的共享数据显示任务。如此,GUI线程就不会因为某一个线程的共享缓冲区加锁不成功而发生阻塞,影响后续其他线程的界面更新显示任务。其他存储线程和Tm的同步过程类似,并行运行。
算法1OpenMP细粒度互斥锁同步算法
Input:线程Tm的显示数据和线程编号m,总的存储线程数threads
//主进程P设置共享缓冲区,初始化线程锁
BUFFERbuffer[threads];
LOCKW[threads],R[threads];
Fo(ri=0;i Lock(W[i]); End //线程Tm Begin //向共享缓冲区m写入显示数据 Lock(R[m]) WriteData(buffer[m]); Unlock(W[m]) … End 西班牙“Maria Canals国际音乐比赛”将于2019年3月23日至4月4日西班牙巴塞罗那举行。该比赛每年举行一次,年龄限制:17至28岁。比赛一等奖奖金为25,000欧元。比赛共分为四轮,预选轮:YOUTUBE;第一轮:独奏20分钟;第二轮:独奏40分钟;第三轮:独奏50分钟;决赛轮:协奏曲。比赛曲目与详情请关注网站。 //GUI线程T0 Begin //从共享缓冲区读取存储线程要显示数据 While(threads) I(fTry_Lock(W[threads])) ReadData(buffer[threads]); threads−−; Unlock(R[threads]) End if End while End Output:在图形界面显示Tm收到的异常数据 该算法能保证每次共享缓存通信过程中最多锁住T0和另一个存储线程,即使这两个线程间运行速度差异较大造成加锁阻塞,也不会引起其他线程的同步空等,保证了其他存储线程能正常地处理实时网络数据而不丢包,提高并行存储系统的可靠性和实时性。 本系统使用单独的GUI 线程来处理界面显示和刷新任务本身可以有效地缓解图形界面处理带来的资源消耗压力,但图形界面的主要目的是实现良好的人机交互,响应用户操作来控制其他存储线程记录的开始、停止等工作。基于Qt组件的图形界面使用特有的信号与槽通信机制来完成事件处理工作[15],而OpenMP线程本身并不支持事件处理工作。 OpenMP各个并行线程复制运行同一份程序代码,变量名称完全一致,但都是thread loacal局部变量,互相隔离不能访问。那么GUI 线程就不能直接和其他存储线程实现信号和槽的跨线程连接。本文通过在主进程中设置桥接对象的方式实现OpenMP 各线程隔离对象之间信号和槽的跨线程连接。 如图3所示,T0线程的私有对象Object_1发送的控制信号Signal_1 首先和主进程P中可访问的共享对象Bridge 的槽函数Slot 相连接。信号触发时,Slot 槽函数会使用Qt的emit函数发送自身定义的传导信号Signal,而Signal信号事先和存储线程Tm的私有对象Object_2的槽函数Slot_2 相连接。传导信号Signal 被时间触发后就会加入线程Tm的消息队列,激活线程Tm的槽函数Slot_2 进行响应,并完成图形界面要求的相关操作。如图中虚线所示,间接地实现了OpenMP线程间私有对象之间信号和槽的链接。 图3 信号和槽跨OpenMP线程桥接 当消息进入并行子线程的消息队列之后,仍需要事件循环机制来保证消息的正常处理过程。OpenMP 线程本身并不具备处理Qt消息队列的功能。借助于Qt本身的事件循环机制,整个OpenMP线程的事件处理流程如算法2所示。 算法2OpenMP线程事件处理机制 Input:QEvent事件队列 //section1 #pragma omp parallel num_threads(thread_count) { QEventLoopeventLoop; //处理本线程网络数据 //Do something //委任eventLoop处理事件循环->见section 2 eventLoop.exec(); } //section2 int QEventLoop::exec(ProcessEventsFlagsflags) { //获取私有数据指针d Q_D(QEventLoop); … //事件循环,消息队列空则退出循环 while(!d->exit.loadAcquire()) { //处理事件 processEvents(flags|WaitForMoreEvents|EventLoopExec); } //阻塞等待 wait_for_more_events(); Output:QEvent的操作结果 如section1 部分伪代码所示,每个OpenMP 线程在Fork 之后创建一个 QEventLoop 事件循环对象[16],然后才开始创建Socket并处理网络数据,在每个线程的末尾调用QEventLoop 的成员函数exec(),委托QEventLoop处理事件循环。事件循环的轮廓如section2 所示,事件首先进入线程的私有事件队列d中。事件处理过程会循环遍历事件队列,并且将入栈的事件发送到它们的目标对象当中,如果队列为空就会阻塞等待新的事件到来,如此循环实现整个线程的事件循环。 通过桥接和事件循环机制,本文将Qt 的事件循环机制嵌入OpenMP 线程,在保持OpenMP 线程级并行优势的同时还兼顾了Qt以消息驱动的图形界面良好的人机交互性能。 为了测试系统的性能,本文在4*28核Intel Core i7处理器,64 GB DDR3内存,配备10 Gb/s万兆网,RAID5磁盘阵列的服务器硬件环境下进行了测试。 首先是系统的可扩展性测试。并行实时测控数据存储系统关注的可扩展性主要指系统在网络流量不断变化的情况下,能否充分利用计算资源来确保系统的实时性,即响应时间维持在较低水平而不是随流量的变化发生较大变化。当系统测试环境确定后,数据包从解析到写入数据库的时间即系统服务时间就已经被确定。系统的响应时间主要取决于数据在缓冲队列中的等待时间,等待时间越短实时性就越高。本文在保证全部输入数据都是正常数据的前提下,测试了单线程存储系统和并行多线程存储系统的响应时间随系统输入总流量的变化情况。测试结果如图4所示,其中多线程的系统响应时间采用每个存储线程响应时间的统计平均值。 从实验结果可以看出,在系统流量为0 时,单线程和多线程系统与纵轴的交点即为系统的服务时间,本文的测试环境下系统的服务时间为20 ms,二者一致。随着总流量的不断增加,由于单线程系统只有一个读写线程,因此数据将会在缓冲区中累积,流量越大累积越多,排队时间越长,平均响应时间也越来越长,甚至出现丢包影响系统的实时性。并行系统在静态负载均衡算法的帮助下会进行任务划分,确定使用的计算核心数,基本保证了每个线程的流量负载都维持较低水平,而且大致相同,因此平均响应时间并不会随着系统总流量的增加而增加,而是基本保持在单核低负载时的水平。 图4 响应时间随系统总流量的变化情况 实时系统另一个重要指标就是系统的容错性。并行实时测控存储系统关注的容错性主要指在异常数据的干扰下系统能否做出正确的处理而不影响系统的响应时间,导致系统丢包。在保证总流量保持在80 MB/s较高负载的情况下,通过不断增加异常数据的比例,来统计系统的响应时间,测试系统的容错性,测试结果如图5所示。 图5 系统响应时间随异常数据比例的变化情况 从测试结果可以看出,在异常数据的比例较低时,对系统的响应时间影响较小。当增大到40%以上时,系统的响应时间明显出现延迟。这主要是因为异常数据在写入数据库之前,需要在图形界面显示给用户,而显示过程要和GUI 线程进行同步通信。尽管本文设计了细粒度同步方法,但GUI线程要负责所有其他存储线程的异常数据显示任务,负载过重,就会引起个别存储线程的等待阻塞,导致平均响应时间发生突变。 综上实验表明,并行实时测控存储系统在系统流量较大,负载较高的情况下,相比现行的串行存储系统具有更好的可扩展性、更强的实时性,能更好地发挥系统的硬件资源优势,进一步提高系统的综合性能。 本文通过引入M/M/1模型的排队系统,设计了一种适合并行实时测控数据存储系统的静态任务划分算法。基于互斥锁完成了线程间的细粒度通信同步机制。借助于事件循环机制将Qt的信号与槽特性成功地嵌入OpenMP线程,保留了二者的优点。在此基础上设计并实现了一套并行实时测控数据存储系统框架,并进行了测试。实验表明,该系统对现行的测控数据存储任务而言具有更好的扩展性、实时性和一定容错能力。后续,本文将针对系统容错能力阈值不高的问题,采用异常数据挑点显示、设置显示缓冲区等多种方法来减轻GUI线程的负载,进一步提高系统的可靠性。3.3 在OpenMP线程内嵌入事件循环
4 实验与分析
5 总结