ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

C/C++编程:nginx服务器模型

2021-09-28 18:00:06  阅读:136  来源: 互联网

标签:请求 编程 worker C++ nginx accept 进程 事件


nginx在启动后,会有一个master进程和多个worker(工作)进程

  • master进程主要用来管理worker进程,包含
    • 接收来自外界的信号
    • 向各worker进程发送信号
    • 监控worker进程的运行状态,当worker进程异常退出后,会自动重新启动新的worker线程

也就是说,master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务,平滑升级、更换日志文件、配置文件实时生效等功能。我们要控制nginx,只需要通过 kill 向master进程发送信号就行了

  • 而基本的网络事件,则是放在worker进程中来处理了。
    • 多个worker进程之间是对等的,他们同等竞争来自客户端的请求,个进程之间是相互独立的。
    • 一个请求,只可能在一个worker进程中处理;一个worker进程,不可能处理其他进程的请求
    • worker进程的个数是可以设置的,一般我们会设置与机器CPU核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。

nginx的进程模型,可以由下图来表示:
在这里插入图片描述

在nginx启动后,如果我们要操作nginx,要怎么做呢?

从上文中我们可以看到,master来管理worker进程,所以我们只需要于master进程通信就可以了

  • master进程会接收来自外界发来的信号,在根据信号做不同的事情

  • 所以我们要控制nginx,只需要通过kill向master进程发送信号就行了。

    • 比如kill -HUP pid,则是告诉nginx,从容的重启nginx,我们一般用这个信号来重启nginx,或者重新加载配置
    • 因为是从容的重启,所以服务是不中断的
  • 那么master进程在接收到HUP信号后是怎么做的呢?

    • 首先,master进程在接到信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以光荣退休了
    • 新的worker在启动后,就开始接收新的请求,而老的worker来收到来自master的信号后,就不再接收新的请求,并且在当前进程中所有未处理完的请求处理完成后,再退出。
  • 当然,直接给master进程发送信号,这是比较老的操作方式,nginx在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如:

    • ./nginx -s reload,就是来重启nginx
    • ./nginx -s stop,就是来停止nginx的运行
  • 如何做到的呢?我们还是拿reload来说,

    • 执行这个命令时,master收到这个信号以后先启动一个新的Nginx进程
    • 而新的Nginx进程在解析到reload参数后,就知道是要控制Nginx来重新加载配置文件,它会向master进程发送信号
    • 然后master会重新加载配置文件,在启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以退休了
    • 新的worker启动之后就可以以新的配置文件接收新的请求了(热部署的原理。)

在这里插入图片描述

现在,我们知道了我们在操作nginx的时候,nginx内部做了什么事情,那么,worker进程又是如何处理请求的呢?

我们前面有提到,worker进程之间是平等的,每个进程,处理请求的机会也是一样的。

