Nginx心法
说明
- 由原来的《认识Nginx》更名为《Nginx心法》
- 本文对应的版本:Tengine Tag
3.0.0
Nginx是谁?它是反向代理服务器的代表之一,是高性能和模块化开发的典范,更是我的老师。
认识它,你将免费获得“最佳的代理体验”、“优雅的代码规范”、“极致的性能设计”和“优秀的模块思想”,还有无数个值得品味的编程细节。
现在比较常用的两个Nginx项目分别为:
考虑到 Tengine 相较于 Nginx 增加了不少特性,本文以 Tengine 为分析对象。
进程模型¶

如上所示,Nginx设计上将程序分为4类,分别为:
-
Master:主进程,是其他3类程序的父进程,负责监控并管理其他进程的生命周期,同时也负责加载配置,其他程序通过继承的方式来获取配置信息。
- 管理生命周期的方式有两种:
- 一种是监控后触发,比如:在发现“Worker 1”异常退出后,主动拉起新的“Worker”取代“Worker 1”。
- 一种是管理员控制,比如:管理员通过命令
nginx -s reload
告诉“Master”去重载配置,此时“Master”会生成新的“Worker”并将旧的“Worker”退出。
- 管理生命周期的方式有两种:
-
管理进程:控制面程序,向上提供北向管理接口,向下负责控制“工作进程”如何处理业务。
- 控制的传输方式是管道(PIPE),支持的命令需要由“工作进程”提供。
- 虽然Nginx没有指出,但是我们应当清楚在控制上“管理进程”和“工作进程”是具有差异性的,具体差异体现在:
- “工作进程”应当是无状态的;
- “工作进程”在遇到“获取数据”类的消息时,应当只提供元数据,不进行任何渲染处理,渲染工作交由“管理进程”来做。
-
工作进程:数据面程序,负责处理业务数据,以完成业务目标。
-
独立进程:由Tengine设计,提供一种可以创建独立进程的机制。
- 这样可以创建一些不受框架约束的独立进程,比如使用不支持异步的 IO库 完成一些 IO 工作,开发一个独立运行的 syslog 程序以对接 syslog 服务器。
这样就很清晰了,“系统管理员”通过“Master”来控制 Nginx 的启动、停止、重载配置、升级等,而“管理员”或“管理程序”则通过“管理进程”来控制业务行为。换言之,“Master”是老板,“管理进程”是管理层领导,而“工作进程”则是基层员工,最后的“独立进程”则有点像外包,具有一定的灵活性,方便扩展。
看到这里,相信会引发一定的争议。因为看的人可能会说:Nginx 没有 “管理进程”,更没有管控数三面分离这一说,有的只是 “Master-Worker” 模型。
其实,如果我们想真正的去看清一件事物,是不能仅留存于过去的状态,或者说停止了向前思考的过程,应当适当的带入未来的思想,否则只会人云亦云,永远寄于事物之下。换言之,Nginx也是经过发展后的产物,它也是有过程和变化的属性,如果不能充分认识其历史和局限并时刻思考其发展趋势,那么就永远也无法掌控它,将永远停留在使用中。
Nginx作为开源项目而到了不同人手上,应当有不同的发展方向,这也是开源项目的魅力所在。比如Tengine项目就是在Nginx的基础上进行二次开发,以满足阿里巴巴的业务需求,同时也将这些需求开源出来,让更多的人受益。
我们将管控数三面分离带入到Nginx中是顺滑的,完全可行的一步:
-
”工作进程“中的各个Worker是工作对等体(worker跟worker之间没有区别,一个worker所作的事情,在另一个worker都能完整复刻和承担),仅是根据配置或指令去处理数据。正因这个性质,worker在异常退出后的重启是无需特殊处理的,这也符合了”数据面“定义和要求。
- 但也具有一定挑战性,包括但不限于:进程间的数据同步和状态共享。
-
我们只需要在”工作进程”与“管理进程”中引入IPC,并将控制过程移交给“管理进程“就可以实现”管理进程“控制”工作进程“的效果,也就是”控制面“与”数据面“的分离。
-
紧接着,”管理进程“只要提供北向API即可实现管控分离,从而达到管控数三面分离。这时我们便可以轻而易举的实现对Nginx进行实时控制,比如:获取统计信息、调整负载均衡算法等。
- 这里会遇到一些挑战,包括但不限于:如何持久化动态修改后的配置
系统架构¶
我第一次接触 Nginx 的时候是在 2018 年的时候,那时候刚毕业工作1年多,有幸被安排到服务器负载均衡组,开启了 Nginx 的使用和开发。
初接触 Nginx 时间是比较晚的,因此对 Nginx 的发展历程并不清楚,但那时候还没有Stream
业务,因此 Nginx 的出身应该是 HTTP
业务。HTTP
业务是 Nginx 的最大特色,功能也最丰富和复杂,因此想要完全掌握也具有一定挑战性。为了更好的认识 Nginx,因此这里重点介绍 Nginx 的基础架构以及HTTp
业务。
体系结构¶
首先从体系结构开始,Nginx可以大致可以分为4个部分,分别为“管理”、“业务”、“组件”和“基础库”,如下所示:

在Nginx中其实并没有区分这些,“组件”和“基础库”也是放到一起的,但是为了更好的理解,于是根据其API所负责的层面及粒度进行分层分类。“基础库”主要是一些算法和数据结构,“组件”则是能令业务更好的复用和开发的抽象,包括网络层面和系统层面。“组件”和“基础库”的实现一般都在src/core
目录中。
同样的,Nginx也没有管理的概念,但这些是任何一个程序都应当包括或扩展的一部分,毕竟程序是需要可管理和可控制的,尤其是当出现问题时,需要有一些调试手段。
业务则集中在4部分,其中“HTTP代理”为核心业务,“Proc”则是阿里拓展出来的功能,用于满足一些无法跟Nginx框架对接的特殊业务需求,大大的增强了Nginx的业务扩展能力。
系统架构¶
Nginx的框架是由上下文限定的,即不同的上下文有不同的框架,框架大致可分为两类,一类是Nginx的外框架,支撑和指导业务框架的开发,提供可靠的事件、内存管理、定时器、配置和日志功能供业务使用,并且定义和限定了模块范围,这样可以保证扩展业务成为可能,且业务之间的能保持隔离;另一类则是业务框架,不同的业务可以定义自己的业务框架。具体如下图所示:

从上图不难看出,每种业务和Nginx外框架之间都存在一个“核心模块”,该模块是业务框架与Nginx外框架的桥梁,它负责定义业务的配置解析规则、模块以及框架,并将自身的业务注册到Nginx中,可以这么说,有了这一层设计后,既可以保证业务与框架的隔离,也能保证业务的扩展性。
但“核心模块”只负责定义和初始化业务框架,其本身并不驱动业务的运转,因此在每个业务中都会存在一个被称为“第0模块”的业务模块,它会具体化业务的配置和框架,驱动业务“核心模块”定义的各阶段的运行。因此各业务模块实际是运行在“第0模块”之上的。
因此我们可以得出的如下图所示的框架上下文:

“Nginx Context”在Nginx中的表现为ngx_cycle
,该全局变量指向当前程序的上下文,每当重新加载配置或更新二进制程序后,都会创建一个新的上下文并由ngx_cycle
指向。
因为Nginx是高度模块化的,有种“模块驱动”的感觉,因此它会把上述各业务上下文再细分为“模块上下文”,即每个模块都有自己的上下文,但与“Nginx Context”有点不同,这里的 Context 指的只是“配置上下文”,而“Nginx Context”则包括了“配置上下文”和“程序上下文”。同时为了保证访问的速度,用数组实现该设计:

如上图所示,我们变更了名字以更符合其定义,即将“xxx框架 Context”改为“xxx配置 Context”。“Nginx Context”的第K
位置指向的是“HTTP配置 Context”,M
指向“Stream配置 Context”,N
指向“Mail配置 Context”,I
指向“Proc配置 Context”。最终每个业务的上下文还会有自己的“Context 数组”去存储各自业务模块的配置上下文。而ngx_cycle
除此之外还有很多全局的属性存在,因此在程序运行的任何时候,都可以通过它去获取想要的数据。
该上下文是静态的,即在程序从启动进入到就绪状态后就确定了,它主要由配置决定。Nginx对启动过程有明确的定义,分为以下几个过程:

图中的create_conf
、conf_parse
、init_conf
和init_module
是发生在“初始化 Nginx Context期间”,其中把“解析命令行参量”和“解析配置文件”放到了一起,用conf_parse
表示,其中前三个过程是完成加载配置的过程。在解释上面流程之前,必须要先清楚一件事,那就是任何类型模块都首先是“Nginx 模块”,然后才有自己的类型,这里就有点“派生”的意思在里面了,因为它必须属于某个基类,而“Nginx 模块”就是基类。
那对于“Nginx模块”来说,一定有如下属性和方法:

如上图所示,“Nginx模块”的属性有如下:
name
:模块名称,用于标识模块。该属性实际上并没有用。index
:模块索引,用于索引“Nginx Context”。比如 A 模块,想获得在“Nginx Context”中自己的位置,就可以通过该索引来获取。ctx_index
:业务上下文的索引,如果一个模块是某个业务的子模块,那么就可以通过该索引来获取自己在业务上下文中的位置。type
:模块类型,用来标识该模块是核心模块还是属于某个业务的子模块。注意,Nginx外框架只定义了“核心模块”,其他类型由业务定义。ctx
:顾名思义,Context 的意思,与type
相对应,用于完成模块的初始化工作,换句话说,用于完成模块的上下文初始化。ctx
是一组方法,可以抽样的理解为继承“Nginx 模块”的子类后可以重写的方法,它会在“核心模块”定义的“启动/初始化/加载配置”流程中被调用,因此不同的模块类型其ctx
是不同的。commands
:Nginx将配置的指令称为“command”,因此这里引用模块自己定义的命令。后文讲解“配置”时再展开讨论。
它有如下几个方法,对应着“启动过程”中的几个关键点:
init_module
:初始化模块,这里可以做一些程序级别的一次性加载工作,并且是在配置加载之前。init_process
:初始化工作进程,这里可以对每个worker进行一次性初始化工作,比如当配置成功加载后,你需要根据配置来为每个工作进程初始化一些数据结构,那么就可以在这里完成。exit_process
:退出工作进程时的清理工作,比如释放一下在init_process
中创建的资源。exit_master
:退出主进程时的清理工作,比如释放一下在init_module
中创建的资源。
那么由“核心模块”以及4大业务组成的各模块,就有如下关系:

配置框架¶
对于Nginx开发者来说,配置是一个非常重要概念。在Nginx中,开发者在不了解全貌的情况下完成命令的开发和修改,这完全得益于Nginx优秀的配置框架。
接着看一下Nginx的配置解析流程:

为了更好的理解配置解析框架,这里贴出一段配置文件的内容:
worker_processes 1;
user root;
error_log logs/info.log info;
events {
use epoll;
worker_connections 1024;
}
http {
#
# default config
#
include mime.types;
default_type application/octet-stream;
sendfile on;
log_format default '$remote_addr - $remote_user [$time_local] "$request" $status';
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
proxy_set_header Upgrade $http_upgrade;
proxy_http_version 1.1;
types {
text/html html;
image/gif gif;
image/jpeg jpg;
}
#
# HTTPS代理
#
server {
listen 443 ssl;
server_name singapore.proxy.homqyy.cn;
ssl_certificate certs/singapore.example.cn/fullchain.pem;
ssl_certificate_key certs/singapore.example.cn/privkey.pem;
ssl_verify_client on;
ssl_client_certificate proxy_ca_chain.pem;
ssl_verify_depth 3;
# 启用WEB代理,即代理CONNECT请求
proxy_connect;
proxy_connect_allow all;
proxy_connect_connect_timeout 120;
proxy_connect_read_timeout 5m;
proxy_connect_send_timeout 5m;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_pass $scheme://$host:$server_port;
}
}
}
- Nginx以“核心模块(
NGX_CORE_MODULE
)”、“主配置(NGX_MAIN_CONF
)”作为起始开始执行配置解析; - 在解析配置时,首先会读取到
worker_processes 1;
,并将其拆分成worker_processes
和1
两个部分,然后放入到cf->args
中; -
由于是
;
结尾,因此是解析结果为OK
,接着会“执行配置handler”-
遍历所有模块的commands,并找到
ngx_core_commands
中同名配置:static ngx_command_t ngx_core_commands[] = { /* 省略... */ { ngx_string("worker_processes"), /* name */ NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1, /* type */ ngx_set_worker_processes, /* set */ 0, /* conf_offset */ 0, /* variable offset */ NULL }, /* post_handler */ /* 省略... */ ngx_null_command } ngx_module_t ngx_core_module = { NGX_MODULE_V1, &ngx_core_module_ctx, /* module context */ ngx_core_commands, /* module directives */ NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING };
-
检查命令位置:有效,支持
NGX_MAIN_CONF
; - 检查参数个数:有效,是
NGX_CONF_TAKE1
,即只支持一个参数; - 由于是
DIRECT_CONF
,因此获取槽位的配置; - 执行命令:调用
ngx_set_worker_processes
- 返回执行结果
-
接着快进到events {...}
,讲解一下 Block 的解析过程:
- 在解析配置时,会读取到
events {;
,并只获取到一个字的event
,将其放入到cf->args
中; -
由于是
{
结尾,因此是解析结果为block start
,接着会“执行配置handler”:-
遍历所有模块的commands,并找到
ngx_events_commands
中同名配置:static ngx_command_t ngx_events_commands[] = { { ngx_string("events"), /* name */ NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, /* type */ ngx_events_block, /* set */ 0, /* conf_offset */ 0, /* variable_offset */ NULL }, /* post handler */ ngx_null_command }; ngx_module_t ngx_events_module = { NGX_MODULE_V1, &ngx_events_module_ctx, /* module context */ ngx_events_commands, /* module directives */ NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING };
-
检查命令位置:有效,支持
NGX_MAIN_CONF
; - 检查参数个数:有效,是
NGX_CONF_NOARGS
,即不支持参数; - 由于是“块配置”(
!DIRECT_CONF && MAIN_CONF
),因此获取槽位的配置指针; -
执行命令:调用
ngx_events_block
:static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { void ***ctx; // 省略... // 1. 创建 events 的配置上下文 ctx = ngx_pcalloc(cf->pool, sizeof(void *)); if (ctx == NULL) { return NGX_CONF_ERROR; } *ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *)); if (*ctx == NULL) { return NGX_CONF_ERROR; } *(void **) conf = ctx; // 2. 将上下文更新到 配置指针 中 // 省略... // 调用配置解析 pcf = *cf; // 3. 保存原有的配置上下文 cf->ctx = ctx; // 4. 设置上下文 cf->module_type = NGX_EVENT_MODULE; // 5. 设置模块类型 cf->cmd_type = NGX_EVENT_CONF; // 6. 设置命令位置 rv = ngx_conf_parse(cf, NULL); // 7. 调用配置解析 *cf = pcf; // 8. 回复原有的配置上下文 }
- 在第7步的时候,会嵌套调用
ngx_conf_parse
,此时区别在于ctx
、module_type
和cmd_type
都改成 events 自己定义的了,然后继续解析子模块的配置。
- 在第7步的时候,会嵌套调用
-
在解析配置时,会读取到
use epoll;
,并将其拆分成use
和epoll
两个部分,然后放入到cf->args
中; -
由于是
;
结尾,因此解析结果为OK
,接着会“执行配置handler”-
遍历所有模块的commands,并找到
ngx_core_commnads
中同名配置:
-
-
检查命令位置:有效,支持
NGX_EVENT_CONF
; - 检查参数个数:有效,是
NGX_CONF_TAKE1
,即只支持一个参数; -
由于是“自定义配置上下文”(
!NGX_DIRECT_CONF && !NGX_MAIN_CONF
),因此获取指定上下文配置:- 前文提到,自己定义的上下文用
ctx_index
索引。 - 关于
ctx_index
很好理解,因为对于自身的配置上下文来说,并非所有的“Nginx 模块”都需要,因此如果直接使用index
则会浪费空间,因为需要为所有“Nginx 模块”都创建一个槽位。但是,如果使用ctx_index
来从0开始索引自身上下文的位置,则只需要给同类型的“Nginx 模块”创建槽位即可。
- 前文提到,自己定义的上下文用
-
执行命令:调用
ngx_event_use
-
-
返回执行结果
events 的嵌套配置解析的效果如下:

从上文的“配置解析流程”中可以看到,还有一个特殊的处理分支是“自定义解析函数”,即模块可以定义自己的配置解析函数,也是配置框架的另一个关键功能。从流程中我们知道,想要“自定义解析函数”只需要在Block
的处理函数中去定义cf->handler
即可,为了更好的展示,我们直接查看http{}
中的types
指令的处理函数:
static char *
ngx_http_core_types(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf = conf;
char *rv;
ngx_conf_t save;
if (clcf->types == NULL) {
clcf->types = ngx_array_create(cf->pool, 64, sizeof(ngx_hash_key_t));
if (clcf->types == NULL) {
return NGX_CONF_ERROR;
}
}
save = *cf;
cf->handler = ngx_http_core_type;
cf->handler_conf = conf;
rv = ngx_conf_parse(cf, NULL);
*cf = save;
return rv;
}
从上面代码可以看出,自定义解析函数只需要设置cf->handler
和cf->handler_conf
即可。
启动流程(0%)¶
核心模块(0%)¶
HTTP业务(0%)¶
Stream业务(5%)¶
Stream业务,即流代理。其代理应用层以下的业务流量,是应用代理能力的补充。最直接就是传输层的TCP和UDP代理,同时也支持“Proxy protocol”(v1和v2)。如何理解流代理,最简约的表达就是“传输层”代理,不考虑数据的应用特征,只进行数据代理/转发,同时在需要时提供“会话层”和“表示层”的支撑,比如TLS加密,这也是源生Nginx的“Stream代理”中携带的能力,基于此思想,我们也能在需要的时候去扩展其“传输能力”、“会话能力”和“表示能力”,完成自身的定制化开发。其协议栈如下所示:

从图中可以直接看出“Stream代理”的完整协议栈,正如前面所说,我们可以根据需要去开阔能力,比如在“传输协议”中加入“SOCK5协议”即可丰富Stream的协议代理能力;在UDP之上中加入“KCP(会话协议框架)”即可增强UDP的代理能力,提供重视时效性的传输需求的流代理业务。
Mail业务(0%)¶
安装和部署¶
也可可参阅:“Nginx官网-安装Nginx”
1. 安装依赖¶
2. 添加nginx源到仓库¶
创建文件 /etc/yum.repos.d/nginx.repo 并写入以下内容:
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
3. 安装Nginx¶
基本使用¶
也可参阅:“Nginx官网-入门指导”
默认配置文件路径:conf/nginx.conf(相当于/usr/local/nginx/conf/nginx.conf)
默认日志路径:logs/(相当于/usr/local/nginx/logs/)
默认页面资源路径:html/(相当于/usr/local/nginx/html)
启动、停止和重载配置¶
通过发送信号来控制Nginx的启动、停止和重载。发送信号的方法是执行以下命令$ nginx -s <signal>
,这里的signal
可以有以下三种:
- start:启动Nginx
- stop:停止Nginx
- reload:重载配置。在修改Nginx的配置文件后,可以通过此命令来加载新的配置。届时旧的连接会继续在旧的worker上运行(相当于运行旧的配置),但新的连接将会由新worker处理(此worker运行新配置)
配置HTTP服务器¶
假设有如下需求:搭建一个HTTP服务器,监听默认端口80,资源路径在/var/www
,首页在/var/www/index.html
。
修改Nginx配置文件nginx.conf为以下内容:
http {
server {
listen 80; # HTTP服务器,监听端口为80
localtion / {
root /var/www/; # 设置资源的默认路径“/var/www/”
}
}
}
接着重载Nginx配置即可:$ nginx -s reload
配置HTTPS服务器¶
假设有如下需求:搭建一个HTTPS服务器,监听默认端口443,资源路径在/var/www
,首页在/var/www/index.html
。
修改Nginx配置文件nginx.conf为以下内容:
http {
server {
listen 443 ssl; # HTTPS服务器,监听端口为443
localtion / {
root /var/www/; # 设置资源的默认路径“/var/www/”
}
}
}
接着重载Nginx配置即可:$ nginx -s reload
!!! warn “注意” HTTPS和HTTP服务器的区别就是在listen指令中,是否有配置ssl参数。
配置HTTP代理¶
假设有如下需求:搭建一个HTTP代理,监听默认端口80,代理到 192.168.1.1:8080 服务器。
修改Nginx配置文件nginx.conf为以下内容:
http {
server {
listen 80; # 监听端口为80
localtion / {
proxy_pass http://192.168.1.1:8080; # 代理到 192.168.1.1:8080 服务器
}
}
}