1、 完成端口实现高性能服务端通信层的关键问题摘 要:为实现高性能稳定的网络通信服务,对完成端口(iocp)应用中信息识别与提取、资源管理、消息乱序处理 3个关键问题进行了分析,提出了 iocp参数扩展、内存池、对象池、环形缓冲等改进的解决方法。使用这些方法对 iocp底层进行了封装,并设计和实现了面向企业应用的可扩展网络程序通信模块。压力和性能测试结果表明,该模块能在合理资源消耗基础上支持海量并发连接,具有较高的数据吞吐量,在实际项目应用中也表现出了良好的性能。关键词:完成端口;i/o 模型;套接字;内存池;环形缓存key issues of high performance server co
2、mmunication layer by using i/o completion portliao hong jian * , yang yu bao, tang lian zhang(network and modern education technology center, guangzhou university, guangzhou guangdong 510006, china) abstract:in order to achieve high performance and stable network communication service, the key issue
3、s of client information identifying and extracting, resource management and message sequence dealing in i/o complete port (iocp) development were analyzed. and the improved methods of iocp parameter extension, memory pool, object pool and ring buffer were proposed respectively. on the basis of under
4、lying encapsulation for iocp using these methods, a scalable network communication module for enterprise applications was designed and implemented. the experimental results show that the module can support massive concurrent connections, and has higher data throughput by reasonable resource consumpt
5、ion. the proposed solution has also showed good performance in the actual project application.key words:i/o complete port (iocp); i/o model; socket; memory pool; ring buffer0 引言 网络服务程序通信层的“高性能”主要表现为对客户端更大的响应规模、更高的并发处理能力、更高的数据吐吞量、更低的系统资源消耗等方面 1 , 这对服务端通信层采用的技术及设计提出了更高的要求。为此,微软研究数年提出了完成端口(i/o complete
6、 port, iocp)机制,它把重叠 i/o操作完成的事件通知放入系统维护的一个队列,并唤醒某个工作线程来处理相应的消息。重叠操作是在与 iocp相关联的一个或多个文件句柄上进行的,大大减少了线程上下文切换,最大限度地提高了系统并发量 2, 能满足“高性能”要求。 iocp是一个高效但复杂精巧的内核对象, 微软的 sdk只提供了简单的说明文档和示例代码 3 , 现有的相关文献或以描述iocp机制的使用方法和步骤为主 4-5 , 或局限于 iocp原理探讨 6 , 或出于应用项目商业版权的考虑,局限于系统的模块设计 7 , 对构建大响应规模服务程序时遇到的棘手问题给出全面具体解决方法的较少。在
7、 iocp开发中,存在的典型问题有:客户端信息识取、资源管理与访问紊乱、消息乱序,这 3个问题的有效解决是构建稳健、高效 iocp服务程序的关键。在研究现有资料并结合开发实践基础上,下面讨论这些问题起因并给出解决办法。 1 iocp关键问题分析 1.1 客户端信息的识别与提取 在 iocp应用中必定要处理大量客户端的 i/o请求,当大量客户端连接服务器并且一个异步 i/o操作成功完成时,服务线程对操作结果进行处理就必须要知道这个 i/o是来自哪个客户端,以及 i/o数据存在何处(如该 socket所对应的客户端 sockaddr_in结构体数据、存放网络数据的缓冲区等)。 文献8给出了 map
8、查找的方法,即创建一个与该 socket一一对应的客户端底层通信对象并将对象放入一个类似于 list或 map的容器中,需要使用时使用容器的查找算法根据 socket值找到它所对应的对象信息。在客户端连接数不多的情况下这种方法简单可行,但是当客户端连接量很大时,频繁查表非常影响效率。 其实在 iocp中,工作者线程调用 api函数getqueuedcompletionstatus取得一个 i/o完成时,会捎带两个具有可扩展的参数,一个是 ulong_ptr类型的“completionkey(完成键)”;另一个是 overlapped结构的指针。其中“完成键”是一个套接字句柄首次与 iocp对象
9、关联时所使用的一个参数,既然在 i/o完成后可取回这个数据,那么可以把它当一个指针使用,在关联之前对它指向的数据结构进行扩展,设计为一个包含客户端信息的结构,在完成返回时,应用层通过读取完成键指向的这个数据结构来识别客户端。 iocp利用 overlapped结构进行重叠 i/o操作,同时规定,如果要投递一个重叠 i/o操作,必须要带有 overlapped结构,即要把指向一个 overlapped结构的指针包括在其参数中。如上所述,在操作完成后也可以拿回这个指针,但是单凭这个指针所指向的overlapped结构,应用程序并不能分辨完成的是哪个类型的操作。不过基于上面的认识,只需要自定义一个扩
10、展的 overlapped结构,在其中加入所需的跟踪信息,当操作结束并取得指向 overlapped结构的指针后,可以用 containing_record宏取出指向扩展结构的基址,进而读取里面的跟踪信息来判断操作类型。 同时,i/o 操作时系统将 overlapped结构的地址传给核心,核心完成相应的操作后仍然通过这个结构传递操作结果,这样overlapped结构中已经包含了数据位移参数,扩展的结构中只要包含数据缓存地址及缓存大小即可得到网络数据。 可见,这两个参数提供具有很强的“伸缩”作用,巧妙和充分利用这两个参数,就能解决客户端信息识别与数据提取的问题。 1.2 系统内存资源管理 内存管
11、理是使用好 iocp的核心问题。使用 iocp机制异步处理大量请求时,很容易出现因内存不足、访问紊乱致使程序崩溃等问题。这不是 iocp的特有问题,而是应用中引起的系统资源耗尽问题。为提高资源使用效率,必须对资源管理进行合理的设计。 1)异步 i/o缓冲区的分配与利用。 在 winsock中,核心模式驱动程序(kernel model driver, kmd)负责缓冲区管理,由 afd.sys文件实现。当调用 wsasend()函数异步操作时,afd.sys 将把数据拷贝到内部 afd缓冲区,然后 wsasend函数立即返回,由 afs.sys在后台负责把数据发送出去。从客户端接收数据的情况相
12、似。在这个过程中会出现调用 wsasend发送错误的情况,但错误码是 wsa_io_pending,这表示 afd缓冲区已满而无法拷贝程序缓冲区数据。这时系统会将程序缓冲区加载到页面内存并锁定,直到 afd缓冲区有空间来拷走程序缓冲区数据,并给iocp一个完成消息,调用返回时解锁页面内存。当大量客户端连接并投递大量异步重叠 i/o时,就会出现大量未决的 i/o缓存被加载到内存并加锁。同时,系统会锁定包含缓冲区的整个内存页面。当服务程序锁定的内存达到系统规定的内存分页极限时,就会产生wsaenobufs错误。 文献9给出了 3点注意事项,除了强调使用工作线程执行异步操作这一点外,其余无甚裨益。解
13、决这个问题的思路是减少可能被锁定的缓存区,目前可行的一个方法是投递零字节缓冲区来实现。先投递零字节空缓冲区的异步读取操作 azerobyteread(),当操作返回时表示有网络数据可以读取,再投递非零字节缓冲区的wsarecv函数来进行真实数据的读取。在真实数据读取完成返回时再投递 azerobyteread(), 如此循环。这样就总能对被锁定的内存进行解锁。此外,尽量使用 virtualalloc函数分配系统页面大小倍数的读写缓存区,这样分配的缓冲块才是与页面对齐的,这样就能减少总的内存锁容量。 以上只是内存使用中的具体技巧,内存管理的高效主要体现在内存的申请和释放效率。每次投递异步 i/o
14、时都必须创建新的句柄数据,数据空间的频繁创建和释放是相当占用系统资源的。这里可以使用内存池的方式来解决,基于 slab算法实现内存池是一个好的思路,可以事先创建这些数据块的队列结构,并将其统一放入一个空闲队列,在投递 i/o操作时,首先查看队列中是否有可用的空间:如有,则将第一个空间取出使用;否则真正向系统申请内存空间。在释放空间时,首先判断队列是否已满,如果未满,就将空间清空后从尾部存入队列;否则才真正释放这段空间。 同样在使用多个重叠 i/o函数 acceptex来守候大量短时连接请求时,可以事先创建一个 socket池,这就省去了连接时临时创建每个 socket的系统开销。 服务程序也可
15、能对非分页池极限产生冲击。使用 iocp时会分配大量的套接字等内核对象,它们驻留在非分页池中而不会被交换到页面文件中。为了防止程序消耗到非分页池的极限,就要尽可能重用 socket等对象。比如使用 transmitfile和 transmitpackets函数时可以通过指定 tf_reuse_socket和 tf_disconnect标志来重用套接字句柄,每当 api完成数据的传输工作后,就会在传输层级别断开连接,这个套接字又可以重新提供给 acceptex()使用。通过设置合理数量的 socket池和 socket重用,尽可能减少内存的动态申请和释放,提升了系统的性能。 2)资源释放与访问紊
16、乱。 iocp资源释放是 iocp编程中的一大难点。如前面提及的在执行wsasend函数出现非 pending的返回错误时,就要对此错误进行处理,包括释放客户端数据结构体和发送缓冲区数据,并关闭当前操作所使用的 socket。 但问题的焦点在于投递的 i/o请求完成后都是放在一个 completion packet队列里面等待getquenedcompletionstatus函数取出的,如果关闭 socket时把和socket相关的资源都释放了,当客户端之前所执行的一些 i/o调用返回,此时又试图去处理返回的 completion packet,一个访问紊乱(access violation)
17、就发生了。 文献10给出了释放操作线性化的方法,但设计机制复杂,实现困难。可行的方法是对数据缓冲区使用引用计数机制。为了避免访问紊乱的发生,为客户端结构体增加一个阻塞 i/o调用的计数,只有当计数为零,即不存在阻塞 i/o调用时,才删除该结构体。 1.3 数据包重排序与字节块包处理 1.3.1 数据包重排序 iocp是个严格的先进先出(first in first out, fifo)系统,先提交的异步 i/o请求先完成。当系统只有一个工作线程并且只在一个 socket投递一个异步操作时,该机制完美无缺;但是为了完全发挥 iocp的性能,可能会有多个工作线程在同一连接上同时投递多个异步操作,线
18、程切换的随机性会导致缓冲区中的数据有可能顺序混乱。已有的文献中只强调了数据接收时数据包的排序处理11-12 , 忽略了发送时的顺序问题。 全面的解决办法分为两个方面:1)针对发送端,调用 wsasend的次序就是缓冲区被填充的次序,在多个工作线程的情况下不使用postqueuedcompletionstatus向 iocp投递“写数据”请求,因为线程切换的随机性会致使 wsasend不一定按 post方法投递的顺序被调用,而导致对方接收到的数据可能乱序。同时避免从不同的线程中同时调用同一个 socket上的 wsasend函数,避免缓冲区的数据处于不可预知的次序。2)使用顺序标签。在每个 socket上使用两个互斥变量分别来标识当前数据包读、写的顺序号,同时在单i/o数据块中设置重叠 i/o操作的序号。在读取数据时,如果当前读取数据块的顺序号与 i/o 操作号相等,说明请求的数据即是现在要读取的数据;否则先缓存这个数据,等到它之前的数据包被处理后才能处理它;发送也采用相同的策略。这样就巧妙地解决了数据包的错序问题。 1.3.2 tcp层引起的字节块中包的处理 tcp协议会根据网络状况和 neagle算法将一个逻辑包单独或分若干次发送。这样,接收到的一个字节块流可能包含一个或多个包,或者包的一部分,由此便带来了字节流数据块和部分包的拼装问题,如图 1所示。