常见并发服务器方案
-
循环式/迭代式服务器:短链接
缺点:无法充分利用多核CPU,不适合执行时间较长的服务 -
并发式服务器:长连接
(1)主进程负责监听client的连接请求
(2)当连接建立后,新fork一个子进程与client通信
适应场景:适用于执行时间比较长的服务
-
prefork服务器
(1)父进程进行listen,然后先fork出大量的子进程===>此时,父子进程都listen客户端的到来
(2)当client连接到来时,可能导致大量的accept返回,但是只有一个accept返回是正确的(只有一个能建立连接成功),其他的是错误的,这种现象叫做“惊群现象”,关于惊群现象请参考UNP2e 第27章
-
反应式(reactive)服务器(reactor模式)
(1)select/poll/epoll
(2)并发处理多个请求,实际上是在一个线程中完成。无法充分利用多核CPU
(3)不适合执行时间比较长的服务,所以为了让客户感觉是在“并发”处理而不是“循环”处理,每个请求必须在相对较短时间内执行
-
reactor + thread per request (过渡方案)
-
reactor + worker thread (过渡方案)
-
reactor + thread pool (能适应密集计算)
(1) Reactor监听到读事件到来后,将调用read函数读取数据,此过程参考select服务器程序,是在单线程中完成的
(2) 将上面读到的数据,封装成一个一个的任务,添加到任务队列中,并通知线程池中的空闲线程去处理任务(decode、compute、encode)
(3) 当处理完一条任务后,再将处理的结果添加到select的写事件上
(4) select发现可写事件发生,就会调用send将数据发送出去
总结:此模式是reactor + thread per request模式的升级版本,因为select仅仅只进行I/O操作,比较耗时的compute任务由线程去处理,所以该模式适用于计算密集型任务。
-
multiple reactors (能适应更大的突发I/O)
reactor in threads ( one Eventloop per thread ) 每个线程都是一个事件循环
reactor in processes ( one loop per process)
特点:
(1) 该模式有多个reactor,即有多个事件循环
(2) 每个reactor都是一个线程或者一个进程
优点:
当只使用一个mainReactor进行监听listenfd、conn_fd的读事件时,可能mainReactor会出现瓶颈,而使用mainReactor+subReactor+subReactor的方式可以解决瓶颈的问题
**如何使用?**假设有3个千兆网卡,可以每个网卡都分配一个subReactor
模式执行流程详解:
(1) mainReactor、subReactor、subReactor分别处在三个线程中
(2) mainReactor中存放着listenfd,监听listenfd的读事件
(3) 当发现client连接请求后,由acceptor接收连接,并把接收到的连接conn_fd1放入subReactor1中,由subReactor1监听conn_fd1的读事件
(4) 当再次发现client连接请求后,由acceptor接收连接,并把接收到的连接conn_fd2放入subReactor2中,由subReactor2监听conn_fd2的读事件
(5) 当再次发现client连接请求后,由acceptor接收连接,并把接收到的连接conn_fd3放入subReactor1中,由subReactor1监听conn_fd3的读事件
(6) 下次连接放入subReactor2中,… …,新的连接交替的放入subReactor1、subReactor2,此种方式叫“伦叫”(round robin),可以保证连接均匀的分布到多个subReactor/事件循环中。
-
multiple reactors + thread pool (one loop per thread + threadpool) (突发I/O与密集计算)
说明:该模式是在 multiple reactors 模式上添加了thread pool,用来解决耗时的compute操作
-
proactor服务器 (proactor模式,基于异步I/O)
(1)理论上 proactor比reactor效率更高一些
(2)异步I/O能够让I/O操作与计算重叠。充分利用DMA直接存储访问特性
(3)Linux异步IO
[1] glibc aio (aio* ),有bug
[2] kernel native aio (aio*),也不完美。目前仅支持O_DIRECT方式来对磁盘读写,跳过系统缓存。要自己实现缓存,难度不小
(4)boost asio实现的proactor,实际上不是真正意义上的异步I/O,底层是用epoll来实现的,模拟异步I/O的
man aio_read
int aio_read(struct aiocb *aiocbp);
man 7 aio
struct aiocb {
/* The order of these fields is implementation-dependent */
int aio_fildes; /* File descriptor */
off_t aio_offset; /* File offset */
volatile void *aio_buf; 缓冲区 /* Location of buffer */
size_t aio_nbytes; /* Length of transfer */
int aio_reqprio; /* Request priority */
struct sigevent aio_sigevent; /* Notification method */
int aio_lio_opcode; /* Operation to be performed;
lio_listio() only */
/* Various implementation-internal fields not shown */
};