Nginx架构原理科普

Nginx架构原理科普

Nginx(发音为“engine X”)是由俄罗斯人 Igor Sysoev 编写的一个免费的、开源的、高性能的 HTTP 服务器和反向代理,也是一个电子邮件(IMAP/POP3/SMTP)代理服务器,其特点是占有内存少,并发能力强。Nginx 因为它的稳定性、丰富的模块库、灵活的配置和较低的资源消耗而闻名 。目前 Nginx 已经被 F5 收购。

Nginx由内核和一系列模块组成,内核提供web服务的基本功能,如启用网络协议,创建运行环境,接收和分配客户端请求,处理模块之间的交互。Nginx的各种功能和操作都由模块来实现。Nginx的模块从结构上分为核心模块、基础模块和第三方模块。这样的设计使Nginx方便开发和扩展,也正因此才使得Nginx功能如此强大。

Nginx 社区分支:

  • Openresty:由章宜春开发的,最大特点是引入了 ngx_lua 模块,支持使用 Lua 开发插件,并且集合了很多丰富的模块,以及 Lua 库。
  • Tengine:主要是淘宝团队开发。特点是融入了因淘宝自身的一些业务带来的新功能。
  • Nginx 官方版本,更新迭代比较快,并且提供免费版本和商业版本。

进程模型与架构原理

Nginx 服务器启动后,产生一个 Master 进程(Master Process),Master 进程执行一系列工作后产生一个或者多个 Worker 进程(Worker Processes)。 其中,Master 进程用于接收来自外界的信号,并向各 Worker 进程发送信号,同时监控 Worker 进程的工作状态。当 Worker 进程退出后(异常情况下),Master 进程也会自动重新启动新的 Worker 进程。Worker 进程则是外部请求真正的处理者。

多个 Worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 Worker 进程中处理,一个 Worker 进程不可能处理其它进程的请求。 Worker 进程的个数是可以设置的,一般我们会设置与机器 CPU 核数一致。同时,Nginx为了更好的利用多核特性,具有 CPU 绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效(CPU affinity)。所有的进程的都是单线程(即只有一个主线程)的,进程之间通信主要是通过共享内存机制实现的。

Nginx在启动后,在系统中会以后台模式(daemon)运行,后台进程包含一个 Master 进程和多个 Worker 进程。我们也可以手动地关掉后台模式,让Nginx在前台运行,并且通过配置让 Nginx 取消 Master 进程,从而可以使Nginx以单进程方式运行。很显然,生产环境下我们肯定不会这么做,所以关闭后台模式,一般是用来调试用的。Nginx是以多进程的方式来工作的,当然Nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式,也是Nginx的默认方式。Nginx采用多进程的方式有诸多好处,本文主要讲解Nginx的多进程模式。

Nginx的进程模型,可以由下图来表示:

在 Nginx 启动后,如果我们要操作 Nginx,要怎么做呢?前面我们说过, Master 进程用来管理 Worker进程,所以我们只需要与 Master 进程通信就行了。 Master 进程会接收来自外界发来的信号,再根据信号做不同的事情。所以我们要控制 Nginx,只需要通过 kill 命令向 Master 进程发送信号就行了。比如kill -HUP pid,则是告诉 Nginx 从容地重启。我们一般用这个信号来重启 Nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的。

Master 进程在接收到 HUP 信号后是怎么做的呢?首先 Master 进程在接到信号后,会先重新加载配置文件,然后再启动新的 Worker 进程,并向所有老的 Worker 进程发送信号,告诉他们可以光荣退休了。新的 Worker 在启动后,就开始接收新的请求,而老的 Worker 在收到来自 Master 的信号后就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后再退出。

当然,直接给 Master 进程发送信号,这是比较老的操作方式,Nginx 在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如 ./nginx -s reload就是来重启Nginx的,./nginx -s stop就是来停止Nginx的运行。如何做到的呢?我们还是以 reload 为例,我们看到在执行命令时启动一个新的 Nginx 进程,而新的 Nginx 进程在解析到 reload 参数后,就知道我们的目的是控制 Nginx 来重新加载配置文件了,它会向 Master 进程发送信号,然后接下来的动作,就和我们直接向 Master 进程发送信号一样了。

