大并发程序设计的思考

之前设计 VPN 大并发时候,第一次听到大并发的时候,心头一片迷茫,不知道如何设计才算是大并发的,网上找了很多资料,基本都没有对此进行过详细的说明,可能需要一些经验,或者阅读更为出色的代码才能得知,但在大并发 VPN 的设计当中,我总结了很多,当然,这些东西并非是一个准则,我对这个方面也只是初学而已,只是记录一些心得,使自己学到的东西更为系统,也方便日后阅读其他优秀设计的时候参照,不论方法是对是错,总得有个自己的感悟。

在我设计的时候,第一个问题就是什么是大并发?其实这个概念很简单,就是同一时间能够响应尽可能多的用户,比如同时在线用户为 1w 人,如果并发性不足,可能后 5k 人就直接被拒绝掉了。对于 UDP 来说,如果你的线程正在处理其他数据,而没有即时的调用 recvfrom,很容易致使 UDP 内核缓冲区满,再有新的数据包将会被无情的丢弃,这样会导致后来的 5k 人所发的 UDP 包很容易丢失以致无法连接。而对于 TCP 来讲,如果没有及时 accept,连接将会丢失。

如此一来,对于 TCP 来讲,只要单独分出一个线程拼命 accept 和收取数据,增加并发数不成问题;对于 UDP 应用,也是分离出线程拼命调用 recvfrom,把数据先缓存到用户态,也能够增加并发。但随之而来会有一个新问题,如何处理数据,如果处理不过来,那虽然接受下来也无济于事,仍然需要客户端无止境的等待,所以这里又涉及到一个吞吐量的问题。如何快速的处理数据。如果能够尽可能多的接受连接而又能尽可能快的处理数据,增加并发量将会是水到渠成的。可以把接受数据部分称为前端,处理数据称为后端,前端直接决定并发量,后端决定吞吐量,当然二者是有相当大的关联性的。

其实说了半天,只是理论上这样,如何实现还需针对具体情况,但这个可以当做一个指导方针,对于收取连接而言,linux 下可以使用 epoll 模型,windows 下可以使用 iocp 模型,都可以最大限度的接受连接,当然这里需要注意的是不要把耗时的操作放到这里,比如收取到数据以后,放入队列,而处理数据线程取队列中的数据,这个过程不能造成频繁的竞态,否则会损失前端接受数据的性能。

至于后端而言,提高效率需要理解处理流程中的瓶颈。

首先不能开启太多的线程,如果使用最简单的多线程模型,一个链接上来开启一个线程处理,似乎很高校,但由于系统需要不断的切换如此多的线程,切换线程时需要保存源线程环境,恢复目标线程环境,是一个相当耗时的操作,如果需要相应 1w 并发,使用 1w 甚至 5k 线程处理是相当不明智的做法。理论上讲,如果有 2 个 cpu,使用 2 个线程,一个 cpu 处理一个线程,不进行切换最为高效,不过由于进行处理数据不是时刻需要使用 cpu,比如读文件或数据库,将可能会有系统等待发生,一个线程不可能跑满整个 cpu,所以适当的提高线程数,尽可能多的压榨 cpu 的每一点性能。

而且在处理数据的过程中,尽可能不要出现长时间等待的状况,比如某个非常耗时的系统调用。这里的等待,也包括线程之间的竞态,如果所有的线程都需要从一个核心 data 中读写数据,可能读写这个数据时需要互斥,这样可能会造成很多线程等待同一个数据的读写,这种情况线程多了反而效果不佳,需要考虑调整程序的整体结构,尽可能的消除线程间的竞态。争取分离消耗 cpu 而且计算过程互不侵犯的部分为多线程,比如对数据包的加解密过程。

另外一个效率消耗在于内存管理,对 VPN 而言,每个用户的 IP 包过来都需要为之创建一个存储单元,如果对于每一个数据包的传递过程都多一次复制,那复制的代价是非常可观的。而且对于通用的 malloc 而言,需要兼顾各种情况,对于某一个具体应用,效率并非很高,比如如果传递的数据都是定长的,可以实现分配一定量的空间串成链表,这样分配和释放都只需要几次有限的操作便可,效率比之 malloc 提升很多,这里对于 IP 包数据而言,由于需要经过分片,所以长度限制在 1500 左右,适当的分配一个定长的大空间虽然会造成一写空间的浪费,但却用空间换取了时间上的优势。

另外一个值得注意的问题就是内存碎片,对于一个需要运行几年的服务器程序而言,看似微小的碎片,会逐渐造成大量不可用内存。所以一般需要长时间运行的程序都会自己实现特定情形的内存管理,尽量减少内存碎片的产生,他的危害虽然不如内存泄露严重,但却非常不易防止。

对于具体实现会有具体的效率提升方式,不过通用的方法就是理解程序运行流程,分离出耗时不耗时的操作,哪里的代码可以并发运行,哪里的代码只需要单线程便可,整个程序的瓶颈在哪等等。尽量减少等待,减少锁竞态,减少内存复制,前端可以尽可能多的接受连接,后端可以尽可能快的处理数据,使用 epoll 或者异步模型处理等待事件,便可很大限度的增加并发访问。

对于大并发的程序,还有一个重要的方面就是测试,不要相信测试人员会对你的程序做出一个合理的评判,性能真正的展现在于在真实环境中运行,大量用户访问的时候。在开发过程中需要考虑的问题便是如何进行测试,只有能够尽可能真实的模拟应用环境,才能让开发者了解自己程序的性能,从而不断的调整,这些测试数据直接成为修改程序流程或者构架的理由,除非对此经验丰富,否则不要去想当然的以为程序将会怎样,真实的数据才是唯一的性能标准。

而且测试过程中的现象直接成为改进程序的理由,如果在访问极限的时候,CPU 还未层充分利用,则可能程序过多挂起在等待过程中,就要通过现象思考等待在何处,哪里成为性能瓶颈,从而为增加 IO 缓存、提高网卡性能指标等等提供依据。当然这个过程也是非常复杂而不易把握的。同样需要经验和对处理过程的充分理解,还需依据合理的代码设计。

Built with Hugo
主题 StackJimmy 设计