常见开源服务器模型学习 未完待续

  https://blog.csdn.net/answer3y/article/details/48276687

本文要描述的主要有如下6种模型:
1)epoll 1线程(listen+accept+epoll_wait+处理) 模型 ...........................................代表产品redis
2)epoll 1线程(listen+accept+epoll_wait) + 1队列通知 + n线程(处理) 模型............代表产品thrift-nonblocking-server
2)epoll 1线程(listen+accept+epoll_wait) + n队列通知 + n线程(处理) 模型............代表产品memcached 
4)epoll 1进程(listen) + n进程(accept+epoll_wait+处理) 模型...............................代表产品nginx
5)epoll 1线程(listen) + n线程(accept+epoll_wait+处理) 模型
6)  epoll 1线程(listen+accept) + n线程(epoll_wait+处理) 模型
最后还有一个章节,对6种模型做一下统一的总结。
---------------------

epoll网络编程几个主要函数的用途,这样能更好的理解下面6种模型
listen_fd = socket(...); // 创建listen_fd
bind(listen_fd, addr); // 把listen_fd绑定到本地地址addr
listen(listen_fd, ...); // 监听listen_fd
fd = accept(listen_fd, ...); // 从listen_fd接受一个新接进来的连接套接字fd
epfd = epoll_create(...); // 创建指定大小的epoll句柄epfd
epoll_ctl(epfd, op, fd, event); // 对epfd做op操作,操作涉及监听fd的event事件
// 比如op是EPOLL_CTL_ADD,意思是把 “对fd监听event事件” 注册到 epfd里面
num = epoll_wait(epfd, events, ...); // 监听句柄epfd上的事件,并把事件通过event数组返回,num返回值是发生事件的个数
---------------------

一、epoll 1线程(listen+accept+epoll_wait+处理) 模型
1、代表开源产品:redis
2、基本原理:
这种模型基本就是教科书上的epoll使用方式:
socket -> bind -> listen -> epoll_wait ->  accept或者处理读写事件 -> epoll_wait ......
redis基本遵循这样循环处理 网络事件和定时器事件
3、echo server测试:10万QPS
4、优点:
1)模型简单。这个模型是最简单的,代码实现方便,(所以这个单线程)适合 计算密集型应用                          如果是 IO 密集型那么最好就要使用多线程来提高cpu的利用率来
2)不用考虑并发问题。模型本身是单线程的,使得服务的主逻辑也是单线程的,那么就不用考虑许多并发的问题,比如锁和同步
3)适合短耗时服务。对于像redis这种每个事件基本都是查内存,是十分适合的,一来并发量可以接受,二来redis内部众多数据结构都是非常简单地实现
5、缺点:
1)顺序执行影响后续事件。因为所有处理都是顺序执行的,所以如果面对长耗时的事件,会延迟后续的所有任务,特别对于io密集型的应用,是无法承受的
---------------------

二、epoll   1线程(listen+accept+epoll_wait)    +    1队列通知    +    n线程(处理) 模型
1、代表开源产品:thrift-nonblocking-server
2、基本原理:
1)在这种模型中,有1+n个线程。
2)有1个线程执行端口的listen并把listen_fd加入该线程的epoll_set,然后循环去做如下事情:1)epoll_wait监听新连接的到来,2)调用accept获得新到的fd,3)把fd放入队列,4) 回到 “1)” 继续epoll_wait
3)另外有n个工作线程,从队列里面获取文件描述符,然后执行:1)读取数据,2)执行任务
3、echo server测试:6万QPS
4、优点:
1)模型简单。这种模型的代码实现也是非常方便的
2)并发能力强。对于任务耗时或者IO密集的服务,可以充分利用多核cpu的性能实现异步并发处理
3)适合生产环境。虽然QPS没有特别高,但是对于目前大部分大型网站的吞吐量,这种网络处理能力也是足够了的,这也是为什么thrift nonblocking server可以用这种模型的原因
4)负载均衡。在这个模型,每个工作工作线程完成任务之后就会去队列里主动获取文件描述符,这个模型天然地就实现了负载均衡的功能。原因有2,一来是只有空闲的线程会拿到任务,二来是所有fd的事件监听都是由监听线程完成
5、缺点:
1)队列是性能瓶颈。
俗话说,不怕锁,只怕锁竞争。这个模型在运行过程中,n+1个线程竞争于队列之上,所以队列的访问是需要加锁的。对于echo server这种每次任务耗时都极短的服务,每次处理完任务就很快就会回到队列锁的争抢行列。大量的锁竞争直接拖垮了QPS。
不过好在常见的生产环境web服务都不是echo server,每次请求都会是毫秒级的,不会在锁竞争上产生问题。
---------------------