现在,我们知道了当我们在操作Nginx的时候,Nginx内部做了些什么事情,那么,Worker 进程又是如何处理请求的呢?我们前面有提到,Worker进程之间是平等的,每个进程处理请求的机会也是一样的。当我们提供80端口的 HTTP 服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?

首先,每个 Worker 进程都是从 Master 进程fork过来,在 Master 进程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多个 Worker 进程。所有 Worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 Worker 进程在注册 listenfd 读事件前抢互斥锁accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,在读事件里调用 accept 接受该连接。当一个 Worker 进程在 accept 这个连接之后,就开始读取、解析、处理请求,在产生数据后再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求完全由 Worker 进程来处理,而且只在一个 Worker 进程中处理。

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

其实除了 Master 进程和 Woker 进程之外,Nginx 中还有两个特殊用途的进程:缓存加载器进程(Cache Loader )和 缓存管理器进程(Cache Manager)。Cache Loader 进程是在Nginx服务启动一段时间后由主进程生成,在缓存元数据重建完成后就自动退出。Cache Manager进程一般存在于主进程的整个生命周期,负责对缓存索引进行管理。通过缓存机制,可以提高对请求的响应效率,进一步降低网络压力。

根据上面的分析,我们可以将Nginx服务器的结构大致分为主进程、工作进程、后端服务器和缓存等部分。下图中展示了各个部分之间的联系和交互:

热升级

热升级是指在不停止服务的情况下更换 Nginx 的binary文件。热升级会经历以下几个步骤:

第一步是把旧的 Nginx binary 文件替换为新的,之所以说只替换 binary 文件是因为大部分场景下,我们新编译的 nginx 文件所指定的相应的配置选项,比如说配置文件的目录在哪里?log 的所在目录在哪里?必须保持和老的 Nginx 是一致的,否则的话没有办法复用 nginx.conf 文件,如果我们仅仅替换 binary 文件,请注意要备份,另外在新版本的 Linux 中,会要求在覆盖一个正在使用的文件时需要用 cp -f 才能够替换。

第二步,我们向现有老的 Master (Old) 进程发生 USR2 信号,之后 Master (Old) 进程会将修改 pid 文件名,添加 后缀 .oldbin。这一步是在为新的 Master 进程让路,虽然 Master、Worker 进程都可以接受信号,但是为了管理方便,通常不对 Worker 进程直接发送信号,所以我们依赖于 Master 进程,他必须把他的 pid 保存下来,为了新的 Master 使用 pid.bin 这个文件名,所以把老的 pid 文件改为 pid.oldbin。

第三步,使用新的 binary 文件启动新的 Master (New) 进程。所以到现在为止,会出现两个 Master 进程:Master(Old) 和 Master (New),如下图所示。 Master (New) 进程会自动启动新的 Worker 进程。这里新的 Master (New) 进程是怎么样启动的呢?它其实是老的 Master(Old) 进程的子进程,不过这个子进程是使用了新的 binary 文件带入来启动的。

第四步,向 Master(Old) 进程发送 QUIT 信号。怎么样找到 Master(Old) 进程呢?我们可以根据 ps 命令或者通过 .oldbin 文件查找到 Master(Old) 进程的进程号,然后向这个进程号发送 QUIT 信号,那么 Master(Old) 进程会优雅的关闭老 Worker 进程,这样我们的热升级就结束。

整个过程中,Master(Old) 进程是一直存活的,这是为了方便我们进行回滚,也就是发现新的 Nginx 程序有问题了,这个时候因为 Master(Old) 进程还在,可以向 Master(Old) 进程发送 HUP 信号,相当于执行了一次 reload,会启动新的 Worker 进程,然后再向 Master (New) 进程发送 QUIT 信号,也就是要求新的 Worker 进程优雅退出,就实现了回滚。当退出老 Master(Old) 进程以后不能进行回滚。如果想回滚,就需要再走一次热升级流程,用备份好的老 Nginx 文件作为新的热升级文件(因此建议备份旧的 Nginx 文件)。

