基于nginx进程/事件模型的总结


nginx进程模型

nginx进程模型这块个人认为非常灵活,可以是前台单进程,也可以是master-worker形式的多进程模型。

正常情况下,不会选择前台单进程的模型,除非是做调试使用。 所以重点讨论master-worker这种多进程模型。 master-worker这种多进程模型就比如是一个饭店经理和一帮店小二。 工作模式如下:

饭店经理主要是管理这帮小弟,他从外界收到各类活儿,会喊一声通知小弟们有这么个客人来招待,小弟们之间都是平等的,没有地位悬殊之差,他们会通过竞争来得到经理给的活儿,第一个过来照应的小弟便得到该任务,得到该任务后便进行任务处理,直接交互给下发任务的客人。同时经理也会监控小弟们的工作状态,确保他们正常工作,一旦有个人失联或者不干了,经理需要赶紧招一个小弟来补上。

其中经理、小弟们都是独立进程。这种独立占用进程的模型好处如下:

  1. 对于每个worker进程来说,独立进程不需要加锁,避免锁带来了的开销
  2. 互相之间不会影响,一个进程退出后,其余进程可以照常工作,服务不会被中断

nginx事件模型

了解进程模型后,我们需要探讨下事件模型的处理,也就是我虽然知道了这个团伙的组织架构和基本职责,但是具体任务处理细节还需要再进一步看看呐。 在讲下面之前,我们需要了解两个基本点

  1. 异步非阻塞
  2. 线程上下文切换带来的cpu开销较大

接下来我们探讨一种情形,一个小弟同时招呼了三位客人,那么有以下几种方式供他选择

  1. 一个个的客人挨个处理,从客人点餐到下单,一直站在旁边服务,然后结束任务,开始服务下一个客人
  2. 先服务第一个客人,把菜单给拿过去,再服务第二个和第三个客人。然后再不断跑去问每个客人点餐完毕了没有
  3. 先服务三个客人让他们点餐,等到有人点餐完毕招呼自己时再过去

第三种方式看起来对于小弟和客人都是最节省体力的一种方式,也就是我们上面所说的异步非阻塞。接下来,用官方语言来描述下nginx的事件处理模型。

异步非阻塞,提供了一种机制,你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。拿epoll为例,当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件,事实上就是这样的。

与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。

更多的并发数,只是会占用更多的内存而已。 有人之前有对连接数进行过测试,在24G内存的机器上,处理的并发请求数达到过200万。

现在的网络服务器基本都采用上述这种方式,这也是nginx性能高效的主要原因。

还有一个小的tips,推荐设置worker的个数为cpu的核数,这种就好比每个服务员得有一个自己的点菜本,不然两个服务员会因为点菜本先争抢起来。在这里就很容易理解了,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。

I/O模型介绍

首先我们需要先明确以下几个概念

  • 用户空间和内核空间
  • 进程切换
  • 进程的阻塞
  • 文件描述符
  • 缓存I/O
  • 几种I/O模式

用户空间和内核空间

关键明确两点,为何要分为两个空间、空间是以什么做区分的。

  1. 为了保证用户进程不能直接操作内核,保证内核的安全,将虚拟空间划分为内核和用户两个空间。
  2. 空间是通过划定字节区域来进行区分。

进程切换

为了控制进程的执行,必须给予内核挂起正在cpu上运行的进程的能力,并可以恢复之前挂起的某个进程。其实也是一种调度,官方称为进程切换。 那么,在内核调度进程的过程中,发生的变化如下

  1. 保存处理机上下文,包括程序计数器和其他寄存器
  2. 更新PCB信息
  3. 把进程的PCB移入相应的队列,如就绪、在某时间阻塞等
  4. 选择恢复的进程进行执行,并更新PCB
  5. 更新内存管理的数据结构
  6. 恢复处理机上下文

具体参考该篇文章:进程切换

进程阻塞

这个按照字面意思来理解就行,主要是搞懂两点,什么情况下会发生阻塞、阻塞状态对于资源的占用情况如何。

  1. 正在进行中的进程,假如说由于请求系统资源失败、新数据尚未到达等原因导致无法继续进行,将会使自己由运行状态转为阻塞状态。
  2. 在阻塞状态下的进程是不会占用cpu资源的。

文件描述符fd 平时在linux会经常通过fopen一个文件得到fd,我理解就是一个文件的索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

缓存I/O 表示一种I/O流向。即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

几种I/O模型

一、阻塞I/O

  1. 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。
  2. 用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

二、非阻塞I/O

  1. 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
  2. 从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
  3. 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
  4. 一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,用户进程其实是需要不断的主动询问kernel数据好了没有。

三、多路复用I/O 参考redis中多路复用即可。

四、异步I/O

  1. 用户进程发起read操作之后,立刻就可以开始去做其它的事。
  2. 另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block.
  3. kernel会等待数据准备完成,然后将数据拷贝到用户内存
  4. 当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成

用一个故事讲述这4个I/O就是

有A,B,C,D四个人在钓鱼:

A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;

B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;

C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;

D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。