一、如何进行良好服务器的设计

1、非阻塞I/O + I/O复用方式:
           在这个多核时代,服务端网络编程如何选择线程模型呢? 赞同libev作者的观点:one loop perthread is usually a good model(一个线程有一个事件循环是一个好的事件模型),这样多线程服务端编程的问题就转换为如何设计一个高效且易于使用的event loop,然后每个线程run一个event loop就行了(当然线程间的同步、互斥少不了,还有其它的耗时事件需要起另外的线程来做)。

event loop是non-blocking网络编程的核心,在现实生活中non-blocking 几乎总是和IO-multiplexing一起使用,原因有两点:
1、没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费CPU资源了。
2、IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO 中read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket上的 IO 事件了。
所以,当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing,单用其中任何一个都没有办法很好的实现功能。

2、epoll + fork方式:
           强大的nginx服务器采用了epoll+fork模型作为网络模块的架构设计,实现了简单好用的负载算法,使各个fork网络进程不会忙的越忙、闲的越闲,并且通过引入一把乐观锁解决了该模型导致的服务器惊群现象,功能十分强大。
 

二、Reactor模型

Reactor模型:是一个异步I/O模型,Reactor设计模式是用于处理服务请求的事件处理模式,由一个或多个输入同时传递给服务处理程序,服务处理程序然后对传入的请求进行解复用,并将它们同步分派给关联的请求处理程序
重要组件: Event事件、Reactor反应堆、Demultiplex事件分发器、Evanthandler事件处理器。

一个Reactor模型的网络服务器交互流程:
1、应用若对某些事件感兴趣,将感兴趣的事件及预置的回调函数Handler注册到Reactor反应堆上,并且在事件发生时候调用回调Handler函数。
2、Reactor反应堆维护的为事件以及事件处理的集合,反应堆通过相应方法(如epoll_ctl()方法的add/mod/del)可以在事件分发器中针对事件进行调整,然后启动Reactor反应堆。
3、反应堆的后端驱动事件分发器的启动,如开启epoll_wait(),服务器等待新用户连接或处理已连接用户的读写事件。
4、若epoll_wait()监听到有新事件产生,事件分发器会将相应事件给反应堆返回,Reactor会通过map表找到Event事件对应的事件处理器来读取用户请求,解码(数据反序列化)、业务处理、打包(数据序列化),再将处理好的数据发送给用户。
在这里插入图片描述
muduo库的Multiple Reactors模型:
1、mainReactor与subReactor相当于将Reactor反应堆与Demultiplex事件分发器两个组件合二为一了,由他们监听事件的发生,事件发生调用回调函数,进行请求的相应一系列处理操作。
在这里插入图片描述
 

三、I/O复用对比

1、select与poll缺点:
1、单个进程能够监视的文件描述符的数量存在最大限制,通常是1024(#define __FD_SETSIZE 1024),每次需要将套接字设置到位数组中,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差。
2、内核 / 用户空间内存拷贝问题,select需要复制大量的文件描述符数据,产生巨大的开销。
3、select返回的是含有整个文件描述符的数组,应用程序需要遍历整个数组才能发现哪些文件描述符发生了事件。
4、select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,
那么之后每次select调用还是会将这些文件描述符通知进程。
poll相比select模型,使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

      以select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的句柄结构内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到100万级别的并发访问,是一个很难完成的任务。

2、epoll优势:
      设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接,此时就需要用到epoll了。
      epoll的设计和实现与select完全不同,epoll通过在Linux内核中epoll_create()申请一个简易的文件系统(文件系统一般用B+树,大大减少磁盘IO消耗低,效率很高)。把原先的select/poll调用分成以下3个部分:
1、调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
2、调用epoll_ctl向epoll对象中添加这100万个连接的套接字;
3、调用epoll_wait收集发生的事件的fd资源;

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这
个epoll对象中添加或者删除事件。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,会将发生事件的集合从红黑树上摘录下来放入双向链表中,直接返回给应用程序,不需要向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
epoll_create在内核上创建的eventpoll结构如下:

struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件,直接返回给应用程序*/
struct list_head rdlist;
....
};

 

四、ET模式与LT模式

epoll对文件描述符有两种操作模式:LT(电平触发)模式和 ET(边沿触发)模式。LT 模式是默认的工作模式。当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。但并不是ET模式绝对会比LT模式效率高,比如在特殊情况下epoll工作在ET模式下,一个套接字远远不断的数据非常多,但ET模式只会上报一次,此时应用程序需要不断循环处理这些数据无法回到epoll_wait,其它有事件上报的程序无法得到及时处理。
LT模式:当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时,还会再次向应用程序通告此事件,直到该事件被处理。
ET模式:当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此一半了来说效率比 LT 模式高。

实际上muduo库采用的为LT模式,主要好处如下:
1、不会丢失数据或者消息: 应用没有读取完数据,内核是会不断上报的。
2、低延迟处理: 每次读数据只需要一次系统调用;照顾了多个连接的公平性,不会因为某个连接上的数据量过大而影响其他连接处理消息。
3、跨平台处理: 像select一样可以跨平台使用。
 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