在一个父进程退出,而它的一个或多个子进程还在运行时,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。所以老 Master(Old) 进程退出后,新的 Master(Old) 进程并不会退出。

以上就是热升级流程,我们可以通过他实现不停机更新我们的 Nginx,这为我们持续使用 Nginx 的最新特性提供了帮助。

模块化

Nginx 的内部结构是由内核和一系列的功能模块所组成,高度模块化的设计是 Nginx 的架构基础。内核的设计非常微小和简洁,完成的工作也非常简单。Nginx的各种功能和操作都由模块来实现,每个模块就是一个功能模块,只负责自身的功能,模块之间严格遵循“高内聚,低耦合”的原则。

模块从结构上分为:核心模块(HTTP模块、EVENT模块和MAIL模块)、 基础模块(HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块)、 第三方模块( HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块)。

模块从功能上还可以分为以下几种:

  • Handlers(处理器模块):此类模块直接处理请求,并进行输出内容和修改 headers 信息等操作。Handlers 处理器模块一般只能有一个。
  • Filters(过滤器模块):此类模块主要对其他处理器模块输出的内容进行修改操作,最后由 Nginx 输出。
  • Proxies(代理类模块):此类模块是 Nginx 的 HTTP Upstream 之类的模块,这些模块主要与后端一些服务比如FastCGI 等进行交互,实现服务代理和负载均衡等功能。

Nginx(内核)本身做的工作实际很少,当它接到一个 HTTP 请求时,它仅仅是通过查找配置文件将此次请求映射到一个 location block,而此 location 中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做 Nginx 真正的劳动工作者。通常一个 location 中的指令会涉及一个 Handler 模块和多个 Filter 模块(当然,多个location可以复用同一个模块)。Handler模块负责处理请求,完成响应内容的生成,而 Filter 模块对响应内容进行处理。

常用使用场景

Nginx的应用场景非常的广泛,下面就以几种常见的为例做一下简单的介绍。

正向代理

正向代理其实就是说客户端无法主动或者不打算主动去向某服务器发起请求,而是委托了 Nginx 代理服务器去向服务器发起请求,并且获得处理结果,返回给客户端。

举个栗子:广大社会主义接班人都知道,为了保护祖国的花朵不受外界的乌烟瘴气熏陶,国家对网络做了一些“优化”,正常情况下是不能外网的,但作为程序员的我们如果没有谷歌等搜索引擎的帮助,再销魂的代码也会因此失色,因此,网络上也曾出现过一些fan qiang技术和软件供有需要的人使用,如某VPN等,其实VPN的原理大体上也类似于一个正向代理,也就是需要访问外网的电脑,发起一个访问外网的请求,通过本机上的VPN去寻找一个可以访问国外网站的代理服务器,代理服务器向外国网站发起请求,然后把结果返回给本机。

正向代理配置实例:

resolver 114.114.114.114 8.8.8.8;
server {
resolver_timeout 5s;
listen 81;
location / {
proxy_pass http://$host$request_uri;
}
}

resolver是配置正向代理的DNS服务器,listen 是正向代理的端口,配置好了就可以在浏览器上面或者其他代理插件上面使用服务器ip+端口号进行代理了。

反向代理

反向代理( Reverse Proxy )方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。正向代理在客户端侧,反向代理在服务端侧。

简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。 下面贴上一段简单的实现反向代理的配置:

server {  
listen 80;
server_name localhost;
location / {
proxy_pass http://www.google.com;

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; #获取客户端真实IP
}
}

保存配置文件后启动 Nginx,这样当我们访问 http://localhost 的时候,就相当于访问 http://www.google.com 了。

负载均衡

负载均衡其意思就是分摊到多个操作单元上进行执行,例如:Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。简单而言就是当有2台或以上服务器时,根据规则将请求分发到指定的服务器上处理,负载均衡配置一般都需要同时配置反向代理,通过反向代理跳转到负载均衡。

