文章目录
一、关于 Nginx
-
Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,同时还是一个负载均衡服务器。其特点是占有内存少,并发能力强。
-
Nginx 以事件驱动的方式编写,所以有非常好的性能。
-
事件驱动:事件驱动就是在持续事务管理过程中,由当前时间点上出现的事件引发的调动可用资源执行相关任务,解决不断出现的问题,防止事务堆积的一种策略。
-
事件驱动模型一般是由
事件收集器
,事件发送器
,事件处理器
三部分组成。事件收集器
专门负责收集所有的事件,包括来自用户的(如鼠标单击事件、键盘输入事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器
负责将收集器收集到的事件分发到目标对象中。目标对象就是事件处理器所处的位置。事件处理器
主要负责具体事件的响应工作,它往往要到实现阶段才完全确定。
-
如果一个系统是以事件驱动程序模型作为编程基础的,那么,它的架构基本上是这样的:预先设计一个事件循环所形成的程序,这个事件循环程序构成了“事件收集器”,它不断地检查目前要处理的事件信息,然后使用“事件发送器”传递给“事件处理器”。“事件处理器”一般运用虚函数机制来实现。
-
事件驱动程序设计更多的关注了事件产生的随机性,使得应用程序能够具备相当的柔性,可以应付种种来自用户、硬件和系统的离散随机事件,这在很大程度上增强了用户和软件的交互性和用户操作的灵活性。
-
事件驱动模型是实现异步非阻塞的一个手段。
-
-
Nginx 因为它的稳定性、丰富的模块库、灵活的配置和低系统资源的消耗而闻名.不仅是因为响应静态页面的速度非常快,而且它的模块数量达到 Apache 的近 2/3,对
proxy
和rewrite
模块的支持很彻底。- proxy
- rewrite:rewrite 是 Nginx 服务器的重要功能之一,用于实现 URL 的重写,URL 的重写是非常有用的功能,比如它可以在我们改变网站结构之后,不需要客户端修改原来的书签,也无需其他网站修改我们的链接,就可以设置为访问,另外还可以在一定程度上提高网站的安全性。
Nginx 可以作为静态页面的 Web 服务器,同时还支持 CGI 协议的动态语言,比如 Perl、PHP 等。
二、Nginx 的特点
- 支持高并发:Nginx 是专门为性能优化而开发的,采用内核 Poll 模型,单机能够支持几万以上的并发连接
- 低资源消耗:Nginx 采取了分阶段资源分配技术,使得 CPU 与内存的占用率非常低。
- 高稳定性:其它 HTTP 服务器,当遇到访问的峰值,或者有人恶意发起慢速连接时,很可能会导致服务器物理内存耗尽频繁交换,失去响应,只能重启服务器。Nginx 采取了分阶段资源分配技术,使得它的 CPU 与内存占用率非常低。
- 高拓展性:设计极具扩展性,由多个不同功能、不同层次、不同类型且耦合度极低的模块组成
- 高可用性:Nginx 支持热部署,其中的 master 管理进程与 worker 工作进程的分离设计;启动速度特别迅速。
- 丰富的使用场景:可以作为 Web 服务端、HTTP 反向代理、负载均衡和前端缓存服务等场景使用。
- 开源协议:使用 BSD 许可协议,免费使用,且可修改源码
Nginx 采用 master-slave 模型(主从模型,一种优化阻塞的模型),能够充分利用 **SMP(对称多处理,一种并行处理技术)**的优势,且能够减少工作进程在磁盘 I/O 的阻塞延迟。当采用 select()/poll() 调用时,还可以限制每个进程的连接数。
Nginx 代码质量非常高,代码很规范,手法成熟,模块扩展也很容易。特别值得一提的是强大的 Upstream 与 Filter 链。
Nginx 采用了一些 os 提供的最新特性,从而大大提高了性能。
三、Nginx 架构
Nginx 在启动后,在 unix 系统中会以 **daemon(守护进程)**的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程。
master 进程主要用来管理 worker 进程,包含:
- 接收来自外界的信号,向各 worker 进程发送信号,
- 监控 worker 进程的运行状态,
- 当 worker 进程退出后(异常情况下),会自动重新启动新的 worker 进程。
而基本的网络事件,则是放在 worker 进程中来处理了。
Nginx 是以多进程的方式来工作的,当然 Nginx 也是支持多线程的方式的,只是主流的方式还是多进程的方式,也是 Nginx 的默认方式。
Nginx进程模型,以及优点:
- 首先,对于每个 worker 进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多。
- 其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快启动新的 worker 进程。
- worker 进程的异常退出,肯定是程序有 bug 了,异常退出,会导致当前 worker 上的所有请求失败,不过不会影响到所有请求,所以降低了风险。
思考:Nginx 采用多 worker 的方式来处理请求,每个 worker 里面只有一个主线程,那能够处理的并发数很有限啊,多少个 worker 就能处理多少个并发,何来高并发呢?Nginx 采用了异步非阻塞
的方式来处理请求,Nginx 是可以同时处理成千上万个请求的。
Nginx的异步非阻塞:
一个 worker 进程可以同时处理的请求数只受限于内存大小,而且在架构设计上,不同的 worker 进程之间处理并发请求时几乎没有同步锁的限制, worker 进程通常不会进入睡眠状态,因此,当 Nginx 上的进程数与 CPU 核心数相等时(最好每一个 worker 进程都绑定特定的 CPU 核心),进程间切换的代价是最小的。
在一个 Web 服务中,延迟最多的就是等待网络传输。基本的网络事件,由 worker 进程来处理。在一个请求需要等待的时候,worker 可以空闲出来处理其他的请求,少数几个 worker 进程就能够处理大量的并发。
-
同步与异步的理解
同步与异步的重点在消息通知的方式上,也就是调用结果通知的方式。
- 同步:当一个同步调用发出去后,调用者要一直等待调用结果的通知后,才能进行后续的执行。
- 异步:当一个异步调用发出去后,调用者不能立即得到调用结果的返回。
异步调用,要想获得结果,一般有两种方式:
- 主动轮询异步调用的结果;
- 被调用方通过callback来通知调用方调用结果
-
阻塞与非阻塞的理解
阻塞与非阻塞的重点在于进/线程等待消息时候的行为,也就是在等待消息的时候,当前进/线程是挂起状态,还是非挂起状态。
- 阻塞:阻塞调用在发出去后,在消息返回之前,当前进/线程会被挂起,直到有消息返回,当前进/线程才会被激活
- 非阻塞:非阻塞调用在发出去后,不会阻塞当前进/线程,而会立即返回。
思考:为什么 Nginx 采用多进程结构而不是多线程结构呢?
- 多进程模型的设计充分利用了多核处理器的并发能力。
Nginx 要保证它的高可用、高可靠性, 如果 Nginx 使用多线程的时候,由于线程之间是共享同一个地址空间的,当某一个第三方模块引发了一个地址空间导致的断错时 (eg: 地址越界), 会导致整个 Nginx 全部挂掉; 当采用多进程来实现时, 往往不会出现这个问题.
1、多进程单线程
Nginx 自己实现了对 epoll 的封装,是多进程单线程的典型代表。使用多进程模式,不仅能提高并发率,而且进程之间是相互独立的,一个 worker 进程挂了不会影响到其他 worker 进程。
2、IO多路复用模型
额外的前提知识:(了解一下)
- 在 Go 里最核心的是 Goroutine ,也就是所谓的协程,协程最妙的一个实现就是异步的代码长的跟同步代码一样。比如在 Go 中,网络 IO 的 read,write 看似都是同步代码,其实底下都是异步调用。
- Go 配合协程在网络 IO 上实现了异步流程的代码同步化。核心就是用 epoll 池来管理网络 fd 。
- 实现形式上,后台的程序只需要 1 个就可以负责管理多个 fd 句柄,负责应对所有的业务方的 IO 请求。这种一对多的 IO 模式我们就叫做 IO 多路复用。
- 多路是指?多个业务方(句柄)并发下来的 IO 。
- 复用是指?复用这一个后台处理程序。
好了,进入正题…
所谓的 I/O 复用,就是多个 I/O 可以复用一个进程。I/O 多路复用允许进程同时检查多个 fd(file descriptor, 进程独有的文件描述符表的索引),以找出其中可执行 I/O 操作的 fd。
系统调用 select() 和 poll() 来执行 I/O 多路复用。在 Linux2.6 中引入的 epoll() 是升级版,提供了更高的性能。通过 I/O 复用,可以在一个进程处理大量的并发 I/O。
- 初级版 I/O 复用
比如一个进程接受了10000个连接,这个进程每次从头到尾的问一遍这10000个连接:“有I/O事件没?有的话就交给我处理,没有的话我一会再来问一遍。”然后进程就一直从头到尾问这10000个连接,如果这10000个连接都没有I/O事件,就会造成CPU的空转,并且效率也很低。
那么,如果发明一个代理,每次能够知道哪个连接有了I/O流事件,就可以避免无意义的空转了,为了避免CPU空转,可以引进一个代理(一开始有一位叫做 select 的代理,后来又有一位叫做 poll 的代理,不过两者的本质是一样的)。
- 升级版 I/O 复用
select() 采用轮询的方式来检查 fd 是否就绪,当 fd 数量较多时,性能欠佳。从 select 那里仅仅知道有 I/O 事件发生了,但并不知道是那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,进行操作。 - 高级版I/O复用
epoll 能更高效的检查大量 fd。select 和 poll 或多或少都要多余的拷贝,盲猜(遍历才知道)fd ,所以效率自然就低了。 epoll 做的无用功最少。
epoll(event poll),不同于忙轮询和无差别轮询,当连接有I/O流事件产生的时候,epoll 就会去告诉进程哪个连接有I/O流事件产生,然后进程就去处理这个事件。此时我们对这些流的操作都是有意义的,而不是无用功。
epoll 实现高效的原因有二:
- 简单高效的数据结构:红黑树。Linux 内核对于 epoll 池的内部实现就是用红黑树的结构体来管理这些注册进程来的句柄 fd。红黑树是一种平衡二叉树,时间复杂度为 O(log n),就算这个池子就算不断的增删改,也能保持非常稳定的查找性能。
- 怎么保证数据准备好之后,立马感知呢?回调设置。在 epoll_ctl 的内部实现中,除了把句柄结构用红黑树管理,另一个核心步骤就是设置 poll 回调。这个是定制监听事件的机制实现。通过 poll 机制让上层能直接告诉底层,我这个 fd 一旦读写就绪了,请底层硬件(比如网卡)回调的时候自动把这个 fd 相关的结构体放到指定队列中,并且唤醒操作系统。
epoll 通过在内核中申请一个简易的文件系统(文件系统一般用B+树数据结构实现),也有人说 epoll 其实就是一个文件系统。其工作流程分为三部分:
- 调用 int epoll_create(int size) 建立一个 epoll 对象,内核会创建一个eventpoll 结构体,用于存放通过 epoll_ctl() 向 epoll 对象中添加进来的事件,这些事件都会挂载在红黑树中。
- 调用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 对象中为 fd 注册事件,所有添加到 epoll 中的事件都会与设备驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个 socktfd 的回调方法,将 socktfd 添加到 event poll 中的双链表(就绪队列)。
- 调用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 来等待事件的发生,timeout 为 -1 时,该调用会阻塞直到有事件发生。注册好事件之后,只要有 fd 上事件发生,epoll_wait() 就能检测到并返回给用户,用户执行阻塞函数时就不会发生阻塞了。
epoll() 在内核维护一个双链表(就绪队列),epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。
可以看出,因为一个进程里只有一个线程,所以一个进程同时只能做一件事,但是可以通过不断地切换来“同时”处理多个请求。
这样,基于 多进程+epoll, Nginx 便能实现高并发。
小结下:epoll 之所以做到了高效,最关键的两点:
- 内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
- epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
- epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;
3、worker 进程工作流程
当一个 worker 进程在 accept() 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,一个完整的请求。一个请求,完全由 worker 进程来处理,而且只会在一个 worker 进程中处理。优点:
- 节省锁带来的开销。每个 worker 进程都彼此独立地工作,不共享任何资源,因此不需要锁。同时在编程以及问题排查上时,也会方便很多。
- 独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程。当然,worker 进程自己也能发生意外退出。
4、惊群效应
什么是惊群效应?
惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。
惊群效应造成了什么问题?
-
Linux 内核对用户进程(线程)频繁地做无效的调度、上下文切换等使系统性能大打折扣。
-
为了确保只有一个进程(线程)得到资源,需要对资源操作进行加锁保护,加大了系统的开销。
Nginx的解决思路其实就是加锁与负载均衡
Nginx 提供了一个 accept_mutex 这个东西,这是一个加在 accept() 上的一把全局互斥锁。即每个 worker 进程在执行 accept() 之前都需要先获取锁(如果某个进程当前连接数达到了最大连接数的7/8,也就是负载均衡点,此时这个进程就不会再去争抢锁资源,而是将负载均衡到其它进程上),accept() 成功之后再解锁。有了这把锁,同一时刻,只会有一个进程执行 accpet() ,这样就不会有惊群问题了。accept_mutex 是一个可控选项,可以显示地关掉,默认是打开的。
四、Nginx 基本数据结构
就是 Nginx 源码里面该怎么去看里面有哪些东西
这里不详述。。。
五、Nginx 的配置系统
除主配置文件 nginx.conf 以外的文件都是在某些情况下才使用的,而只有主配置文件是在任何情况下都被使用的。
在 nginx.conf 中,包含若干配置项。每个配置项由配置指令和指令参数 2 个部分构成。指令参数也就是配置指令对应的配置值。
Nginx 的配置信息分成了几个作用域(scope,有时也称作上下文),这就是 main,server 以及 location。
1、全局配置块
配置影响 Nginx 全局的指令。主要包括:
- 配置运行 Nginx 服务器用户(组)
- worker process 数
- Nginx 进程
- PID 存放路径错误日志的存放路径
- 一个 Nginx 进程打开的最多文件描述符数目
#配置worker进程运行用户(和用户组),nobody也是一个Linux用户,一般用于启动程序,没有密码
user nobody;
#user www www;
#配置工作进程数目,根据硬件调整,通常等于CPU数量或者2倍于CPU数量
worker_processes 1;
#配置全局错误日志及类型,[debug | info | notice | warn | error | crit],默认是error
error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#配置进程pid文件
pid logs/nginx.pid;
#一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(系统的值ulimit -n)与Nginx进程数相除,但是Nginx分配请求并不均匀,所以建议与ulimit -n的值保持一致。
worker_rlimit_nofile 65535;
2、events 块
配置影响Nginx服务器或与用户的网络连接。主要包括:
- 事件驱动模型的选择
- 最大连接数的配置
参考事件模型,use [ kqueue | rtsig | epoll | /dev/poll | select | poll ];
#epoll模型是Linux 2.6以上版本内核中的高性能网络I/O模型,如果跑在FreeBSD上面,就用kqueue模型。
use epoll;
#单个进程最大连接数(最大连接数=连接数*进程数)
worker_connections 65535;
3、http 块
可以嵌套多个 server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。主要包括:
- 定义 MIMI-Type
- 自定义服务日志
- 允许 sendfile 方式传输文件
- 连接超时时间
- 单连接请求数上限
4、server 块
配置虚拟主机的相关参数,一个 http 中可以有多个 server。主要包括:
- 配置网络监听
- 配置 https 服务
- 基于名称的虚拟主机配置
- 基于 IP 的虚拟主机配置
#虚拟主机的常见配置
server {
listen 80; #配置监听端口
server_name localhost; #配置服务名
charset utf-8; #配置字符集
access_log logs/host.access.log main; #配置本虚拟主机的访问日志
location / {
root html; #root是配置服务器的默认网站根目录位置,默认为Nginx安装主目录下的html目录
index index.html index.htm; #配置首页文件的名称
}
error_page 404 /404.html; #配置404错误页面
error_page 500 502 503 504 /50x.html; #配置50x错误页面
}
#配置https服务,安全的网络传输协议,加密传输,端口443
server {
listen 443 ssl;
server_name localhost;
ssl_certificate cert.pem;
ssl_certificate_key cert.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html index.htm;
}
}
5、location 块
配置请求的路由,以及各种页面的处理情况。主要包括:
- 请求根目录配置更改
- 网站默认首页配置
- location 的 URI
root html; #root是配置服务器的默认网站根目录位置,默认为Nginx安装主目录下的html目录
index index.html index.htm; #配置首页文件的名称
proxy_pass http://127.0.0.1:88; #反向代理的地址
proxy_redirect off; #是否开启重定向
#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
#以下是一些反向代理的配置,可选。
client_max_body_size 10m; #允许客户端请求的最大单文件字节数
client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数,
proxy_connect_timeout 90; #nginx跟后端服务器连接超时时间(代理连接超时)
proxy_send_timeout 90; #后端服务器数据回传时间(代理发送超时)
proxy_read_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时)
proxy_buffer_size 4k; #设置代理服务器(Nginx)保存用户头信息的缓冲区大小
proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的设置
proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2)
proxy_temp_file_write_size 64k; #设定缓存文件夹大小
六、Nginx 的模块化体系结构
Nginx 的内部结构是由核心部分和一系列的功能模块所组成。这样划分是为了使得每个模块的功能相对简单,便于开发,同时也便于对系统进行功能扩展。
Nginx 将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块,进行处理。每个模块实现特定的功能。
有两个模块比较特殊,他们居于 Nginx core 和各功能模块的中间。
- http 模块
- mail 模块
这 2 个模块在 Nginx core 之上实现了另外一层抽象,处理与 HTTP 协议和 Email 相关协议(SMTP/POP3/IMAP)有关的事件,并且确保这些事件能被以正确的顺序调用其他的一些功能模块。
Nginx 的模块根据其功能基本上可以分为以下几种类型:
1、Nginx event 模块
搭建了独立于操作系统的事件处理机制的框架,及提供了各具体事件的处理。包括ngx_events_module,ngx_event_core_module和ngx_epoll_module等。
nginx具体使用何种事件处理模块,这依赖于具体的操作系统和编译选项。
2、Nginx handler 模块
handler 模块就是接受来自客户端的请求并产生输出的模块。
handler 模块处理的结果通常有三种情况: 处理成功,处理失败(处理的时候发生了错误)或者是拒绝去处理。
handler 模块必须提供一个真正的处理函数,这个函数负责对来自客户端请求的真正处理。这个函数的处理,既可以选择自己直接生成内容,也可以选择拒绝处理,由后续的 handler 去进行处理,或者是选择丢给后续的 filter 进行处理。
3、Nginx filter 模块
Filter 模块是过滤响应头和内容的模块,可以对回复的头和内容进行处理。
它的处理时间在获取回复内容之后,向用户发送响应之前。
它的处理过程分为两个阶段,过滤 HTTP 回复的头部和主体,在这两个阶段可以分别对头部和主体进行修改。
4、Nginx upstream 模块
handler、filter,利用这两类模块,可以使 Nginx 轻松完成任何单机工作。
upstream 模块,将使 Nginx 跨越单机的限制,完成网络数据的接收、处理和转发。
数据转发功能,为 Nginx 提供了跨越单机的横向处理能力,使 Nginx 摆脱只能为终端节点提供单一功能的限制,而使它具备了网络应用级别的拆分、封装和整合的战略功能。在云模型大行其道的今天,数据转发是 Nginx 有能力构建一个网络应用的关键组件。
upstream 模块是从 handler 模块发展而来,指令系统和模块生效方式与 handler 模块无异。不同之处在于,upstream 模块在 handler 函数中设置众多回调函数。实际工作都是由这些回调函数完成的。每个回调函数都是在 upstream 的某个固定阶段执行,各司其职,大部分回调函数一般不会真正用到。
upstream 最重要的回调函数是 create_request、process_header 和 input_filter,他们共同实现了与后端服务器的协议的解析部分。
5、Nginx 负载均衡模块
负载均衡模块用于从 upstream 指令定义的后端主机列表中选取一台主机。
Nginx 先使用负载均衡模块找到一台主机,再使用 upstream 模块实现与这台主机的交互。
七、Nginx 的请求处理
Nginx 使用一个多进程模型来对外提供服务,其中一个 master 进程,多个 worker 进程。
实际上的业务处理逻辑都在 worker 进程。worker 进程中有一个函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个 Nginx 服务被停止。
worker 进程中,ngx_worker_process_cycle()
函数就是这个无限循环的处理函数。在这个函数中,一个请求的简单处理流程如下:
- 操作系统提供的机制(例如 epoll, kqueue 等)产生相关的事件。
- 接收和处理这些事件,如是接受到数据,则产生更高层的 request 对象。
- 处理 request 的 header 和 body。
- 产生响应,并发送回客户端。
- 完成 request 的处理。
- 重新初始化定时器及其他事件。
从 Nginx 的内部来看,一个 HTTP Request 的处理过程涉及到以下几个阶段。
- 初始化 HTTP Request(读取来自客户端的数据,生成 HTTP Request 对象,该对象含有该请求所有的信息)。
- 处理请求头。
- 处理请求体。
- 如果有的话,调用与此请求(URL 或者 Location)关联的 handler。
- 依次调用各 phase handler 进行处理。phase 字面意思就是阶段,phase handlers 就是包含若干个处理阶段的一些 handler。在处理到某个阶段的时候,依次调用该阶段的 handler 对 HTTP Request 进行处理,并产生一些输出。通常 phase handler 是与定义在配置文件中的某个 location 相关联的。一个 phase handler 通常执行以下几项任务:
- 获取 location 配置。
- 产生适当的响应。
- 发送 response header。
- 发送 response body。
当 Nginx 读取到一个 HTTP Request 的 header 的时候,Nginx 首先查找与这个请求关联的虚拟主机的配置。如果找到了这个虚拟主机的配置,那么通常情况下,这个 HTTP Request 将会经过以下几个阶段的处理(phase handlers):
- NGX_HTTP_POST_READ_PHASE: 读取请求内容阶段
- NGX_HTTP_SERVER_REWRITE_PHASE: Server 请求地址重写阶段
- NGX_HTTP_FIND_CONFIG_PHASE: 配置查找阶段:
- NGX_HTTP_REWRITE_PHASE: Location请求地址重写阶段
- NGX_HTTP_POST_REWRITE_PHASE: 请求地址重写提交阶段
- NGX_HTTP_PREACCESS_PHASE: 访问权限检查准备阶段
- NGX_HTTP_ACCESS_PHASE: 访问权限检查阶段
- NGX_HTTP_POST_ACCESS_PHASE: 访问权限检查提交阶段
- NGX_HTTP_TRY_FILES_PHASE: 配置项 try_files 处理阶段
- NGX_HTTP_CONTENT_PHASE: 内容产生阶段
- NGX_HTTP_LOG_PHASE: 日志模块处理阶段
八、Nginx 是反向代理服务器?
不管是正向代理还是反向代理,代理是在服务器和客户端之间假设的一层服务器,代理将接收客户端的请求并将它转发给服务器,然后将服务端的响应转发给客户端。
1、正向代理
一个位于客户端和原始服务器 (origin server) 之间的服务器,为客户端服务,对客户端是透明的对原始服务器是非透明的,即服务端并不知道自己收到的是来自代理的访问还是来自真实客户端的访问。
示例:客户端<—>VPN<—>谷歌服务器
-
由于防火墙的原因,我们并不能直接访问谷歌,那么我们可以借助VPN来实现,这就是一个简单的正向代理的例子。
-
正向代理“代理”的是客户端,而且客户端是知道目标的,而目标是不知道客户端是通过VPN访问的。
2、反向代理
反向代理是为服务端服务的,反向代理可以帮助服务器接收来自客户端的请求,帮助服务器做请求转发,负载均衡等。反向代理对服务端是透明的,对客户端是非透明的,即客户端并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。
示例:客户端<—>百度服务器<—>内网服务器
- 当我们在外网访问百度的时候,其实会进行一个转发,代理到内网去,这就是所谓的反向代理。
- 反向代理“代理”的是服务器端,而且这一个过程对于客户端而言是透明的。
3、反向代理的作用
- 保障应用服务器的安全(增加一层代理,可以屏蔽危险攻击,更方便的控制权限)
- 实现负载均衡(稍等~下面会讲)
- 实现跨域(号称是最简单的跨域方式)
现实中反向代理多数是用在负载均衡中,反向代理是实现负载均衡的基础。反向代理应用十分广泛,CDN 服务就是反向代理经典的应用场景之一。
九、Nginx 的 Master-Worker 模式
-
Master进程的作用是?
- 读取并验证配置文件 nginx.conf;接收来自客户端的信号、通知、管理 worker 进程;
-
Worker进程的作用是?
- 每一个 Worker 进程都维护一个线程(避免线程切换),处理连接和请求;注意 Worker 进程的个数由配置文件决定,一般和 CPU 个数相关(有利于进程切换),配置几个就有几个 Worker 进程。
如何计算 worker 连接数?
-
如果只访问 Nginx 的静态资源,一个发送请求会占用了 worker 的2个连接数
-
如果是作为反向代理服务器,一个发送请求会占用了 worker 的4个连接数
如何计算最大并发数?
- 如果只访问 Nginx 的静态资源,最大并发数量应该是:
worker_connections * worker_processes / 2
- 如果是作为反向代理服务器,最大并发数量应该是:
worker_connections * worker_processes / 4
1、如何做到热部署?
所谓热部署,就是配置文件 nginx.conf
修改后,不需要 stop Nginx
,不需要中断请求,就能让配置文件生效!(nginx -s reload
重新加载 /nginx -t
检查配置 /nginx -s stop
)
我们知道 worker 进程负责处理具体的请求,如果想达到热部署的效果,可以想象:
方案一:
- 修改配置文件 nginx.conf 后,主进程 master 负责推送给 worker 进程更新配置信息,worker 进程收到信息后,更新进程内部的线程信息。(有点 volatile 的味道)
方案二:
- 修改配置文件 nginx.conf 后,重新生成新的 worker 进程,当然会以新的配置进行处理请求,而且新的请求必须都交给新的 worker 进程,至于老的 worker 进程,等把那些以前的请求处理完毕后,kill 掉即可。
Nginx 采用的就是方案二来达到热部署的!
2、如何实现高并发?
异步,非阻塞,使用了epoll 和大量的底层代码优化。
如果一个 server 采用一个进程负责一个 request 的方式,那么进程数就是并发数。正常情况下,会有很多进程一直在等待中。
而 Nginx 采用一个 master 进程,多个 worker 进程的模式。
- master 进程主要负责收集、分发请求。每当一个请求过来时,master 就拉起一个 worker 进程负责处理这个请求。
- 同时 master 进程也负责监控 worker 的状态,保证高可靠性
- worker 进程一般设置为跟 cpu 核心数一致。Nginx 的 worker 进程在同一时间可以处理的请求数只受内存限制,可以处理多个请求。
- Nginx 的异步非阻塞工作方式把当中的等待时间利用起来了。在需要等待的时候,这些进程就空闲出来待命了,因此表现为少数几个进程就解决了大量的并发问题。
每进来一个 request,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request,并等待请求返回。那么,这个处理的 worker 很聪明,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了。
此时,如果再有 request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。
3、高并发下的高效处理?
问题:
- Apache 是不支持高并发的服务器。在 Apache 上运行数以万计的并发访问,会导致服务器消耗大量内存。
- 操作系统对其进行进程或线程间的切换也消耗了大量的 CPU 资源,导致 HTTP 请求的平均响应速度降低。
- 以上都决定了 Apache 不可能成为高性能 Web 服务器,轻量级高并发服务器 Nginx 就应运而生了。
Nginx 的 worker 进程个数与 CPU 绑定、worker 进程内部包含一个线程高效回环处理请求,这的确有助于效率,但这是不够的。要同时处理那么多的请求,要知道,有的请求需要发生 IO,可能需要很长时间,如果等着它,就会拖慢 worker 的处理速度。
Nginx 采用了 Linux 的 epoll 模型,epoll 模型基于事件驱动机制,它可以监控多个事件是否准备完毕,如果 OK,那么放入 epoll 队列中,这个过程是异步的。worke r只需要从 epoll 队列循环处理即可。
Nginx的优点:
- Nginx 使用基于事件驱动架构,使得其可以支持数以百万级别的 TCP 连接。
- 高度的模块化和自由软件许可证使得第三方模块层出不穷(这是个开源的时代啊)。
- Nginx 是一个跨平台服务器,可以运行在 Linux、Windows、FreeBSD、Solaris、AIX、Mac OS 等操作系统上。
- 这些优秀的设计带来的极大的稳定性。
4、Nginx挂了怎么办?
Nginx 既然作为入口网关,很重要,如果出现单点问题,显然是不可接受的。
有没有一种解决方案,可以保证Nginx挂掉了,服务也可以照常使用,答案肯定是有的,这就是我们接下来要学习的高可用集群,采用的是一主一备的模式,当主节点Nginx挂掉,备份服务器Nginx立刻跟上,这样就保证了服务的高可用性。
解决方案:Keepalived+Nginx 实现高可用
Keepalived 是一个高可用解决方案,主要是用来防止服务器单点发生故障,可以通过和 Nginx 配合来实现 Web 服务的高可用。
思路:
-
第一:请求不要直接打到 Nginx 上,应该先通过 Keepalived (这就是所谓虚拟IP,VIP)
-
第二:Keepalived 应该能监控 Nginx 的生命状态(提供一个用户自定义的脚本,定期检查 Nginx 进程状态,进行权重变化,从而实现 Nginx 故障切换)
5、反向代理【proxy_pass】
所谓反向代理,对应到 Nginx 中其实就是在 location 这一段配置中的 root 替换成 proxy_pass 即可。root 说明是静态资源,可以由 Nginx 进行返回;而 proxy_pass 说明是动态请求,需要进行转发,比如代理到 Tomcat 上。
反向代理,上面已经说了,过程是透明的,比如说 request -> Nginx -> Tomcat,那么对于 Tomcat 而言,请求的 IP 地址就是 Nginx 的地址,而非真实的 request 地址,这一点需要注意。不过好在 Nginx 不仅仅可以反向代理请求,还可以由用户自定义设置 HTTP HEADER。
6、负载均衡【upstream】
随着业务的不断增长和用户的不断增多,一台服务已经满足不了系统要求了。这个时候就出现了服务器集群。
如果请求数过大,单个服务器解决不了,我们增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器的情况改为请求分发到多个服务器上,就是负载均衡。
上面的反向代理中,我们通过proxy_pass
来指定Tomcat
的地址,很显然我们只能指定一台Tomcat
地址,那么我们如果想指定多台来达到负载均衡呢?
-
第一,通过upstream来定义一组
Tomcat
,并指定负载策略(IPHASH、加权论调、最少连接),健康检查策略(Nginx
可以监控这一组Tomcat的状态)等。 -
第二,将proxy_pass替换成upstream指定的值即可。
Upstream 指定后端服务器地址列表,在 server 中拦截响应请求,并将请求转发到 Upstream 中配置的服务器列表。
upstream balanceServer {
server 10.1.22.33:12345;
server 10.1.22.34:12345;
server 10.1.22.35:12345;
}
server {
server_name fe.server.com;
listen 80;
location /api {
proxy_pass http://balanceServer;
}
}
上面的配置只是指定了 Nginx 需要转发的服务端列表,但并没有指定分配策略。默认情况下采用的是轮询策略,将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的,但是如果其中某一台服务器压力太大,出现延迟,会影响所有分配在这台服务器下的用户。
负载均衡分配策略
-
轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除。
upstream server_pool{ server 192.168.5.21:80 server 192.168.5.22:80 }
-
weight:weight 代表权重,默认为1,权重越高被分配的客户端越多,weight 和访问比率成正比,用于后端服务器性能不均的情况。
upstream server_pool{ server 192.168.5.21 weight=10; server 192.168.5.22 weight=10; }
-
ip_hash:每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 的问题。
upstream server_pool{ ip_hash; server 192.168.5.21:80; server 192.168.5.22:80; }
-
fair(第三方):按后端服务器的响应时间来分配请求,响应时间短的优先分配。
upstream server_pool{ server 192.168.5.21:80; server 192.168.5.22:80; fair; }
负载均衡可能带来的问题?
负载均衡所带来的明显的问题是,一个请求,可以到A server,也可以到B server,这完全不受我们的控制,只是我们得注意的是:用户状态的保存问题,如Session会话信息,不能在保存到服务器上。
受集群单台服务器内存等资源的限制,负载均衡集群的服务器也不能无限增多。但因其良好的容错机制,负载均衡成为了实现高可用架构中必不可少的一环。
负载均衡的作用
- 分摊服务器集群压力
- 保证客户端访问的稳定性
7、缓存
缓存,是 Nginx 提供的可以加快访问速度的机制,在配置上就是一个开启,同时指定目录,让缓存可以存储到磁盘上。
8、动静分离
为了加快服务器的解析速度,可以把动态页面和静态页面交给不同的服务器来解析,加快解析速度,降低原来单个服务器的压力。
我们需要分文件类型来进行过滤,比如 jsp 直接给 tomcat 处理,因为 Nginx 并不是 servlet 容器,没办法处理 JSP,而 html,js,css 这些不需要处理的,直接给 Nginx 进行缓存即可。(动静分离充分利用各自的优势完成高性能访问)
动静分离其实就是 Nginx 服务器将接收到的请求分为动态请求和静态请求。
- 静态请求直接从 Nginx 服务器所设定的根目录路径去取对应的资源
- 动态请求转发给真实的后台(前面所说的应用服务器,如 Tomcat)去处理。
这样做不仅能给应用服务器减轻压力,将后台 api 接口服务化,还能将前后端代码分开并行开发和部署。
动静分离从目前实现角度来讲大致分为两种,
- 一种是纯粹把静态文件独立成单独的域名,放在独立的服务器上,也是目前主流推崇的方案;
- 另外一种方法就是动态跟静态文件混合在一起发布,通过 Nginx 来分开。
十、为什么选择 Nginx
1、IO多路复用epoll(IO复用)
2、轻量级
- 功能模块少 - Nginx 仅保留了 HTTP 需要的模块,其他都用插件的方式,后天添加
- 代码模块化 - 更适合二次开发,如阿里巴巴 Tengine
3、CPU亲和
把 CPU 核心和 Nginx 工作进程绑定,把每个 worker 进程固定在一个 CPU 上执行,减少切换 CPU 的 cache miss,从而提高性能。