三、epoll   1线程(listen+accept+epoll_wait)  +  n队列通知  +  n线程(处理) 模型
1、代表开源产品:memcached
2、基本原理:
这种模型基本类似于 上一种模型,   区别在于 把1个队列换成n个队列,每个工作线程绑定一个队列,每个工作线程从自己的队列消费数据,其他的保持一致
3、echo server测试:20万QPS
4、优点:
1)并发能力更强。相比于单队列的模型,多队列的好处是减少了队列的锁竞争。 对于短耗时任务能得到比较多的提升,很适合缓存类应用
5、缺点:
1)有可能导致负载不均。因为监听线程是不会去根据不同线程的处理速度决定把任务分配给哪个线程的,  如果每个任务的耗时不均衡,那么就可能导致有些线程累死,有些线程饿死
6、memcached对该模型改进:
memcached是多线程且是缓存类应用,非常适合这个模型。改进如下:
1)工作线程拿到的fd,这个fd会加到本线程的epoll_set里面,这个fd的后续读写事件都由该线程处理
2)工作线程和监听线程之间建立了管道,工作线程的管道fd也加入到工作线程的epoll_set里面,那么就可以让 ‘新fd到来’和‘旧fd可读写’ 这两种事件都由epoll_set监听,减少调度的复杂性
3)因为memcached的任务基本是查内存的工作,耗时短而且相对均匀,所以对负载问题不是很敏感,可以使用该模型

---------------------

四、epoll 1进程(listen) + n进程(accept+epoll_wait+处理) 模型
1、代表开源产品:nginx
2、基本原理:(依据nginx的设计分析)
1)master进程监听新连接的到来,并让其中一个worker进程accept。这里需要处理惊群效应问题,详见nginx的accept_mutex设计
2)worker进程accept到fd之后,把fd注册到到本进程的epoll句柄里面,由本进程处理这个fd的后续读写事件
3)worker进程根据自身负载情况,选择性地不去accept新fd,从而实现负载均衡
3、echo server测试:后续补充
4、优点:
1)进程挂掉不会影响这个服务
2)和第二种模型一样,是由worker主动实现负载均衡的,这种负载均衡方式比由master来处理更简单
5、缺点:
1)多进程模型编程比较复杂,进程间同步没有线程那么简单
2)进程的开销比线程更多
---------------------

五、epoll 1线程(listen) + n线程(accept+epoll_wait+处理) 模型
1、代表开源产品:无
2、基本原理:
1)该设计和第四种的多进程模型基本一致,每个worker进程换成worker线程
3、echo server测试:后续补充
4、优点:
1)多线程模型更简单,线程同步方便
2)多线程模型线程开销比进程更少
---------------------

六、epoll 1线程(listen+accept) + n线程(epoll_wait+处理) 模型 (200万QPS实现echo server方案)
1、对应开源产品:无
2、基本原理:
1)1个线程监听端口并accept新fd,把fd的监听事件round robin地注册到n个worker线程的epoll句柄上
2)如果worker线程是多个线程共享一个epoll句柄,那么事件需要设置EPOLLONESHOT,避免在读写fd的时候,事件在其他线程被触发
3)worker线程epoll_wait获得读写事件并处理之
3、echo server测试:200万QPS(因为资源有限,测试client和server放在同一个物理机上,实际能达到的上限应该还会更多)
4、优点:
1)减少竞争。在第四和第五种模型中,worker需要去争着完成accept,这里有竞争。而在这种模型中,就消除了这种竞争
5、缺点:
1)负载均衡。这种模型的连接分配,又回到了由master分配的模式,依然会存在负载不均衡的问题。可以让若干个线程共享一个epoll句柄,从而把任务分配均衡到多个线程,实现全局更好的负载均衡效果
---------------------

七、总结和启示
1、除了第一个模型,其他模型都是多线程/多进程的,所以都采用了“1个master-n个worker”结构
2、如果accept由master执行,那么master就需要执行分配fd的任务,这种设计会存在负载不均的问题但是这种情况下,accept由谁执行不会存在竞争,性能更好
3、如果accept让worker去执行,那么同一个listen_fd,这个时候由哪个worker来执行accept,便产生了竞争;产生了竞争就使得性能降低
4、对于一般的逻辑性web业务,选择由master去accept并分配任务更合适一些。 因为大部分web业务请求耗时都较长(毫秒级别),而且请求的耗时不尽相同,所以对负载均衡要求也较高; 另外一般的web业务,也不可能到单机10万QPS的量级
5、这几个模型的变化,无非就是三个问题的选择:
1) 多进程,多线程,还是单线程?
2)每个worker自己管理事件,还是由master统一管理?
3)accept由master还是worker执行?
6、在系统设计中,需要根据实际情况和需求,选择合适的网络事件方案
---------------------

https://blog.csdn.net/zmzsoftware/article/details/17356199

  