而Nginx目前支持自带3种负载均衡策略,还有2种常用的第三方策略:

  1. 轮询(RR):默认的策略。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器宕掉,能自动剔除。
  2. 权重(weight):可以给不同的后端服务器设置一个权重值(weight),用于调整不同的服务器上请求的分配率。权重数据越大,被分配到请求的几率越大;该权重值,主要是针对实际工作环境中不同的后端服务器硬件配置进行调整的。
  3. ip_hash:每个请求按照发起客户端的 ip 的 hash 结果进行匹配,这样的算法下一个固定 ip 地址的客户端总会访问到同一个后端服务器,这也在一定程度上解决了集群部署环境下 Session 共享的问题。
  4. fair:智能调整调度算法,动态的根据后端服务器的请求处理到响应的时间进行均衡分配。响应时间短处理效率高的服务器分配到请求的概率高,响应时间长处理效率低的服务器分配到的请求少。Nginx 默认不支持 fair 算法,如果要使用这种调度算法,请安装 upstream_fair 模块。
  5. url_hash:按照访问的 URL 的 hash 结果分配请求,每个请求的 URL 会指向后端固定的某个服务器,可以在 Nginx 作为静态服务器的情况下提高缓存效率,示例如下。同样要注意 Nginx 默认不支持这种调度算法,要使用的话需要安装 Nginx 的 hash 软件包。

Nginx 支持同时设置多组的负载均衡,用来给不同的 server 来使用。与此同时,upstream可以设定每个后端服务器在负载均衡调度中的状态,相关配置示例如下:

upstream #自定义组名 {
server x1.google.com; #可以是域名
server x2.google.com;
#server x3.google.com
#down 不参与负载均衡
#weight=5; 权重,越高分配越多
#backup; 预留的备份服务器
#max_fails 允许失败的次数
#fail_timeout 超过失败次数后,服务暂停时间
#max_coons 限制最大的接受的连接数
#根据服务器性能不同,配置适合的参数

#server 106.xx.xx.xxx; 可以是ip
#server 106.xx.xx.xxx:8080; 可以带端口号
#server unix:/tmp/xxx; 支出socket方式
}

HTTP服务器

Nginx本身也是一个静态资源的服务器,当只有静态资源的时候,就可以使用Nginx来做服务器,同时现在也很流行动静分离,就可以通过Nginx来实现,首先看看Nginx做静态资源服务器。

server {
listen 80;
server_name localhost;
location / {
root /root/website/;
index index.html;
}
}

这样如果访问 http://localhost 就会默认访问到 /root/website/ 目录下面的index.html,如果一个网站只是静态页面的话,那么就可以通过这种方式来实现部署。

动静分离

动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。

upstream dynamic_server{  
server 192.168.0.2:8080;
server 192.168.0.3:8081;
}
server {
listen 80;
server_name localhost;
location / {
root /root/website/;
index index.html;
}
# 所有静态请求都由nginx处理,存放目录为html
location ~ \.(gif|jpg|jpeg|png|bmp|swf|css|js)$ {
root /root/website/;
}
# 所有动态请求都转发给tomcat处理
location ~ \.(jsp|do)$ {
proxy_pass http://dynamic_server;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /root/website/;
}
}

这样我们就可以把 html、图片、css、js等放到 /root/website/ 目录下,而 Tomcat 只负责处理jsp和请求。例如当我们后缀为gif的时候,Nginx默认会从 /root/website/ 获取到当前请求的动态图文件返回,当然这里的静态文件跟Nginx是同一台服务器。我们也可以在另外一台服务器,然后通过反向代理和负载均衡配置过去就好了。只要搞清楚了最基本的流程,很多配置就很简单了,另外localtion后面其实是一个正则表达式,所以非常灵活。

References

  1. https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/
  2. http://tengine.taobao.org/book/chapter_09.html
  3. https://www.jianshu.com/p/dfcbdf6b0b05

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×