深入理解Nginx
一、为什么Nginx要用root用户启动
通常Nginx作为Web服务器和反向代理,会监听80或433端口。在Linux中,要开启1024以内的端口监听服务,必须用特权身份运行,所以Nginx master进程必须以root身份运行。
二、master与worker
master进程由root运行,worker在编译安装情况下默认以nobody用户身份运行,用yum安装默认为nginx用户身份运行,也可以在配置中用user指令来指定由哪个用户运行。
master :master进程主要用来管理worker进程,只能有一个master进程,接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
worker:处理基本的网络事件。可以有多个worker进程,且相互之间是平等的,会竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。
1. worker进程如何处理请求
当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后断开连接。
2. Nginx进程模型的优势
对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销。
采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程。
3. reload原理
我们使用nginx -s reload后,会向master进程发送HUP信号
master进程校验配置文件语法是否正确
master进程打开新的监听端口
master进程用新配置启动新的worker进程
master进程向老worker发送QUIT信号
老worker进程关闭监听句柄,处理完当前连接后结束进程
三、异步非阻塞
Nginx多进程事件模型
Nginx采用多worker的方式处理请求,每个worker只有一个主线程,那能够处理多少并发就看有多少个worker了?其实不然,Nginx采用了异步非阻塞的方式,这样就可以同时处理成千上万个请求。
1. 同步与异步的理解
同步与异步的重点在消息通知的方式上,也就是调用结果通知的方式。
同步:当一个同步调用发出去后,调用者要一直等待调用结果的通知后,才能进行后续的执行。
异步:当一个异步调用发出去后,调用者不能立即得到调用结果的返回。
异步调用,要想获得结果,一般有两种方式:
主动轮询异步调用的结果;
被调用方通过callback来通知调用方调用结果
2. 阻塞与非阻塞的理解
阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。
阻塞阻塞调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活
非阻塞非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回。
3. I/O多路复用
所谓的I/O复用,就是多个I/O可以复用一个进程。I/O多路复用允许进程同时检查多个fd(file descriptor,进程独有的文件描述符表的索引),以找出其中可执行I/O操作的fd。
系统调用select()和poll()来执行I/O多路复用。在Linux2.6中引入的epoll()是select()的升级版,提供了更高的性能。通过I/O复用,我们可以在一个进程处理大量的并发I/O。
1) 初级版I/O复用
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”然后进程就一直从头到尾问这10000个连接,如果这10000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低。
那么,如果发明一个代理,每次能够知道哪个连接有了I/O流事件,就可以避免无意义的空转了,为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。
2) 升级版I/O复用 select()
select()采用轮询的方式来检查fd是否就绪,当fd数量较多时,性能欠佳。因为从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
3) 高级版I/O复用 epoll()
epoll能更高效的检查大量fd,UNIX中提供了类似功能的kqueue调用。
epoll可以理解为event poll,不同于忙轮询和无差别轮询,当连接有I/O流事件产生的时候,epoll就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个事件。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的)
4. Nginx的异步非阻塞
一个worker进程可以同时处理的请求数只受限于内存大小,而且在架构设计上,不同的worker进程之间处理并发请求时几乎没有同步锁的限制,worker进程通常不会进入睡眠状态,因此,当Nginx上的进程数与CPU核心数相等时(最好每一个worker进程都绑定特定的CPU核心),进程间切换的代价是最小的。
在一个Web服务中,延迟最多的就是等待网络传输。基本的网络事件,由worker进程来处理。在一个请求需要等待的时候,worker可以空闲出来处理其他的请求,少数几个worker进程就能够处理大量的并发。
举例来说:同样的4个进程,如果采用一个进程负责一个request的方式;那么,同时进来4个request之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第5个request进来了。就无法及时反应了,因为4个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个request,就新开个进程来处理。
nginx不这样,每进来一个request,会有一个worker进程去处理。但不是全程的处理,而是处理到可能发生阻塞的地方。比如向后端服务器转发request,并等待请求返回。那么,这个处理的worker不会这么一直等着,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。