上篇概述了web系统和两大类web服务器,并在后文介绍了当前应用最广的Apache服务器。Apache服务器作为历史最悠久的服务器,对web服务器的发展起着不可磨灭的作用,大多数服务器的工作原理和流程都是基于Apache的服务框架完成的。
近年来一款名为Nginx的服务器走向流行, NGINX以高性能的负载均衡器,缓存,和web服务器闻名。虽然市场份额远不及Apache,但世界上40%的最忙碌的网站如FaceBook都在运用Nginx,默认的 Nginx 和 Linux 设置可以很好的工作。本篇解读一下Nginx的工作原理和与Apache的异同。
Nginx的工作模块和工作原理
Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。
工作模块的分类可以从结构和功能上各分三类。从结构上大体分为如下几部分:
用户根据自己的需要开发的模块都属于第三方模块。正是有了这么多模块的支撑,Nginx的功能才会如此强大。
Nginx的模块从功能上分为如下三类:
Nginx的工作流程就是基于按功能划分的三类模块运行的,Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。下图摘自网上,展示了Nginx模块常规的HTTP请求和响应过程。
Nginx的进程模型
Nginx有单工作进程和多工作进程两种模式。单工作进程指除主进程外还有一个工作进程且该工作进程是单线程的。多工作进程模式下,每个工作进程包含多个线程。Nginx默认为但工作进程模式。
Nginx启动后,会有一个master进程和多个worker进程(worker进程非之前提到的工作进程)。
master进程
主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。
worker进程
worker进程处理基本的网络事件。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致。
worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接。如何做到只有一个worker进程处理这个连接呢?
①首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。
②所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。
③当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,完成请求与响应。
Nginx进程模型如下所示:
Nginx为什么性能好
性能的好坏一定是工作方式直接造成的,相较于Apache,Nginx的优势在于处理高并发的性能好于前者。回忆一下Apache的工作方式。每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了。
Nginx采用的是进程的工作方式,连接由每个worker进程处理,独立的进程不需要加锁,省去了大量的锁开销。就算一个进程因异常而终止也不会影响其他进程。但问题又来了,之前提到worker进程的数量一般等于CPU数量,一般地电脑CPU数量也就几个,每个worker进程能只有一个主线程,这又如何处理高并发呢?
Nginx的另一个高明之处就是采用了异步非阻塞方式,该方式可以同时处理成千上万个请求。虽然Apache也有异步非阻塞版本,但语气自身一些模块冲突,不常用。流行的Apache大多都是一步阻塞的工作方式。下面来聊聊这两种方式。
异步阻塞和异步非阻塞
异步非阻塞到底是怎么回事呢?我们先回忆一下一个完整的请求响应流程。首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,如果采用阻塞调用,那就只能等了,等事件准备好了再继续。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢,cpu空闲下来没人用,cpu利用率自然上不去了,更别谈高并发了。
而如果采用非阻塞调用,事件没有准备好,马上返回EAGAIN,告诉你,事件还没准备好,过会再来吧。过一会,再来检查一下事件直到事件准备好了为止,在这期间,可以先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但不时地过来检查一下事件的状态,带来的开销也是不小的。所以,才会有了异步非阻塞的事件处理机制。
异步非阻塞机制具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。以epoll为例,当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这里的并发请求,是指未处理完的请求,线程只有一个,所以只能同时处理一个请求,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。
推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。
Nginx这块不好理解,Chuck也是看了好几天才看出些眉目,建议多读几遍,接下去的文章将 讲解Tomcat和Nginx + Tomcat的部署。