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

摘要: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)模型简单。这个模型是最简单的,代码实现方便,适合计算密集型应用
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





©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页