中国民航信息网络股份有限公司 段锴 李永进 郝鹏 王波
传统上通过采用select 或poll 等系统调用可以实现多个TCP 网络连接的I/O 复用,但该方式并不支持基于System V 消息队列的报文传输。有名管道技术FIFO的描述符与Socket 的描述符相类似,可以被select 系统调用支持,因此可借助FIFO 设计一种方法,达到对System V 消息队列进行I/O 复用的目的。方法中设计地从消息队列接收报文的处理流程,在不降低处理性能的前提下,有效解决了有名管道和消息队列的操作不是原子操作带来的不一致问题,实现单一网络通信线程可以同时处理来自网络连接和System V 消息队列的报文。
随着移动互联网技术的不断发展和航空公司数字化转型的持续推进,航空公司运营模式也从单一型点对点航线向复合型网格化航线发展,全球运营航线网络的规模进而呈指数级高速增长。作为对外提供国内、国际航班库存查询的航班计划及库存查询系统(后简称“航班查询系统”或“系统”),需要面对海量访问压力下,进行复杂实时计算的挑战。采用基于多进程、分布式并行计算的架构,通常是解决该问题的主要思路。
对于多进程、分布式的架构来说,进程间通信是必须采用的主要技术之一。常用的进程间通信技术包括Socket 网络套接字、System V 消息队列、FIFO 有名管道等。其中,Socket 网络套接字是服务器之间通信的主要技术手段,System V 消息队列和FIFO 有名管道是服务内部进程间通信的主要手段。除此之外,当一个进程需要同时处理多个网络连接或交互式输入时,就需要使用到I/O 复用(I/O Multiplexing) 技术[1]。I/O 复用的机制是指,进程通过预先告知操作系统内核,使得内核一旦发现进程指定的一个或多个I/O 条件就绪时,就会通知进程进行处理,从而提升进程间通信的效率。目前支持I/O 复用的系统调用包括select、poll、epoll 等[2]。本文研究和解决的主要问题,就是利用I/O 多路复用技术去优化航班查询系统中服务端网络通信的效率。
航班查询系统中服务端网络通信的拓扑图如图1 所示,大量客户端应用与服务端系统通过socket 连接进行消息交互。服务端系统由多个航班查询应用进程、服务端网络通信服务进程WSH(Workstation Handler)、以及请求和应答(图中Q_Out)消息队列组成。其中WSH(Workstation Handler)进程是专门负责服务端网络通信的进程,一方面它需要处理多个客户端的socket网络连接,从中接收不同客户端进程的请求报文并放至航班查询进程的请求队列中;另一方面是该进程还要处理一个本地System V 消息队列Q_OUT,从中获取应答报文返回客户端。WSH 进程需要同时处理多个网络连接和消息队列的I/O,因此WSH 进程需要采用I/O复用技术,来提升处理系统的效率。
图1 服务端网络通信的进程WSH (Workstation Handler)Fig.1 The process of server network communication WSH (Workstation Handler)
常见的多网络连接I/O 复用技术,主要是通过调用select 或poll 等系统服务实现,但是由于System V 消息队列的标识符不属于描述符标识,因此不能在消息队列Q_OUT 上采用select 或poll 等调用。其他I/O 复用方案有采用多线程技术,即使用多个线程分别去处理Q_OUT 队列和网络Socket 的报文。但是多线程方案存在以下缺点:
(1)多线程的编程实现难度高,调试复杂;(2)多线程的可靠性较差,一个线程挂掉将导致整个进程挂掉;(3)多线程比单线程有额外的资源开销要求(例如内存和多核CPU),在某些特定用户的环境下(单核CPU 和内存受限),多线程方案不能被采用。
针对上述问题,本文提出了一种用于System V 消息队列的I/O 复用技术方案,以使得WSH 进程在不使用多线程技术的前提下,可以同时处理来自网络连接和System V 消息队列的报文,该方案也可以用于其他有类似服务端处理网络通信需求的系统,具体方案分为以下5个步骤。
WSH 进程创建有名管道FIFO 和消息队列Q_OUT。
系统调用mkfifo 用于创建有名管道FIFO:
int mkfifo(const char *path, mode_t mode); //其中path 是基于配置文件的配置,例如path=/opt/app/config/todewsh.fifo。
系统调用msgget 用于创建消息队列Q_OUT:
int msgget(key_t key, int msgflg); //其 中key是基于配置文件的配置,例如ipckey=372539。
由于FIFO 是半双工的,不能打开来既读又写,另外还基于FIFO 的特性,如果FIFO 当前没有被打开来写的话,以阻塞方式打开FIFO 只读的操作会被阻塞。由于WSH 进程启动时并不能保证已有航班查询进程打开有名管道FIFO 来写,所以WSH 进程必须以非阻塞的方式打开有名管道FIFO 来读:
int fiforead = open(“/opt/app/config/todewsh.fifo”,O_RDONLY|O_NONBLOCK)。
WSH 进程将FIFO 的描述符标识fiforead 放入可读描述字集合,同时也将客户端的网络Socket 加入相应描述字集合,然后调用select 进行I/O 复用。如果有客户端发送请求报文时,WSH 进程会被select 调用触发去接收数据,然后把请求报文发送至航班查询进程。
航班查询进程调用封装好的API(应用程序编程接口) Server_NetPutMsg 实现将应答报文放入Q_OUT 队列,同时打开并将事件报文放入有名管道todewsh.fifo。Server_NetPutMsg 函数首先调用msgsnd 系统调用,将应答报文放入消息队列Q_OUT,如果放入成功,再以只写的方式打开todewsh.fifo,为了防止航班查询进程被阻塞,仍然要以非阻塞的方式打开:int fifowrite=open(“/opt/app/config/todewsh.fifo”,O_WRONLY|O_NONBLOCK);打开以后再调用系统调用write 往管道todewsh.fifo 写入一个字符“T”当作事件报文。
由于Server_NetPutMsg 函数放消息队列Q_OUT 和打开以及放管道todewsh.fifo 事件是三个独立的API,非原子操作,所以存在放Q_OUT 成功、放管道事件失败的可能性,包括WSH 进程从Q_OUT 读取应答报文和从管道读取事件也是不同的API,同样存在一致性问题。当产生不一致状态时,如果WSH 进程只是基于select 的事件进行处理的话,势必造成Q_OUT 的数据延迟到下一次管道事件发送成功时才能被取到,这对实时性要求很高的在线系统来说是不允许的,因此需要增加下列操作来避免不一致状态的产生。
(1)首先判断上次从Q_OUT 取到的应答个数是否超过10 个,如果超过,则Q_OUT 仍有待处理数据的可能性很大,因此select 调用不设置超时;如果没超时,则select 调用设置超时为1ms。(2)基于以上的超时设置进行select 调用。(3)select 返回后判断管道是否有事件,如果有则执行第(4)步,没有则执行第(5)步。(4)调用read 方法读取管道事件,如果没读到事件则执行第(7)步;如果读到事件,则继续调用msgrcv 方法读取Q_OUT 队列数据。如果没读到继续从第(4)步重新开始,如果读到则执行第(6)步。(5)如果管道没有事件或者select 超时,为了避免管道事件和Q_OUT 队列数据的不一致造成延误队列数据发送,则调用msgrcv 方法读取Q_OUT 队列数据。如果没读到数据执行第(7)步,如果读到则调用read 方法尝试读取管道事件,不论管道有无事件,继续执行步骤(6)。(6)将计数器Rcv_Qout_Num 加1,并将数据发送客户端,然后判断Rcv_Qout_Num 是否大于10 个,如果大于10,继续执行第(7)步,如果不大于10 则继续从第(3)步重新开始。(7)继续基于select 的返回处理网络事件,以读取客户端的请求数据,这块逻辑与普通网络I/O 复用处理相同,不再赘述。处理完毕后继续从第(1)步循环处理。
WSH 进程被select 调用触发,按照上述流程读取管道的事件和消息队列Q_OUT 中的应答报文。航班查询进程放入管道todewsh.fifo 一个字节的事件后,WSH 进程就会被select 调用触发,进而首先从管道中读取一个字符,再从消息对列Q_OUT 读取应答报文,然后将数据发送给客户端。利用上述处理流程和方法,WSH 进程就可以借用有名管道,实现多个网络连接和消息队列的I/O 复用处理。
中国民航旅客服务系统为海内外60 多家航空公司提供库存管理和运营服务,目前是世界第三大航空旅游分销系统提供商和全球最大的结算数据处理中心[3]。旅客服务系统依托于信息技术,是高并发实时交易系统,具有旅客服务主业务链条长、相关参与方数量多、安全风险等级高的特点,涉及从航空产品的顶层设计到末端接触点服务的全流程和全生命周期[4]。航班计划及库存查询系统,是中国民航旅客服务系统中的重要核心子系统之一,该系统的查询访问量峰值超过10000 笔每秒。通过本文的方法优化,全面提升了整个系统的响应时间,平均响应时间由300ms 缩短至150ms,查询请求处理效率提升100%。
经过航班查询系统的实际检验,相对于多线程方案,本文提出的I/O 复用方案具有以下几个优点:(1)复用了select 的处理模型,没有对普通socket 处理流程带来复杂变化,同时具备很好的处理性能;(2)保持了单进程、单线程的特点,适用范围广泛;(3)有名管道FIFO和System V IPC 队列均是操作系统提供,访问它们的接口也都是系统调用,保证了程序的可靠性。
引用
[1] STEVENS,Richard W.UNIX Network Programming,Interprocess Communications[J].CODES'93:IEEE/ACM International Workshop on Computer-Aided Hardware-Software Codesign,1998,11(11):225-228.
[2] Richard W Stevens,Bill Fenner,Andrew M Rudoff.UNIX网络编程卷1 套接字联网API 第3版[M].北京:人民邮电出版社,2019.
[3] 崔志雄.中国航信数字化转型进行时[J].企业管理,2020(6):105-107.
[4] 梁海峰,黄恺,杜建国.民航旅客服务系统交易模型及关键技术研究[J].电子测试,2017(22):80-83.