IO多路复用与进/线程池的优劣对比

首先提一下惊群

惊群现象

主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。

那么,由于所有子进程都继承了父进程的 sockfd,那么当连接进来时,所有子进程都将收到通知并“争着”与它建立连接,这就叫“惊群现象”。大量的进程被激活又挂起,只有一个进程可以accept() 到这个连接,这当然会消耗系统资源。

Nginx(engine x) 是一个高性能的HTTP和反向代理服务器,其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

Nginx对惊群现象的处理

Nginx 提供了一个 accept_mutex 这个东西,这是一个加在accept上的一把共享锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept()。**有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),**这样就不会有惊群问题了。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。
Nginx进程详解

worker进程工作流程
当一个 worker 进程在 accept() 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,一个完整的请求。

一个请求,完全由 worker 进程来处理,而且只能在一个 worker 进程中处理。

这样做带来的好处:

1、节省锁带来的开销。每个 worker 进程都是独立的进程,不共享资源,不需要加锁。同时在编程以及问题查上时,也会方便很多。

2、独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程。当然,worker 进程的也能发生意外退出。

多进程模型每个进程/线程只能处理一路IO,那么 Nginx是如何处理多路IO呢?

如果不使用 IO 多路复用,那么在一个进程中,同时只能处理一个请求,比如执行 accept(),如果没有连接过来,那么程序会阻塞在这里,直到有一个连接过来,才能继续向下执行。

多路复用,允许我们只在事件发生时才将控制返回给程序,而其他时候内核都挂起进程,随时待命。

再来看IO多路复用

IO多路复用是利用select、poll、epoll同时监视多个流的IO事件的状态:
1   空闲时把当前进/线程阻塞
2   当有一个或多个流由IO事件发生时,就从阻塞态中唤醒,select 和 poll会轮询一遍所有的流, 而epoll只轮询那些真正发生变化的流,并且依次处理就绪了的流。
  1. select 服务端一直在轮询,如果有客户端链接上来就创建一个连接放到数组array中,并继续轮询,如果在轮询的过程中有客户端发生IO事件就去处理;select只能监视1024个连接(一个进程只能创建1024个文件);而且存在线程安全问题;

  2. epoll:也是监测IO事件,如果发生IO事件,它会告诉你是哪个连接发生了事件,而不是轮询访问。而且它epoll 线程安全,但是只有linux平台支持

使用epoll 框架

for( ; ; )  //  无限循环
      {
          nfds = epoll_wait(epfd,events,20,500);  //  最长阻塞 500s
          for(i=0;i<nfds;++i)
          {
              if(events[i].data.fd==listenfd) //有新的连接
              {
                  connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
                  ev.data.fd=connfd;
                 ev.events=EPOLLIN|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
             }
             else if( events[i].events&EPOLLIN ) //接收到数据,读socket
             {
                 n = read(sockfd, line, MAXLINE)) < 0    //读
                 ev.data.ptr = md;     //md为自定义类型,添加数据
                 ev.events=EPOLLOUT|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
             }
             else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
             {
                 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
                 sockfd = md->fd;
                 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                 ev.data.fd=sockfd;
                 ev.events=EPOLLIN|EPOLLET;
                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
             }
             else
             {
                 //其他的处理
             }
         }
     }

O多路复用的优势在于,当处理消耗 相比IO消耗 , 几乎可以忽略不计时,这时处理大量的并发IO,而不会消耗太多CPU/内存。

**典型的例子是nginx做代理,**代理的转发逻辑相对比较简单直接,那么IO多路复用很适合。相反,如果是一个做复杂计算的场景,计算本身可能是个 指数复杂度的东西,IO不是瓶颈。那么怎么充分利用CPU或者显卡的核心多干活才是关键。

IO多路复用适合处理很多闲置的IO,因为IO socket的数量的增加并不会导致进/线程数的增加,也就不会引起stack内存,内核对象,切换时间的损耗。

因此 IO多路复用非常适合长链接场景。

另外,IO多路复用不会遇到并发编程的一系列问题。

如果做不到“处理过程相对于IO可以忽略不计”,IO多路复用的并不一定比线程池方案更好。

总结

IO多路复用适合用于:处理过程简单,进/线程池适合处理过程复杂 的情况

IO多路复用+单进(线)程比较省资源适合处理大量的闲置的IO

IO多路复用+多单进(线)程与线程池方案相比有好处,但是并不会
有太大的优势如果压力很大,什么方案都得跪,这时就得扩容。当然因为IO多路复用+单进(线)程比较省资源,所以扩容时能省钱。

发布了110 篇原创文章 · 获赞 44 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_44030580/article/details/105435927