当我们提供80端口的HTTP服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢

  • 首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程
    • 所有worker进程的listenfd会在新连接到来时变得可读(在linux2.6以前是这样,但是2.6以及以后,所有worker进程的listenfd在新连接到来时只有一个变得可读)
    • 为保证只有一个进程处理该连接,所有worker进程在册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。
    • 当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。(linux已经解决了accept问题

在这里插入图片描述
在这里插入图片描述
也就是说,客户端请求到一个master之后,worker获取任务的机制不是直接分配也不是轮询,而是一种争取的机制,“抢”到任务后再执行任务,比如选择目标服务器tomcat等,然后返回结果。

在这里插入图片描述

惊群现象

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

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

  • nginx提供了一个叫做accept的东西,这是一个加在accept上的一把共享锁。即每个worker进程在执行accept之前都需要先获取锁,获取不到就放弃执行accept()。有了这把锁之后,同一时刻,就只会有一个进程去accept(),这样就不会有惊群问题了(当accept()之后就释放锁,这个时候内核只会唤醒一个阻塞在accept_mutex上的进程,其他进程还是会休眠)。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。

  • 其实在linux2.6版本以后,linux内核已经解决了accept()函数的“惊群”现象,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程(线程),所以如果服务器采用accept阻塞调用方式,在最新的linux系统中已经没有“惊群效应”了,所以这个选项可以关掉

从上面我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。那这样有什么好处呢?

  • 首先,对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多。
  • 其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程
  • 当然,worker进程的异常退出,肯定是程序有bug了,异常退出,会导致当前worker上的所有请求失败,不过不会影响到所有请求,所以降低了风险

总的来说,使用多进程模式,不仅能够提高并发率,而且进程之间彼此独立,一个worker进程挂了也不会影响其他worker进程

当然,worker进程竞争监听客户端的连接请求也是有它的缺点的。比如说

  • 可能所有的请求都被一个worker进程给竞争获取了,导致其他进程都比较空闲,而某一个进程会处于忙碌的状态,这种状态可能还会导致无法及时响应连接而丢弃discard掉本有能力处理的请求
  • 这种不公平的现象,是需要避免的,尤其是在高可靠web服务器环境下。
  • 针对这种现象,Nginx采用了一个是否打开accept_mutex选项的值,ngx_accept_disabled标识控制一个worker进程是否需要去竞争获取accept_mutex选项,进而获取accept事件。

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

  • 如果不使用IO多路复用,那么在一个进程中,同时只能处理一个请求,比如执行accept(),如果没有连接过来,那么程序会阻塞在这里,直到有一个连接过来,才能继续往下执行
  • 而多路复用,允许我们只在事件发生时才将控制返还给程序,而其他时候内核都挂起进程,随时待命

这也是nginx进程为什么这么快的原因:nginx采用IO多路复用模型nginx:

  • nginx会注册一个事件:“如果来自一个新客户端的连接请求到来,再通知我”,此后只有连接请求到来,服务器才会执行accept()来接收请求。
  • 又比如向上游服务器(比如 PHP-FPM)转发请求,并等待请求返回时,这个处理的worker不会在这阻塞,它会在发送完请求后,注册一个事件:“如果缓冲区接收到数据了,告诉我一声,我再将它读进来”,于是进程空闲下来等待事件发生

上面讲了很多关于nginx的进程模型,接下来,我们来看看nginx是如何处理事件的。

  • 有人可能就要问了,nginx采用多worker的方式来处理请求,每个worker里面只有一个主线程,那能够处理的并发数很有限啊,多少个worker就能处理多少个并发,何来高并发呢?

  • 非也,这就是nginx的高明之处,nginx采用了异步非阻塞的方式来处理请求,也就是说,nginx是可以同时处理成千上万的请求的

  • 想想apache的常用工作方式(apache也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用)每个请求都会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。

  • 这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的CPU开销很大,自然性能就上不去了,而这些开销完全是没有意义的

为什么nginx可以采用异步非阻塞的方式来处理呢?或者异步非阻塞到底是怎么回事呢?

我们先回到原点,看看一个请求的完整过程

  • 首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据
  • 具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作
  • 如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,就只能等了,等事件准备好了,你再继续吧
  • 阻塞调用会进入内核等待,CPU就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢,CPU空闲下来没人用,CPU利用率自然就上不去了,更别谈高并发了
  • 好吧,你说加进程数,这跟Apache的线程模型有什么区别,注意,不要增加无谓的上下文切换
  • 所以,在nginx里,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞了。
  • 非阻塞就是,事件没有准备好,马上返回EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就可以先去做其它事情,然后再来看看事件好了没
  • 虽然不阻塞了,但你得不时地过来检查一下事件的状态,你可以做更多的事情了,但是带来的开销也是不小的。
  • 所以,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。
  • 这种机制正好解决了我们上面的两个问题,拿epoll为例(在后面的例子中,我们多以epoll为例子,以代表这一类函数),当事件没有准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。
  • 这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没有准备好时,才会在epoll里面等着
  • 这样,我们就可以并发的处理大量的请求了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个,只是再请求间不断地切换而已,切换也是因为异步事件未准备好,而主动让出的
  • 这里的切换是没有代价的,你可以理解为循环处理多个准备好的事件,事实上就是这样
  • 与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。

我们之前说过,推荐设置worker的个数为cpu的核数,这里就可以知道原因了

  • 因为更多的 worker 数,只会导致进程相互竞争 cpu ,从而带来不必要的上下文切换。
  • 而且,nginx为了更好的理由多核特性,提供了CPU亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效

像这种小的优化在nginx中非常常见,同时也说明了nginx作者的苦心孤诣。比如,nginx在做4个字节的字符串比较时,会将4个字符转换成一个int型,再作比较,以减少cpu的指令数等等。

现在,知道了nginx为什么会选择这样的进程模型与事件模型了。对于一个基本的web服务器来说,事件通常有三种类型,网络事件、信号、定时器。从上面我们可以知道,网络事件通过异步非阻塞可以很好的解决掉。那么如何处理信号与定时器

  • 首先,信号的处理。对nginx来说,有一些特定的信号,代表着特定的意义。信号会中断掉程序当前的运行,在改变状态后,继续执行。如果是系统调用,则可能会导致系统调用的失败,需要重入。关于信号的处理,大家可以学习一些专业书籍,这里不多说。对于nginx来说,如果nginx正在等待事件(epoll_wait时),如果程序收到信号,在信号处理函数处理完后,epoll_wait会返回错误,然后程序可再次进入epoll_wait调用。

  • 另外,再来看看定时器。由于epoll_wait等函数在调用的时候是可以设置一个超时时间的,所以nginx借助这个超时时间来实现定时器。nginx里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入epoll_wait前,先从该红黑树里面拿到所有定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。所以,当没有事件产生,也没有中断信号时,epoll_wait会超时,也就是说,定时器事件到了。这时,nginx会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件。由此可以看出,当我们写nginx代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件。

  • Nginx源码阅读笔记-Master Woker进程模型

  • 被问懵逼:谈谈 Nginx 快的原因?

标签:请求,编程,worker,C++,nginx,accept,进程,事件
来源: https://blog.csdn.net/zhizhengguan/article/details/120529882

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有