rainzhaojy / blogs

200 stars 28 forks source link

高并发网络编程的那些事 #9

Open rainzhaojy opened 7 years ago

rainzhaojy commented 7 years ago

构建大型应用时,必然要解决的一个问题就是如何支持高并发的网络连接。虽然目前的技术和框架都很丰富,我们可以直接使用这些库进行高并发的网络编程,但深入理解网络编程相关知识还是很有用处的,可以帮助我们更好的用好这些库和做性能调优。

经典书推荐

关于网络编程有一些经典书,这里推荐一下:

C10k problem

C10k problem指Concurrent 10k Connections,原文出处在此,即单机同时支持1万个并发连接,99年由Dan Kegel提出。

C10k问题早就被解决了,目前单机支持c10m并发也已经不是问题了。解决c10k问题的一个主要方法是利用OS层各种异步IO模型,譬如linux上的epoll。

如何实现高并发

实现高并发可以从很多维度实现,首先要提高单机并发连接数,可以利用异步IO模型,结合多线程或多进程,提高单机并发量。

其次,为了充分利用服务器性能,实现更高并发量,还可以做更多调优,譬如多队列网卡、中断合并、kernel调优等等。

最后,可以通过服务器集群实现更高并发,构建一个分布式的后台系统,现在的高并发网路应用基本都是分布式架构。分布式架构涉及很多知识点,包括:

分布式架构有机会再讨论,本文主要讨论异步IO模型相关知识和相关的网络库。

I/O模型:

Stevens在《UNIX网络编程》一书6.2 I/O Models中介绍了五种IO Model:

当应用程序(Application)调用kernel API读取网络数据时,它会经历两个阶段:

  1. Waiting for the data to be ready: 1.1 对于TCP协议,数据可能没有从网络上接收到,也可能已经收到一部分在TCP buffer里,但TCP协议并不认为可以通知上层应用(需要按序或重传等) 1.2 对于UDP协议,不涉及buffer和按序的问题,可能是还没有从网络上收到一个完整的UDP报文
  2. Copying the data from the kernel to the process

记住这两个阶段很重要,因为这些IO Model的区别就是在两个阶段上各有不同的做法:

image

select, poll, epoll

不同OS提供了不同的IO multiplexing机制: select on unix/linux, IOCP on windows, Kqueue on BSD, epoll on Linux, Linux提供了select, poll 和 epoll,本文介绍他们的工作原理和优缺点。

select和poll的原理基本相同:

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看,32位机默认是1024个,64位机默认是2048。

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

相比select,poll解决了单个进程能够打开的fd数量有限制这个问题:select受限于FD_SIZE的限制(一般为1024),如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。

select和poll共同的一个缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd数量增多而线性增大。

epoll:

epoll支持水平触发(level trigger)和边缘触发(edge trigger),最大的特点在于边缘触发,edge trigger只支持nonblocking socket,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

epoll解决了select、poll的缺点:

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口); 2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。 3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

开源网络库:

C/C++网络库有:

其他的还有ICE, cppsocket, netclass, poco, SimpleSocket, socketman, Sockets

除了C/C++,还有各种优秀的库可以帮助我们支持高并发网络连接: