Open rainzhaojy opened 7 years ago
构建大型应用时,必然要解决的一个问题就是如何支持高并发的网络连接。虽然目前的技术和框架都很丰富,我们可以直接使用这些库进行高并发的网络编程,但深入理解网络编程相关知识还是很有用处的,可以帮助我们更好的用好这些库和做性能调优。
关于网络编程有一些经典书,这里推荐一下:
C10k problem指Concurrent 10k Connections,原文出处在此,即单机同时支持1万个并发连接,99年由Dan Kegel提出。
C10k问题早就被解决了,目前单机支持c10m并发也已经不是问题了。解决c10k问题的一个主要方法是利用OS层各种异步IO模型,譬如linux上的epoll。
实现高并发可以从很多维度实现,首先要提高单机并发连接数,可以利用异步IO模型,结合多线程或多进程,提高单机并发量。
其次,为了充分利用服务器性能,实现更高并发量,还可以做更多调优,譬如多队列网卡、中断合并、kernel调优等等。
最后,可以通过服务器集群实现更高并发,构建一个分布式的后台系统,现在的高并发网路应用基本都是分布式架构。分布式架构涉及很多知识点,包括:
分布式架构有机会再讨论,本文主要讨论异步IO模型相关知识和相关的网络库。
Stevens在《UNIX网络编程》一书6.2 I/O Models中介绍了五种IO Model:
当应用程序(Application)调用kernel API读取网络数据时,它会经历两个阶段:
记住这两个阶段很重要,因为这些IO Model的区别就是在两个阶段上各有不同的做法:
recvfrom()
EWOULDBLOCK
aio_read
不同OS提供了不同的IO multiplexing机制: select on unix/linux, IOCP on windows, Kqueue on BSD, epoll on Linux, Linux提供了select, poll 和 epoll,本文介绍他们的工作原理和优缺点。
select和poll的原理基本相同:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看,32位机默认是1024个,64位机默认是2048。
cat /proc/sys/fs/file-max
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
相比select,poll解决了单个进程能够打开的fd数量有限制这个问题:select受限于FD_SIZE的限制(一般为1024),如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。
FD_SIZE
select和poll共同的一个缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd数量增多而线性增大。
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++,还有各种优秀的库可以帮助我们支持高并发网络连接:
构建大型应用时,必然要解决的一个问题就是如何支持高并发的网络连接。虽然目前的技术和框架都很丰富,我们可以直接使用这些库进行高并发的网络编程,但深入理解网络编程相关知识还是很有用处的,可以帮助我们更好的用好这些库和做性能调优。
经典书推荐
关于网络编程有一些经典书,这里推荐一下:
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读取网络数据时,它会经历两个阶段:
记住这两个阶段很重要,因为这些IO Model的区别就是在两个阶段上各有不同的做法:
recvfrom()
就被block,阶段1和阶段2都被blocking,早期很多应用就是这么用的,然后使用多线程,也就是每个网络连接对应一个线程,好一点的做法是使用线程池。Apache HTTP Server就是这么工作的recvfrom()
后,阶段1不会被blocking,会直接返回错误EWOULDBLOCK
,App可以轮询调用recvfrom()
直到有数据返回,这一做法被成为polling,这种做法并不高效,会浪费CPU资源,比较适合CPU只做一件事的场景aio_read
传入一些参数(file descriptor, buffer pointer, buffer size, file offset, 回掉方法等),然后方法直接返回,等到阶段2结束时,kernel通过回掉通知Appselect, 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++,还有各种优秀的库可以帮助我们支持高并发网络连接: