一、简介
Nginx 是一款轻量级的 Web (HTTP)服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器
关键字: 事件驱动 反向代理 负载平衡 响应静态页面的速度非常快
优势:能支持高达 50,000 个并发连接数 ;支持热部署 ;很高的稳定性(抵御dos攻击)
二、架构: 在 unix 系统中会以 daemon (守护进程)的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程(多进程的工作方式)
1、多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。
2、一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它进程的请求。
3、推荐设置 worker 的个数为 cpu 的核数
4、异步非阻塞 (非阻塞不会让出cpu导致切换浪费)
三、基础概念
1、connection
是对 tcp 连接的封装;
Nginx 通过设置 worker_connectons 来设置每个worker进程支持的最大连接数;
Nginx 能建立的最大连接数,应该是worker_connections * worker_processes;对于 HTTP 请求本地资源来说,能够支持的最大并发数量是
worker_connections * worker_processes
,而如果是 HTTP 作为反向代理来说,最大并发数量应该是worker_connections * worker_processes/2
。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接;
2、request
Nginx 中指 http 请求;
web服务器工作流:http 请求是典型的请求-响应类型的的网络协议,而 http 是文本协议,所以我们在分析请求行与请求头,以及输出响应行与响应头,往往是一行一行的进行处理。如果我们自己来写一个 http 服务器,通常在一个连接建立好后,客户端会发送请求过来。然后我们读取一行数据,分析出请求行中包含的 method、uri、http_version 信息。然后再一行一行处理请求头,并根据请求 method 与请求头的信息来决定是否有请求体以及请求体的长度,然后再去读取请求体。得到请求后,我们处理请求产生需要输出的数据,然后再生成响应行,响应头以及响应体。在将响应发送给客户端之后,一个完整的请求就处理完了。
3、keepalive
长连接: http 请求是基于 TCP 协议之上的,那么,当客户端在发起请求前,需要先与服务端建立 TCP 连接(三次握手),当连接断开后(四次挥手)。而 http 请求是请求应答式的,如果我们能知道每个请求头与响应体的长度,那么我们是可以在一个连接上面执行多个请求的,这就是所谓的长连接,但前提条件是我们先得确定请求头与响应体的长度。对于请求来说,如果当前请求需要有body,如 POST 请求,那么 Nginx 就需要客户端在请求头中指定 content-length 来表明 body 的大小,否则返回 400 错误。也就是说,请求体的长度是确定的,那么响应体的长度呢?先来看看 http 协议中关于响应 body 长度的确定:
- 对于 http1.0 协议来说,如果响应头中有 content-length 头,则以 content-length 的长度就可以知道 body 的长度了,客户端在接收 body 时,就可以依照这个长度来接收数据,接收完后,就表示这个请求完成了。而如果没有 content-length 头,则客户端会一直接收数据,直到服务端主动断开连接,才表示 body 接收完了。
- 而对于 http1.1 协议来说,如果响应头中的 Transfer-encoding 为 chunked 传输,则表示 body 是流式输出,body 会被分成多个块,每块的开始会标识出当前块的长度,此时,body 不需要通过长度来指定。如果是非 chunked 传输,而且有 content-length,则按照 content-length 来接收数据。否则,如果是非 chunked,并且没有 content-length,则客户端接收数据,直到服务端主动断开连接。
从上面,我们可以看到,除了 http1.0 不带 content-length 以及 http1.1 非 chunked 不带 content-length 外,body 的长度是可知的。此时,当服务端在输出完 body 之后,会可以考虑使用长连接。能否使用长连接,也是有条件限制的。如果客户端的请求头中的 connection为close,则表示客户端需要关掉长连接,如果为 keep-alive,则客户端需要打开长连接,如果客户端的请求中没有 connection 这个头,那么根据协议,如果是 http1.0,则默认为 close,如果是 http1.1,则默认为 keep-alive。如果结果为 keepalive,那么,Nginx 在输出完响应体后,会设置当前连接的 keepalive 属性,然后等待客户端下一次请求。当然,Nginx 不可能一直等待下去,如果客户端一直不发数据过来,岂不是一直占用这个连接?所以当 Nginx 设置了 keepalive 等待下一次的请求时,同时也会设置一个最大等待时间,这个时间是通过选项 keepalive_timeout 来配置的,如果配置为 0,则表示关掉 keepalive,此时,http 版本无论是 1.1 还是 1.0,客户端的 connection 不管是 close 还是 keepalive,都会强制为 close。
如果服务端最后的决定是 keepalive 打开,那么在响应的 http 头里面,也会包含有 connection 头域,其值是"Keep-Alive",否则就是"Close"。如果 connection 值为 close,那么在 Nginx 响应完数据后,会主动关掉连接。所以,对于请求量比较大的 Nginx 来说,关掉 keepalive 最后会产生比较多的 time-wait 状态的 socket。一般来说,当客户端的一次访问,需要多次访问同一个 server 时,打开 keepalive 的优势非常大,比如图片服务器,通常一个网页会包含很多个图片。打开 keepalive 也会大量减少 time-wait 的数量。
4、pipe
http1.1 引入新特性,keepalive 的一种升华,基于长连接的,目的就是利用一个连接做多次请求;
对 pipeline 来说,客户端不必等到第一个请求处理完后,就可以马上发起第二个请求;
5、linger_close
延迟关闭,也就是说,当 Nginx 要关闭连接时,并非立即关闭连接,而是先关闭 tcp 连接的写,再等待一段时间后再关掉连接的读。
四、配置
1、nginx.conf
指令上下文:
- main: Nginx 在运行时与具体业务功能(比如http服务或者email服务代理)无关的一些参数,比如工作进程数,运行的身份等。
- http: 与提供 http 服务相关的一些配置参数。例如:是否使用 keepalive 啊,是否使用gzip进行压缩等。
- server: http 服务上支持若干虚拟主机。每个虚拟主机一个对应的 server 配置项,配置项里面包含该虚拟主机相关的配置。在提供 mail 服务的代理时,也可以建立若干 server,每个 server 通过监听的地址来区分。
- location: http 服务中,某些特定的URL对应的一系列配置项。
- mail: 实现 email 相关的 SMTP/IMAP/POP3 代理时,共享的一些配置项(因为可能实现多个代理,工作在多个监听地址上)。
示例:
worker_processes 1; //一般设置为cpu核数
error_log logs/error.log error;
pid logs/nginx.pid;
events {
worker_connections 1024; //每个worker的最大连接数
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
server_names_hash_bucket_size 128;
keepalive_timeout 1800s; //支持长连接
client_max_body_size 0;
proxy_connect_timeout 5s;
proxy_read_timeout 1800s;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
upstream web_vmaxfmproxy_pool { server 10.43.136.220:27430; }
upstream web_vmaxdatacheck_pool { server 10.43.136.220:27340; }
server {
listen 28888;
server_name web_web_pool;
location ~ ^/web/cometd {
proxy_pass http://web_web_pool;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
if ($uri ~ ^/vmaxfmproxy/){ proxy_pass http://web_vmaxfmproxy_pool; break; }
if ($uri ~ ^/vmaxdatacheck/){ proxy_pass http://web_vmaxdatacheck_pool; break; }
}
location /rdk/service {
proxy_pass http://localhost:5555;
}
location ~ /rdk/app/(?<section>.*) {
proxy_pass http://web_rdk_server_pool/rdk_server/app/$section;
}
location /web/res/web-framework/default.html {
rewrite /web/res/web-framework/default.html /rdk/app/portal/web/index.html permanent;
}
}
}
五、nginx模块
- event module: 搭建了独立于操作系统的事件处理机制的框架,及提供了各具体事件的处理。包括 ngx_events_module, ngx_event_core_module和ngx_epoll_module 等。Nginx 具体使用何种事件处理模块,这依赖于具体的操作系统和编译选项。
- phase handler: 此类型的模块也被直接称为 handler 模块。主要负责处理客户端请求并产生待响应内容,比如 ngx_http_static_module 模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。
- output filter: 也称为 filter 模块,主要是负责对输出的内容进行处理,可以对输出进行修改。例如,可以实现对输出的所有 html 页面增加预定义的 footbar 一类的工作,或者对输出的图片的 URL 进行替换之类的工作。
- upstream: upstream 模块实现反向代理的功能,将真正的请求转发到后端服务器上,并从后端服务器上读取响应,发回客户端。upstream 模块是一种特殊的 handler,只不过响应内容不是真正由自己产生的,而是从后端服务器上读取的。
- load-balancer: 负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来作为某个请求的转发服务器。
六、nginx 请求过程
所有实际上的业务处理逻辑都在 worker 进程。worker 进程中有一个函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个 Nginx 服务被停止。
worker 进程中,ngx_worker_process_cycle()函数就是这个无限循环的处理函数。在这个函数中,一个请求的简单处理流程如下:
- 操作系统提供的机制(例如 epoll, kqueue 等)产生相关的事件。
- 接收和处理这些事件,如是接受到数据,则产生更高层的 request 对象。
- 处理 request 的 header 和 body。
- 产生响应,并发送回客户端。
- 完成 request 的处理。
- 重新初始化定时器及其他事件。
为了让大家更好的了解 Nginx 中请求处理过程,我们以 HTTP Request 为例,来做一下详细地说明。
从 Nginx 的内部来看,一个 HTTP Request 的处理过程涉及到以下几个阶段。
- 初始化 HTTP Request(读取来自客户端的数据,生成 HTTP Request 对象,该对象含有该请求所有的信息)。
- 处理请求头。
- 处理请求体。
- 如果有的话,调用与此请求(URL 或者 Location)关联的 handler。
- 依次调用各 phase handler 进行处理。
在这里,我们需要了解一下 phase handler 这个概念。phase 字面的意思,就是阶段。所以 phase handlers 也就好理解了,就是包含若干个处理阶段的一些 handler。
在每一个阶段,包含有若干个 handler,再处理到某个阶段的时候,依次调用该阶段的 handler 对 HTTP Request 进行处理。
通常情况下,一个 phase handler 对这个 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: 日志模块处理阶段
在内容产生阶段,为了给一个 request 产生正确的响应,Nginx 必须把这个 request 交给一个合适的 content handler 去处理。如果这个 request 对应的 location 在配置文件中被明确指定了一个 content handler,那么Nginx 就可以通过对 location 的匹配,直接找到这个对应的 handler,并把这个 request 交给这个 content handler 去处理。这样的配置指令包括像,perl,flv,proxy_pass,mp4等。
如果一个 request 对应的 location 并没有直接有配置的 content handler,那么 Nginx 依次尝试:
- 如果一个 location 里面有配置 random_index on,那么随机选择一个文件,发送给客户端。
- 如果一个 location 里面有配置 index 指令,那么发送 index 指令指明的文件,给客户端。
- 如果一个 location 里面有配置 autoindex on,那么就发送请求地址对应的服务端路径下的文件列表给客户端。
- 如果这个 request 对应的 location 上有设置 gzip_static on,那么就查找是否有对应的
.gz
文件存在,有的话,就发送这个给客户端(客户端支持 gzip 的情况下)。 - 请求的 URI 如果对应一个静态文件,static module 就发送静态文件的内容到客户端。
内容产生阶段完成以后,生成的输出会被传递到 filter 模块去进行处理。filter 模块也是与 location 相关的。所有的 fiter 模块都被组织成一条链。输出会依次穿越所有的 filter,直到有一个 filter 模块的返回值表明已经处理完成。
这里列举几个常见的 filter 模块,例如:
- server-side includes。
- XSLT filtering。
- 图像缩放之类的。
- gzip 压缩。
在所有的 filter 中,有几个 filter 模块需要关注一下。按照调用的顺序依次说明如下:
- write: 写输出到客户端,实际上是写到连接对应的 socket 上。
- postpone: 这个 filter 是负责 subrequest 的,也就是子请求的。
- copy: 将一些需要复制的 buf(文件或者内存)重新复制一份然后交给剩余的 body filter 处理。
七、upstream模块
1、upstream 模块
将使 Nginx 跨越单机的限制,完成网络数据的接收、处理和转发。
数据转发功能,为 Nginx 提供了跨越单机的横向处理能力,使 Nginx 摆脱只能为终端节点提供单一功能的限制,而使它具备了网路应用级别的拆分、封装和整合的战略功能。在云模型大行其道的今天,数据转发是 Nginx 有能力构建一个网络应用的关键组件。
upstream 属于 handler,只是他不产生自己的内容,而是通过请求后端服务器得到内容,所以才称为 upstream(上游)。请求并取得响应内容的整个过程已经被封装到 Nginx 内部,所以 upstream 模块只需要开发若干回调函数,完成构造请求和解析响应等具体的工作。
2、负载均衡模块
负载均衡模块用于从upstream
指令定义的后端主机列表中选取一台主机。
Nginx 先使用负载均衡模块找到一台主机,再使用 upstream 模块实现与这台主机的交互。
如果需要使用 ip hash 的负载均衡算法:
upstream test {
ip_hash;
server 192.168.0.1;
server 192.168.0.2;
}