常见开源产品epoll网络事件模型分析(附200万QPS实现长连接echo server方案)

6 篇文章 0 订阅
3 篇文章 0 订阅

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

 

摘要:redis、thrift-noblocking-server、memcached、nginx等开源产品 四种不同场景下不同网络模型 分析

最近利用业余时间对基于epoll的网络服务模型做了一些调研和测试,既是复习也是一次再学习。在调研过程中,结合现有的开源产品的方案做了一些分析,看了很多源码也看了很多资料。大概有所了解,遂做一下总结和分享,如果不对,欢迎指正。
原文链接: http://blog.csdn.net/answer3y/article/details/48276687

-----------------------
2016年07月30日注,后续博客需要更新的内容:
更深入分析瓶颈在哪里?
增加类redis的单线程框架的实验效果
 

本文要描述的主要有如下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、在系统设计中,需要根据实际情况和需求,选择合适的网络事件方案

八、其他说明
1、压测实验内容:
1) echo server :同机器和其他机器一共100~200个线程的client长连接,不停请求server发送一个“hello world”,服务端返回接收到的字符串
2、设备和环境:
1) Intel(R) Xeon(R) CPU E5-2420 0 @ 1.90GHz 24核处理器,64G内存,
2) 没有root权限,没有做内核参数调整实验
3、后续工作:
1)把实验代码上git
2)上述实验均基于长连接,后续对端连接的模式做研究
4、参考:
特别指出:200万QPS方案参考自 http://bbs.chinaunix.net/thread-4067753-1-1.html

 

 

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\n" );
            while( 1 )
            {
                memset( buf, '\0', 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\n" );
                        break;
                    }
                    //返回-1 那么寿命发生错误直接停止
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );// ==0 ET模式返回0 表示已经接受了所有数据了
                }
                else
                {//接收ok
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}

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) 当连接建立遇到错误时,描述符变为既可读又可写;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值