void ET( epoll_event* events, int number, int epollfd, int listenfd )//边沿非阻塞  方式
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if ( events[i].events & EPOLLIN )
        {//这段代码不会被重复触发  所以循环取出数据 确保把socket缓冲区存储的数据读出来
            printf( "event trigger once
" );
            while( 1 )
            {
                memset( buf, '', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {//对于非阻塞IO 下面成立表示数据读取ok 此后epoll就能再一次触发fd 上的EPOLLIN事件
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {   //无需关闭socket   说明还有数据未接收 等待下一次处理
                        printf( "read later
" );
                        break;
                    }
                    //返回-1 那么寿命发生错误直接停止
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );// ==0 ET模式返回0 表示已经接受了所有数据了
                }
                else
                {//接收ok
                    printf( "get %d bytes of content: %s
", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened 
" );
        }
    }
}

 connect()函数

1.阻塞模式

客户端调用connect()函数将激发TCP的三路握手过程,但仅在连接建立成功或出错时才返回。返回的错误可能有以下几种情况:

    1>.如果TCP客户端没有接收到SYN分节的响应即(ACK),则返回ETIMEDOUT,阻塞模式的超时时间在75秒(4.4BSD内核)到几分钟之间。

    2>.如果对客户的SYN的响应时RST,则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没有启动),这称为硬错,客户一接收到RST,马上就返回错误 ECONNREFUSED.

    3>.如果某客户发出的SYN在中间的路由器上引发了一个目的地不可达ICMP错误,多次尝试发送失败后返回错误号为EHOSTUNREACH或ENETUNREACH.

附加产生RST的三种情况,一是SYN到达某端口但此端口上没有正在侦听的服务器、 二是TCP想取消一个已有连接、  三是TCP接收了一个根本不存在的连接上的分节

 2 非阻塞connect:

当一个非阻塞的tcp套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的tcp三路握手继续进行。我们接着使用select检测这个连接或成功或失败的已建立条件。非阻塞connect有三个用途:

(1) 我们可以把三路握手叠加在其他处理上,完成一个connect要花的RTT时间,而RTT波动很大,从局域网上的几毫秒到几百毫秒甚至是广域网的几秒。这段时间内也许有我们想要执行的其他工作可执行;

(2) 我们可以使用这个技术同时建立多个连接;这个技术随着web浏览器流行起来;

(3) 既然使用select等待连接建立,我们可以给select指定一个时间限制,使得我们能够缩短connect的超时。

非阻塞connect细节:

(1) 尽管套接字是非阻塞的,如果连接到的服务器在同一个主机上,那么当我们调用connect时候,连接通常立刻建立,我们必须处理这种情形;

(2) 源自Berkeley的实现(和posix)有关select和非阻塞connect的以下两个原则:

--(a) 当连接成功建立时,描述符变为可写;

--(b) 当连接建立遇到错误时,描述符变为既可读又可写;

关于accept函数调用的理解和整理      也可以阅读  unix网络编程p362       非阻塞accept 

                                 accept之前内核有两个队列 要知道

.accept()函数

1.阻塞模式

          第一种情况:阻塞模式下调用accept()函数,而且没有新连接时,进程会进入睡眠状态。  (最常见情况 也是最好理解的一种情况

     第二种情况:就是,当使用IO多路复用的函数如select/EPOLL_WAIT函数等方法去检测监听套接字一个外来连接时  很多时候我们将监听套接字设置为非阻塞     很多人觉得 IO多路复用函数会告诉我们该套接字上已有连接就绪  那么accept函数调用就不应该阻塞啊!  可是这里有bug啊 !

在比较忙的服务器中,在建立三次握手之后调用accept之前可能出现客户端断开连接的情况,再这样的情况下;如,三次握手之后,客户端发送rst,然后服务器调用accept,  即  服务器在io多路复用函数返回到调用accept之前 服务器收到客户端的RST   这是已经完成三次握手的 连接会被  服务器驱除出队列   假设队列中没有其他完成的连接了     服务器此时调用accept  但是没有已经完成3次握手的连接   于是服务器阻塞了  无法处理其他已经就绪的连接了     值到有其客户端建立一个连接位置

      解决办法如下        就是下面的那样 

2.非阻塞模式

非阻塞模式下调用accept()函数,而且没有新连接时,将返回  EWOULDBLOCK 错误。

非阻塞模式select() + accept() 

非阻塞accept:

在比较忙的服务器中,在建立三次握手之后,调用accept之前,可能出现客户端断开连接的情况,再这样的情况下;如,三次握手之后,客户端发送rst,然后服务器调用accept。posix指出这种情况errno设置为CONNABORTED;

注意Berkeley实现中,没有返回这个错误,而是EPROTO,同时完成三次握手的连接会从已完成队列中移除;在这种情况下,如果我们用select监听到有新的连接完成,但之后又被从完成队列中删除,此时如果调用阻塞accept就会产生阻塞;

解决办法:

(1) 使用select监听套接字是否有完成连接的时候,总是把这个监听套接字设置为非阻塞;

(2) 在后续的accept调用中忽略以下错误,EWOULDBLOCK(Berkeley实现,客户中止连接),  ECONNABORTED(posix实现,客户中止连接), EPROTO(serv4实现,客户中止连接)和EINTR(如果有信号被捕获);

 http://www.cnblogs.com/fighting-boy/p/9875288.html         (linux网络编程    里面有两个大一点的demo  web服务器  一个防火墙)

     epoll     EPOLLONESHOT

https://segmentfault.com/a/1190000003063859

https://blog.csdn.net/linuxheik/article/details/73294658

https://www.cnblogs.com/charlesblc/p/5538363.html

https://blog.csdn.net/liuhengxiao/article/details/46911129

原文地址:https://www.cnblogs.com/zhangkele/p/10284234.html