07-源码、模块与底层机制

1. 阶段定位

第七阶段的目标,是从“会部署、会配置、会排障 Nginx”,进一步进入“能从源码层面理解 Nginx 为什么这样工作”。前六个阶段已经覆盖了部署、配置、请求匹配、反向代理、负载均衡、HTTPS、安全治理、缓存、性能优化、生产排障、高可用和运维体系;本阶段要把这些能力向下沉一层,理解它们在 Nginx 内部是如何被组织和执行的。

很多人在学习 Nginx 源码时容易陷入两个极端:一开始就想完整读完所有源码,结果很快被目录、宏、回调、结构体和平台差异淹没;或者只阅读几篇源码分析文章,知道一些名词,但无法把配置、请求、事件、upstream、filter、内存池和模块机制串成完整链路。

本阶段不追求逐行背源码,而是围绕核心主线建立可验证的源码理解:

1
2
3
4
5
6
7
8
9
10
11
Nginx 如何启动
-> 配置如何解析成模块配置
-> worker 如何进入事件循环
-> 连接如何被 accept
-> HTTP 请求如何解析
-> 请求如何进入 phase handler
-> proxy_pass 如何进入 upstream
-> 响应如何经过 header/body filter
-> 日志如何写入
-> 内存、buffer、timer、shared memory 如何支撑高并发
-> reload、热升级、模块加载如何保持生产可用

完成本阶段后,你应该能回答:

  • Nginx 为什么能用较少 worker 承载大量并发连接。
  • master 和 worker 的职责边界是什么。
  • reload 为什么通常不会中断正在处理的请求。
  • 配置文件是如何被解析、合并并交给模块使用的。
  • 一次 HTTP 请求从连接建立到 access log 写入经历哪些内部阶段。
  • locationrewriteaccesscontentproxy_passlog 在源码中分别对应哪些处理阶段。
  • upstream 是如何创建连接、发送请求、接收响应、处理失败重试和超时的。
  • header filter 与 body filter 为什么可以组成过滤链。
  • ngx_pool_tngx_buf_tngx_chain_t 分别解决什么问题。
  • limit_reqproxy_cache 等能力为什么需要 shared memory zone。
  • 如何编写一个最小 HTTP content handler 模块或 header filter 模块。

本阶段学完后,不一定意味着你要成为 Nginx 内核开发者,但你应该具备资深 Nginx 工程师的源码定位能力:遇到复杂线上问题时,知道该看哪个模块、哪个结构、哪个阶段,而不是只靠猜测和搜索配置模板。

2. 学习边界

2.1 本阶段重点

本阶段重点关注 Nginx 开源版本核心机制,主要包括:

  • 源码目录结构和编译参数。
  • debug 编译、调试符号和源码阅读环境。
  • master/worker 启动流程。
  • 配置解析、配置上下文和模块配置合并。
  • 模块体系:core module、event module、http module、mail module、stream module。
  • cycle、conf、module、connection、request、upstream 等核心结构。
  • 事件驱动模型:epoll/kqueue/select、非阻塞 IO、accept、事件循环。
  • HTTP 请求解析流程。
  • phase handler:rewrite、access、content、log 等阶段。
  • upstream 反向代理流程。
  • header filter、body filter 响应过滤链。
  • 内存池 ngx_pool_t
  • buffer、chain 和输出链路。
  • timer、红黑树、超时事件。
  • slab、shared memory、zone。
  • reload、平滑退出、二进制热升级。
  • 静态模块、动态模块和第三方模块开发基础。
  • 最小 HTTP content handler 模块开发。
  • 最小 header filter 或 log 阶段模块开发。

2.2 本阶段暂不深入

下面内容与源码有关,但不是本阶段主线:

  • Nginx Plus 商业版私有能力源码。
  • OpenResty Lua VM、cosocket、ngx_lua 的完整实现。
  • Tengine、APISIX、Kong、Ingress Controller 的源码体系。
  • HTTP/3、QUIC、BoringSSL、OpenSSL 内部源码细节。
  • 自研高性能网络库与 Nginx 的完整性能对比。
  • 操作系统内核网络协议栈源码。
  • 复杂第三方模块的完整工业级实现。
  • Nginx 所有指令的源码逐项分析。

本阶段的学习目标不是把 Nginx 所有源码读完,而是把“启动、配置、事件、请求、upstream、filter、内存、模块”这几条核心链路读通。

3. 学习准备

3.1 建议环境

建议使用 Linux 环境阅读和调试 Nginx 源码。可以使用物理机、虚拟机、云服务器或 WSL,但最好具备完整编译和调试能力。

环境项 建议
操作系统 Ubuntu、Debian、CentOS、Rocky Linux、AlmaLinux 均可
Nginx 版本 建议选择稳定版,例如 1.24.x 或较新的 mainline 版本
编译器 GCC 或 Clang
调试工具 gdb、strace、ltrace、perf、addr2line
阅读工具 VS Code、CLion、vim、ctags、cscope、ripgrep
压测工具 wrk、ab、hey、curl
抓包工具 tcpdump、Wireshark

建议先使用一个固定源码版本学习,不要频繁切换版本。不同版本内部结构会有差异,但核心思想长期稳定。

3.2 依赖安装示例

Ubuntu/Debian:

1
2
3
sudo apt update
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev \
openssl libssl-dev gdb strace lsof curl wget git cscope exuberant-ctags

CentOS/Rocky/AlmaLinux:

1
2
3
sudo yum groupinstall -y "Development Tools"
sudo yum install -y pcre pcre-devel zlib zlib-devel openssl openssl-devel \
gdb strace lsof curl wget git cscope ctags

3.3 下载源码与 debug 编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz
cd nginx-1.24.0

./configure \
--prefix=/opt/nginx-debug \
--with-debug \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_realip_module \
--with-http_v2_module

make -j$(nproc)
sudo make install
/opt/nginx-debug/sbin/nginx -V

重点关注:

  • --prefix:安装目录。
  • --with-debug:是否开启 debug 日志能力。
  • --with-http_ssl_module:是否编译 SSL 模块。
  • --with-http_stub_status_module:是否编译状态模块。
  • --add-module:是否添加静态第三方模块。
  • --add-dynamic-module:是否添加动态第三方模块。

3.4 开启 debug 日志

--with-debug 只是编译支持 debug,还需要在配置中开启 debug 级别日志:

1
2
3
4
5
error_log logs/error.log debug;

events {
debug_connection 127.0.0.1;
}

debug_connection 可以避免所有客户端都输出 debug 日志,适合实验或小范围定位。

4. 源码目录结构

Nginx 源码目录非常清晰,先理解目录职责,比直接打开某个 .c 文件更重要。

1
2
3
4
5
6
7
8
nginx-1.24.0/
├── auto/ # configure 脚本使用的自动探测、编译配置脚本
├── conf/ # 默认配置文件模板
├── contrib/ # 辅助工具,例如 vim 语法文件
├── docs/ # 部分文档
├── misc/ # 杂项脚本
├── src/ # 核心源码
└── objs/ # configure 和 make 生成的中间文件、模块列表、目标文件

src/ 是源码阅读重点:

1
2
3
4
5
6
7
src/
├── core/ # 核心框架、内存池、字符串、数组、日志、cycle、配置等
├── event/ # 事件模块、连接、定时器、epoll/kqueue/select 等
├── http/ # HTTP 核心、请求解析、phase、upstream、filter、各 HTTP 模块
├── mail/ # 邮件代理模块
├── os/ # 操作系统相关封装,例如 unix、win32
└── stream/ # TCP/UDP 四层代理模块

重点文件速查:

文件 重点内容
src/core/nginx.c main 入口、启动参数解析、cycle 初始化、进程模型入口
src/core/ngx_cycle.c cycle 初始化、打开日志、监听端口、模块初始化
src/core/ngx_conf_file.c 配置文件解析框架
src/core/ngx_module.c 模块编号、模块签名、动态模块支持
src/os/unix/ngx_process_cycle.c master/worker 进程循环
src/event/ngx_event.c 事件模块初始化
src/event/modules/ngx_epoll_module.c Linux epoll 事件模块
src/event/ngx_event_accept.c accept 新连接
src/http/ngx_http.c HTTP 模块初始化、server/location 合并
src/http/ngx_http_request.c HTTP 请求创建、读取、处理、结束
src/http/ngx_http_core_module.c HTTP 核心模块、phase engine、location
src/http/ngx_http_upstream.c upstream 通用框架
src/http/modules/ngx_http_proxy_module.c proxy_pass 模块实现
src/http/ngx_http_header_filter_module.c 默认 header filter
src/http/ngx_http_write_filter_module.c 输出 write filter
src/core/ngx_palloc.c 内存池实现
src/core/ngx_buf.c buffer 和 chain 工具函数
src/core/ngx_slab.c slab 分配器
src/event/ngx_event_timer.c 定时器红黑树

5. Nginx 核心设计思想

5.1 事件驱动

Nginx 不是为每个连接创建一个线程,而是使用事件驱动模型处理大量连接。

1
2
3
4
5
6
大量连接 fd
-> 注册到 epoll/kqueue/select
-> worker 阻塞等待事件
-> 连接可读/可写时触发回调
-> 回调中只处理当前能处理的部分
-> 没处理完继续挂回事件循环

事件驱动的优势是避免大量线程上下文切换,可以用少量 worker 处理大量空闲 keepalive 连接,网络 IO 不阻塞整个 worker,请求处理过程也能拆成多个事件回调。

5.2 非阻塞 IO

Nginx 的连接通常被设置为非阻塞。读不到数据时不会一直卡在 read,写不出去时不会一直卡在 write,当前操作暂时无法完成就返回 NGX_AGAIN,等待下一次事件通知。

返回值 含义
NGX_OK 当前步骤成功完成
NGX_ERROR 当前步骤失败
NGX_AGAIN 暂时无法完成,需要等待后续事件
NGX_DONE 当前流程已被接管或异步完成
NGX_DECLINED 当前模块不处理,交给后续模块

5.3 多进程 worker

Nginx 默认采用 master/worker 多进程模型:

1
2
3
4
5
6
7
8
9
10
11
master process
-> 读取配置
-> 打开监听端口
-> 创建 worker
-> 管理 reload、quit、reopen、upgrade 信号

worker process
-> 继承监听 fd
-> 进入事件循环
-> accept 连接
-> 处理请求和响应

多进程模型让 worker 之间相对隔离,可以利用多核 CPU,配合 reload 可以实现较平滑的配置切换,也避免了多线程共享复杂状态带来的锁竞争和内存安全问题。

5.4 模块化 pipeline

Nginx 的绝大多数能力都是模块实现的。配置指令属于模块,请求阶段由模块挂载 handler,响应输出由 filter 模块串成链。

1
2
3
4
5
6
配置指令
-> 模块解析配置
-> 初始化阶段注册 handler/filter
-> 请求到来时进入 phase engine
-> 不同模块在不同阶段处理请求
-> 响应经过 filter chain 输出

5.5 内存池

Nginx 高频使用内存池,而不是每次请求都频繁 malloc/free

1
2
3
为连接或请求创建 pool
-> 请求处理过程中从 pool 分配小块内存
-> 请求结束时统一释放整个 pool

内存池能减少频繁内存分配释放开销,降低内存碎片,简化错误路径清理逻辑,适合请求生命周期明确的场景。

6. 启动流程总览

Nginx 的入口在 src/core/nginx.cmain 函数。启动流程可以先按主干理解:

1
2
3
4
5
6
7
8
9
10
11
12
main
-> ngx_debug_init
-> ngx_strerror_init
-> ngx_get_options
-> ngx_time_init
-> ngx_regex_init
-> ngx_log_init
-> ngx_os_init
-> ngx_crc32_table_init
-> ngx_preinit_modules
-> ngx_init_cycle
-> ngx_signal_process 或 ngx_process_cycle

不要一开始纠结每个初始化函数的细节,先理解几个关键点:

  • ngx_get_options 解析 -c-p-s-t-V 等命令行参数。
  • ngx_preinit_modules 给模块分配 index。
  • ngx_init_cycle 是非常关键的初始化函数,会读取配置、初始化模块、打开监听端口、创建共享内存等。
  • ngx_process_cycle 根据进程模式进入 master 或 single process 逻辑。

6.1 nginx -t 做了什么

nginx -t 并不是简单检查文件是否存在,而是会走一遍较完整的配置解析和初始化流程,只是不真正进入正式服务循环。

1
2
3
4
5
6
7
8
解析命令行参数
-> 初始化 cycle
-> 解析 nginx.conf
-> 加载 include 文件
-> 检查指令上下文是否合法
-> 创建和合并模块配置
-> 尝试打开必要资源
-> 输出 syntax is ok / test is successful

所以 nginx -t 能发现很多问题:指令拼写错误、指令放错上下文、include 文件不存在、server/location 配置不合法、监听端口配置冲突、日志路径权限问题、证书文件读取失败等。但它不能替代完整联调,因为它不能证明 upstream 一定可用,也不能证明业务请求一定正确。

6.2 ngx_cycle_t 的意义

ngx_cycle_t 可以理解为 Nginx 当前运行周期的全局上下文,它包含配置上下文、监听端口、打开的文件、共享内存 zone、连接数组、日志对象、模块信息、路径和前缀。

1
2
3
4
5
一次启动或 reload
-> 创建一个新的 cycle
-> 解析配置并初始化资源
-> 如果成功,用新 cycle 替换旧 cycle
-> 如果失败,继续保留旧 cycle

这也是 reload 相对安全的重要原因之一:新配置初始化失败时,旧 worker 可以继续工作。

7. master/worker 进程模型

7.1 master 进程职责

master 进程不直接处理普通 HTTP 请求,它主要负责管理生命周期:

  • 读取和验证配置。
  • 打开监听端口。
  • 创建 worker 进程。
  • 处理信号。
  • reload 时启动新 worker 并通知旧 worker 退出。
  • worker 异常退出时按需拉起。
  • 日志 reopen。
  • 二进制热升级。

源码重点:

1
src/os/unix/ngx_process_cycle.c

重点函数:

  • ngx_master_process_cycle
  • ngx_start_worker_processes
  • ngx_spawn_process
  • ngx_signal_worker_processes
  • ngx_reap_children

7.2 worker 进程职责

worker 进程负责真正处理连接和请求:初始化 worker 级别模块、进入事件循环、accept 新连接、读取请求、执行 HTTP phase、访问 upstream、写响应、写日志、处理 keepalive 和优雅退出。

重点函数:

  • ngx_worker_process_cycle
  • ngx_worker_process_init
  • ngx_process_events_and_timers

worker 主循环可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for ( ;; ) {
if (ngx_exiting) {
/* 处理优雅退出 */
}

ngx_process_events_and_timers(cycle);

if (ngx_terminate) {
exit(0);
}

if (ngx_quit) {
/* 关闭监听 socket,不再接收新连接 */
}
}

7.3 reload 内部机制

执行:

1
nginx -s reload

本质是向 master 发送 HUP 信号。典型流程:

1
2
3
4
5
6
7
8
master 收到 HUP
-> 重新读取配置
-> 创建新的 cycle
-> 如果新配置失败,保留旧 worker
-> 如果新配置成功,启动新 worker
-> 通知旧 worker 优雅退出
-> 旧 worker 关闭监听 fd,不再接收新连接
-> 旧 worker 处理完已有请求后退出

注意事项:

  • reload 不是 restart,通常不会直接杀掉正在处理的请求。
  • 长连接、慢下载、WebSocket 可能导致旧 worker 长时间存在。
  • 如果配置错误,新 worker 不会成功接管,旧 worker 继续服务。
  • 如果修改了影响共享内存 zone 大小或名称的配置,要特别关注兼容性。
  • reload 不能解决所有问题,例如二进制文件升级需要走热升级或重启流程。

7.4 worker 为什么不是越多越好

常见建议是:

1
worker_processes auto;

worker 数通常接近 CPU 核数即可。过多 worker 可能导致进程调度开销增加、CPU cache 命中率下降、accept 竞争加重、上游连接数放大、日志和磁盘 IO 压力增加。源码层面理解 worker 模型后,就不会简单认为“并发高就把 worker 调到很大”。

8. 配置解析与模块初始化

8.1 配置不是普通文本

Nginx 配置文件最终会被解析成各个模块自己的配置结构。每个模块可以定义支持哪些指令、指令能出现在哪些上下文、指令参数数量和格式、指令解析函数、创建配置结构的函数、合并配置结构的函数以及初始化阶段回调。

一个指令是否合法,不仅看语法,还看它所属模块声明的上下文。

8.2 ngx_command_t

模块通过 ngx_command_t 声明配置指令。

1
2
3
4
5
6
7
8
9
10
typedef struct ngx_command_s ngx_command_t;

struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_uint_t conf;
ngx_uint_t offset;
void *post;
};

关键字段:

字段 含义
name 指令名称,例如 proxy_passrootlimit_req
type 指令上下文、参数数量、类型限制
set 指令解析函数
conf 指令属于 main/server/location 哪类配置
offset 写入模块配置结构的位置
post 附加处理或校验

例如一个指令只能出现在 location 中,通常会通过 type 标记限制上下文。

8.3 配置上下文

HTTP 模块常见三层配置:

1
2
3
main conf      http 级别
server conf server 级别
location conf location 级别

对应模块回调通常包括:

  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

配置继承和覆盖的本质,是父级配置和子级配置在 merge 阶段被合并。

1
2
3
4
5
6
7
8
9
10
11
http {
proxy_connect_timeout 3s;

server {
proxy_connect_timeout 5s;

location /api/ {
proxy_pass http://backend;
}
}
}

源码层面会让 location /api/ 最终拿到合并后的配置。如果 location 没显式设置,就继承 server 或 http 的值。

8.4 HTTP 模块初始化

HTTP 模块初始化大致做几类事情:

  • 创建 HTTP main/server/location 配置。
  • 解析 http {} 内部指令。
  • 收集 server 块。
  • 处理 listen 和 server_name。
  • 构建 location 树。
  • 合并 server/location 配置。
  • 初始化 phase engine。
  • 初始化 header/body filter 链。

重点文件:

1
2
src/http/ngx_http.c
src/http/ngx_http_core_module.c

8.5 学配置解析时的重点问题

阅读配置解析源码时,建议带着下面问题:

  • httpserverlocation 是如何嵌套解析的。
  • 一个模块如何声明自己的配置指令。
  • 指令为什么只能放在特定上下文。
  • server/location 配置如何继承和合并。
  • include 是如何展开的。
  • nginx -t 为什么能发现上下文错误。
  • 动态模块为什么也能提供新指令。

9. 模块体系

9.1 模块分类

Nginx 模块可以按层级分为:

模块类型 作用 示例
core module 核心框架和基础能力 ngx_core_module
event module 事件机制 ngx_epoll_modulengx_kqueue_module
http module HTTP 请求处理 ngx_http_core_modulengx_http_proxy_module
mail module 邮件代理 POP3/IMAP/SMTP 相关模块
stream module TCP/UDP 四层代理 ngx_stream_proxy_module

HTTP 模块还可以按职责分为:

HTTP 模块类别 作用 示例
handler 模块 生成或处理响应 static、proxy、fastcgi、stub_status
filter 模块 修改响应头或响应体 gzip、headers、chunked、write
upstream 模块 与后端通信 proxy、fastcgi、uwsgi、scgi
phase 模块 挂载到请求处理阶段 rewrite、access、limit_req、auth_basic
variable 模块 提供变量 geo、map、realip

9.2 ngx_module_t

每个模块都会有一个 ngx_module_t 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ngx_module_t ngx_http_xxx_module = {
NGX_MODULE_V1,
&ngx_http_xxx_module_ctx,
ngx_http_xxx_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};

其中:

  • ctx:模块上下文,定义模块初始化和配置创建/合并回调。
  • commands:模块支持的配置指令。
  • type:模块类型,例如 NGX_HTTP_MODULE
  • 其他回调用于 master/init process/init thread/exit 等生命周期。

9.3 静态模块与动态模块

静态模块:

1
2
3
./configure --add-module=/path/to/module
make
make install

特点是模块编译进 Nginx 二进制,启动时天然存在,部署简单,但升级模块通常需要重新编译 Nginx。

动态模块:

1
2
./configure --add-dynamic-module=/path/to/module
make modules

配置加载:

1
load_module modules/ngx_http_xxx_module.so;

特点是模块以 .so 文件加载,更灵活,但需要 ABI、编译参数和版本兼容,且 load_module 必须放在 main 上下文。

9.4 模块学习顺序

建议按下面顺序读模块:

1
2
3
4
5
6
7
8
ngx_http_static_module
-> ngx_http_stub_status_module
-> ngx_http_access_module
-> ngx_http_rewrite_module
-> ngx_http_proxy_module
-> ngx_http_headers_filter_module
-> ngx_http_gzip_filter_module
-> ngx_http_limit_req_module

原因是 static 模块能帮助理解 content handler;stub_status 模块代码相对短,适合理解最小模块形态;access/rewrite 能帮助理解 phase handler;proxy 能帮助理解 upstream;headers/gzip 能帮助理解 filter;limit_req 能帮助理解 shared memory 和 slab。

10. 事件模型

10.1 事件模块做什么

事件模块负责告诉 Nginx:哪些连接现在可读、哪些连接现在可写、哪些定时器超时。在 Linux 上,核心通常是 epoll:

1
2
3
4
5
socket fd
-> 设置非阻塞
-> 注册到 epoll
-> epoll_wait 等待事件
-> 触发 read/write handler

重点源码:

1
2
3
4
src/event/ngx_event.c
src/event/ngx_event_accept.c
src/event/ngx_event_timer.c
src/event/modules/ngx_epoll_module.c

10.2 ngx_event_t

事件通常用 ngx_event_t 表示,连接读写各有一个事件:

1
2
3
ngx_connection_t
-> read: ngx_event_t
-> write: ngx_event_t

事件里最重要的是 handler:

1
ev->handler(ev);

也就是说,当某个 fd 可读或可写时,Nginx 会调用对应事件上挂载的 handler。不同阶段会不断替换 handler,从而推进请求状态机。

10.3 连接接受流程

新连接接受大致流程:

1
2
3
4
5
6
7
8
9
监听 socket 可读
-> ngx_event_accept
-> accept4/accept
-> 从 connection 池获取 ngx_connection_t
-> 设置非阻塞
-> 创建连接 pool
-> 初始化 read/write event
-> 调用监听端口对应 handler
-> HTTP 场景进入 ngx_http_init_connection

重点函数:

  • ngx_event_accept
  • ngx_get_connection
  • ngx_http_init_connection

10.4 accept mutex 与 reuseport

多 worker 同时监听同一个端口时,可能出现惊群问题。Nginx 历史上使用 accept mutex 降低多个 worker 同时竞争 accept 的问题。新版本 Linux epoll 与内核能力已经缓解很多惊群问题,accept_mutexreuseport 又是两个不同方向的机制:reuseport 会让多个 worker 拥有独立监听 socket,由内核分发连接。

生产中是否开启相关参数,要结合系统版本、Nginx 版本和压测结果判断。

10.5 定时器

Nginx 使用红黑树管理事件定时器。典型超时包括:

  • client_header_timeout
  • client_body_timeout
  • send_timeout
  • proxy_connect_timeout
  • proxy_read_timeout
  • proxy_send_timeout
  • keepalive 超时

源码重点:

1
src/event/ngx_event_timer.c

定时器逻辑简化:

1
2
3
4
5
添加事件定时器
-> 插入红黑树
-> 事件循环计算最近超时时间
-> epoll_wait 最多等待该时间
-> 返回后处理已超时事件

这解释了为什么 Nginx 的 timeout 不是单独线程逐个扫描,而是与事件循环统一管理。

11. HTTP 请求生命周期

11.1 从连接到请求

HTTP 请求处理入口大致是:

1
2
3
4
5
6
7
8
9
ngx_event_accept
-> ngx_http_init_connection
-> ngx_http_wait_request_handler
-> ngx_http_create_request
-> ngx_http_process_request_line
-> ngx_http_process_request_headers
-> ngx_http_process_request
-> ngx_http_handler
-> phase engine

重点文件:

1
2
3
src/http/ngx_http_request.c
src/http/ngx_http_parse.c
src/http/ngx_http_core_module.c

11.2 请求行解析

请求行示例:

1
GET /api/user?id=1 HTTP/1.1

Nginx 要解析出 method、URI、args、HTTP version 和 request line 原始内容。常见变量来源如下:

变量 来源理解
$request 原始请求行
$request_method 请求方法
$request_uri 原始 URI,通常包含 query string
$uri 规范化后的 URI,可能被 rewrite 改写
$args query string
$is_args 有参数时为 ?,无参数时为空

11.3 请求头解析

Nginx 会解析 HTTP header,并把常用头放到结构体中。

请求头 源码学习关注点
Host 决定虚拟主机匹配
Connection 影响 keepalive
Content-Length 影响请求体读取
Transfer-Encoding 影响 chunked 请求体
Expect 可能触发 100 Continue
X-Forwarded-For 需要配合 realip 模块处理

请求头过大时,可能出现 400 Bad Request414 Request-URI Too Large494 Request Header Too Large。对应配置包括:

1
2
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

11.4 请求体读取

不是所有请求都会立即读取 body。不同模块有不同策略:静态文件 GET 请求通常不需要请求体;proxy 模块需要根据配置决定是否把请求体转发给 upstream;access 阶段通常只依赖 header 和变量。

请求体相关配置:

1
2
3
4
client_max_body_size 100m;
client_body_buffer_size 128k;
client_body_temp_path logs/client_body_temp;
client_body_timeout 60s;

理解源码时要注意:请求体读取可能是异步的,读不完整时会返回,等待下一次可读事件。

11.5 请求结束

请求结束并不只是返回状态码,还包括释放请求资源、写 access log、处理 lingering close、判断连接是否 keepalive、复用连接等待下一个请求,或关闭连接并释放 connection。

重点函数:

  • ngx_http_finalize_request
  • ngx_http_free_request
  • ngx_http_close_request
  • ngx_http_set_keepalive
  • ngx_http_close_connection

12. phase handler 请求阶段

12.1 phase engine 的意义

Nginx HTTP 请求不是进入一个大函数从头执行到尾,而是被拆成多个阶段。每个阶段可以挂载多个 handler。

1
2
3
4
5
6
7
8
9
10
11
post read
-> server rewrite
-> find config
-> rewrite
-> post rewrite
-> preaccess
-> access
-> post access
-> precontent
-> content
-> log

这就是很多配置指令“看起来顺序相近,但执行阶段不同”的根本原因。

12.2 主要阶段说明

阶段 作用 常见模块
post read 读取请求头后早期处理 realip
server rewrite server 级 rewrite rewrite
find config 查找 location core
rewrite location 级 rewrite rewrite
post rewrite 处理 URI 改写后的跳转 core
preaccess 访问控制前置处理 limit_req、limit_conn
access 权限控制 access、auth_basic、auth_request
post access access 结果统一处理 core
precontent content 前处理 try_files、mirror 等
content 生成响应或发起 upstream static、proxy、fastcgi、stub_status
log 请求结束后写日志 log

12.3 rewrite 阶段

rewrite 模块运行在 rewrite 阶段,可能触发 URI 重新查找 location。典型风险包括:last 会触发新的 location 匹配;break 停止当前 rewrite,但不一定重新匹配 location;复杂正则会增加 CPU 开销和维护成本;多次内部跳转可能触发循环保护。

源码学习重点:

1
2
src/http/modules/ngx_http_rewrite_module.c
src/http/ngx_http_script.c

12.4 access 阶段

access 阶段常见模块包括 ngx_http_access_modulengx_http_auth_basic_modulengx_http_auth_request_moduleallow/deny 是非常典型的 access 阶段处理。

1
2
3
4
location /admin/ {
allow 10.0.0.0/8;
deny all;
}

源码阅读时关注:模块如何注册 access handler;多个 access handler 之间如何组合;satisfy anysatisfy all 如何影响结果;access 拒绝如何变成 403。

12.5 content 阶段

content 阶段负责真正生成响应或把请求交给 upstream。典型 content handler 包括 static、proxy、fastcgi、return、stub_status。一个 location 通常最终只有一个主要 content handler,否则会发生覆盖或冲突。

12.6 log 阶段

log 阶段在请求结束后执行,典型模块是:

1
src/http/modules/ngx_http_log_module.c

access log 中很多变量是在请求生命周期中逐步填充的,例如 $status$body_bytes_sent$request_time$upstream_status$upstream_response_time。理解 log 阶段有助于解释为什么某些请求虽然失败,也仍然能写 access log。

13. location 匹配的源码理解

13.1 配置阶段构建 location

Nginx 在配置解析阶段会收集 location,并构建适合查找的数据结构。运行时请求进来后,不是简单按配置文件行号逐行匹配。

1
2
3
4
5
location = /exact { }
location ^~ /static/ { }
location /api/ { }
location ~ \.php$ { }
location ~* \.jpg$ { }

运行时查找要结合精确匹配、最长前缀匹配、^~ 前缀短路正则、正则匹配、命名 location 和内部跳转。

13.2 为什么 URI 改写会重新匹配

当 rewrite 使用 lasttry_files 内部跳转时,Nginx 可能重新进入 location 查找。

1
2
3
4
5
原始 URI /a
-> 匹配 location /a
-> rewrite 到 /b
-> 重新查找 location
-> 匹配 location /b

Nginx 对内部跳转次数有限制,避免配置错误导致无限循环。

13.3 学习 location 源码时关注什么

不要只背匹配优先级,要从源码角度理解 location 在配置阶段如何分类、普通前缀 location 如何组织、正则 location 为什么在前缀匹配后处理、命名 location 为什么只能内部跳转、rewrite/try_files/error_page 如何触发内部跳转,以及 alias 为什么需要特别处理路径拼接。

重点文件:

1
src/http/ngx_http_core_module.c

14. upstream 机制

14.1 upstream 是什么

upstream 是 Nginx 与后端服务通信的通用框架。proxy_passfastcgi_passuwsgi_passscgi_pass 等都基于 upstream 思想。

1
2
3
4
5
6
7
8
9
client
-> Nginx 接收请求
-> content 阶段进入 proxy 模块
-> 创建 upstream
-> 连接后端
-> 发送请求头和请求体
-> 接收响应头
-> 接收响应体
-> 响应经过 filter 返回客户端

重点文件:

1
2
src/http/ngx_http_upstream.c
src/http/modules/ngx_http_proxy_module.c

14.2 proxy 模块与 upstream 框架分工

ngx_http_proxy_module 负责 proxy 语义:解析 proxy_pass、生成发往后端的请求行、设置发往后端的请求头、处理 proxy 相关配置、指定 upstream 回调。

ngx_http_upstream.c 负责通用 upstream 流程:选择 peer、建立连接、管理超时、发送请求、接收响应头、转发响应体、失败重试、finalize upstream。

14.3 upstream 关键结构

结构 作用
ngx_http_upstream_t 单次请求的 upstream 状态
ngx_http_upstream_srv_conf_t upstream 配置块状态
ngx_http_upstream_peer_t peer 选择和连接相关回调
ngx_peer_connection_t 与后端连接相关的数据
ngx_http_upstream_state_t upstream 状态、耗时、响应码统计

理解 $upstream_* 变量时,ngx_http_upstream_state_t 很重要。

14.4 upstream 请求流程

典型流程:

1
2
3
4
5
6
7
8
9
10
ngx_http_proxy_handler
-> ngx_http_upstream_create
-> 设置 create_request/process_header/finalize_request 等回调
-> ngx_http_read_client_request_body
-> ngx_http_upstream_init
-> ngx_http_upstream_connect
-> ngx_http_upstream_send_request
-> ngx_http_upstream_process_header
-> ngx_http_upstream_process_body
-> ngx_http_upstream_finalize_request

proxy 模块提供的关键回调包括:create_request 构造发给后端的 HTTP 请求;reinit_request 在重试时重新初始化请求;process_header 解析后端响应头;abort_request 异常中止;finalize_request 收尾。

14.5 超时源码理解

常见 upstream 超时:

1
2
3
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
配置 含义
proxy_connect_timeout 建立后端连接的超时时间
proxy_send_timeout 向后端发送请求过程中的写超时
proxy_read_timeout 等待后端响应过程中的读超时,不是完整接口总耗时

proxy_read_timeout 通常表示两次读操作之间的最大等待时间,而不是“整个 upstream 响应必须在这个时间内完成”。

14.6 失败重试

1
2
3
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;

源码层面需要关注:哪些错误会触发 next upstream;请求体已经发送后是否还能安全重试;非幂等请求重试风险;upstream 状态如何记录到 $upstream_status;多个 upstream response time 为什么用逗号分隔。

14.7 upstream keepalive

1
2
3
4
5
6
7
8
9
10
11
upstream backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 64;
}

location /api/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}

upstream keepalive 是 Nginx 到后端的连接复用,不是客户端到 Nginx 的 keepalive。keepalive 连接按 worker 维护,worker 数量会放大后端潜在连接数。长时间空闲连接可能被后端关闭,Nginx 需要处理 stale connection。

15. filter 链

15.1 filter 的作用

响应返回客户端前,会经过一组 filter。filter 可以处理响应头,也可以处理响应体。

1
2
3
4
5
upstream/static/content handler
-> header filter chain
-> body filter chain
-> write filter
-> client
filter 作用
header filter 输出响应头
chunked filter HTTP/1.1 chunked 编码
gzip filter 响应体压缩
range filter 范围请求
not_modified filter 304 处理
headers filter 添加或修改响应头
write filter 最终写入连接

15.2 header filter

header filter 处理状态码、Content-TypeContent-LengthTransfer-EncodingLast-ModifiedETagCache-Control 和自定义响应头。

1
add_header X-Request-Id $request_id always;

headers 模块会通过 header filter 修改输出头。

15.3 body filter

body filter 处理响应体,通常以 ngx_chain_t 链表传递 buffer。典型场景包括 gzip 压缩响应体、chunked 编码输出、range 截取部分内容、sub_filter 替换响应体内容,以及 write filter 最终发送数据。

15.4 filter 链顺序

filter 模块初始化时会把自己插入 filter 链。常见写法:

1
2
3
4
5
6
7
8
static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;

ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_xxx_header_filter;

ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_xxx_body_filter;

理解这个机制后,就能解释为什么 filter 顺序会影响输出、为什么某些模块必须在特定 filter 前后执行、为什么 body filter 必须正确处理链表中的多个 buffer,以及为什么 filter 不应该假设响应体一次性完整到达。

16. 内存池、buffer 与 chain

16.1 ngx_pool_t

Nginx 内存池用于高效管理生命周期明确的内存。

pool 生命周期
cycle pool 当前 cycle 生命周期
connection pool 当前连接生命周期
request pool 当前请求生命周期
upstream pool 通常依附请求或 upstream 流程

内存池分配函数:

1
2
3
4
void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
函数 特点
ngx_palloc 按对齐方式分配
ngx_pnalloc 不强制按默认对齐分配
ngx_pcalloc 分配后清零
ngx_pool_cleanup_add 注册 pool 销毁时的清理回调

16.2 大块内存与 cleanup

小块内存通常从 pool 当前块中分配,大块内存会单独分配并挂到 large 链表。某些资源不是普通内存,例如打开的文件、临时文件、共享资源引用或特殊句柄,可以通过 cleanup 机制在 pool 销毁时释放。

16.3 ngx_buf_t

Nginx 使用 ngx_buf_t 表示一段数据。buffer 可以来自内存、文件、mmap、临时文件或 upstream 接收缓冲。

字段/标记 含义
pos / last 内存 buffer 中可用数据范围
file_pos / file_last 文件 buffer 中可用数据范围
temporary 可修改的临时内存
memory 只读内存
in_file 数据来自文件
flush 要求刷新输出
last_buf 整个响应最后一个 buffer
last_in_chain 当前 chain 最后一个 buffer

16.4 ngx_chain_t

ngx_chain_t 是 buffer 链表:

1
2
3
4
5
6
typedef struct ngx_chain_s ngx_chain_t;

struct ngx_chain_s {
ngx_buf_t *buf;
ngx_chain_t *next;
};

chain 的意义在于:响应体可能由多个片段组成;部分数据来自内存,部分来自文件;filter 可以逐段处理响应;非阻塞写可能一次写不完,需要保留未发送部分。学习 body filter 时必须理解 chain,否则很容易写出只处理第一个 buffer 的错误模块。

17. shared memory、slab 与 zone

17.1 为什么需要共享内存

Nginx worker 是多进程模型,普通内存不在 worker 之间共享。如果多个 worker 需要共享状态,就需要 shared memory。典型场景包括限流计数、连接数统计、proxy cache 元数据、upstream zone、SSL session cache。

17.2 shared memory zone

1
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

这里的 api_limit:10m 就是一个共享内存 zone。源码层面关注 zone 名称、zone 大小、init 回调、worker 如何访问共享内存,以及 reload 时新旧 cycle 如何处理同名 zone。

17.3 slab 分配器

共享内存里不能直接使用普通进程堆分配方式。Nginx 使用 slab 分配器管理 shared memory 中的小块内存。

1
src/core/ngx_slab.c

slab 适合在共享内存中分配小对象,由多 worker 访问,并配合锁保护共享结构。

17.4 limit_req 的源码主线

limit_req 是学习 shared memory 的好例子。

1
2
3
4
5
6
7
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=5r/s;

server {
location /api/ {
limit_req zone=req_zone burst=20 nodelay;
}
}

源码重点:

1
src/http/modules/ngx_http_limit_req_module.c

需要理解:limit_req_zone 在 http 级别定义共享状态;key 通常是客户端 IP 或业务标识;zone 中存储 key 对应的请求速率状态;红黑树和队列用于查找与淘汰;多 worker 访问共享 zone 时需要锁;burst 和 nodelay 影响排队或立即拒绝策略。

18. 变量系统

18.1 变量不是简单字符串替换

Nginx 变量例如 $uri$host$request_time$upstream_addr,背后通常是变量索引、取值回调和缓存标记。

变量可能来自:

  • 请求行解析结果。
  • 请求头。
  • 连接信息。
  • upstream 状态。
  • map/geo 等模块计算。
  • rewrite 脚本引擎。
  • 第三方模块自定义。

18.2 变量求值时机

变量不一定在请求一开始就全部计算。很多变量是用到时才求值。

例如:

  • $request_time 通常在日志阶段才有最终意义。
  • $upstream_response_time 只有访问 upstream 后才有意义。
  • $sent_http_* 在响应头发送阶段后更有意义。
  • map 变量可能在被引用时才执行匹配。

这解释了为什么某些变量在不同阶段值不同,或者在没有 upstream 时为空。

18.3 map 模块源码理解

map 是理解变量系统的好入口。

1
2
3
4
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

源码学习重点:map 如何注册变量;map 如何在配置阶段构建匹配结构;运行时如何根据源变量计算目标变量;正则 map 和普通 hash map 的差异。

重点文件:

1
2
src/http/modules/ngx_http_map_module.c
src/http/ngx_http_variables.c

19. 日志系统源码理解

19.1 error log

error log 是 Nginx 内部统一日志系统,常见级别包括:

1
debug, info, notice, warn, error, crit, alert, emerg

源码里常见:

1
2
ngx_log_error(NGX_LOG_ERR, log, err, "message");
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, "debug message");

debug 日志需要编译时带 --with-debug,配置 error_log ... debug,并可选使用 debug_connection 限定客户端。

19.2 access log

access log 属于 HTTP log 模块,发生在 log 阶段。

1
2
3
4
5
log_format main '$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent $request_time '
'$upstream_addr $upstream_status $upstream_response_time';

access_log logs/access.log main;

源码重点:

1
src/http/modules/ngx_http_log_module.c

关注点:log_format 如何在配置阶段编译成操作数组;请求结束后如何按变量生成日志行;buffer 日志与 gzip 日志如何工作;open_log_file_cache 如何缓存日志文件句柄。

19.3 为什么源码学习要重视日志

源码阅读不只是看函数调用,还要学会把日志和源码对应起来。

1
2
3
4
5
6
开启 debug log
-> 发起一个最小请求
-> 找到关键日志行
-> 用日志内容搜索源码
-> 定位对应函数
-> 反推请求流转路径

这比单纯从 main 开始硬读效率高很多。

20. reload、退出与热升级

20.1 常见信号

Nginx 使用信号控制进程行为:

信号/命令 作用
nginx -s reload / HUP 重新加载配置
nginx -s quit / QUIT 优雅退出
nginx -s stop / TERM/INT 快速退出
nginx -s reopen / USR1 重新打开日志文件
USR2 二进制热升级
WINCH 优雅关闭 worker

20.2 优雅退出

优雅退出的核心是:

1
2
3
4
5
6
worker 收到退出指令
-> 关闭监听 socket
-> 不再 accept 新连接
-> 继续处理已有连接
-> 已有请求处理完成后释放资源
-> worker 退出

这也是为什么长连接可能拖慢 reload 后旧 worker 的退出。

20.3 日志 reopen

日志切割常见流程:

1
2
mv access.log access.log.1
nginx -s reopen

reopen 后,master 会通知 worker 重新打开日志文件。否则即使文件名被移动,worker 仍可能写入旧文件句柄。

20.4 二进制热升级

热升级大致流程:

1
2
3
4
5
6
7
替换 nginx 二进制文件
-> 向旧 master 发送 USR2
-> 旧 master 启动新 binary 的 master/worker
-> 新旧 master/worker 短暂共存
-> 验证新版本正常
-> 通知旧 worker 优雅退出
-> 必要时回滚旧 binary

热升级风险高于普通 reload,生产中必须有 SOP、备份和回滚验证。需要特别注意新旧二进制编译参数兼容、动态模块 ABI 兼容、共享内存 zone 兼容性、长连接和监听 fd 继承,以及失败后能否明确回滚到旧 master。

21. 最小 HTTP 模块开发

21.1 模块目录结构

建议创建目录:

1
2
3
ngx_http_hello_module/
├── config
└── ngx_http_hello_module.c

config 文件示例:

1
2
3
ngx_addon_name=ngx_http_hello_module
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_module.c"

21.2 最小 content handler 模块

下面示例实现一个 hello_nginx 指令,在 location 中返回固定文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

static char *ngx_http_hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r);

static ngx_command_t ngx_http_hello_commands[] = {
{
ngx_string("hello_nginx"),
NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
ngx_http_hello,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};

static ngx_http_module_t ngx_http_hello_module_ctx = {
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
};

ngx_module_t ngx_http_hello_module = {
NGX_MODULE_V1,
&ngx_http_hello_module_ctx,
ngx_http_hello_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};

static char *
ngx_http_hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;

clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
clcf->handler = ngx_http_hello_handler;

return NGX_CONF_OK;
}

static ngx_int_t
ngx_http_hello_handler(ngx_http_request_t *r)
{
ngx_buf_t *b;
ngx_chain_t out;
ngx_str_t type = ngx_string("text/plain");
ngx_str_t response = ngx_string("hello nginx module\n");

if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
return NGX_HTTP_NOT_ALLOWED;
}

r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = response.len;
r->headers_out.content_type = type;

if (r->method == NGX_HTTP_HEAD) {
return ngx_http_send_header(r);
}

b = ngx_create_temp_buf(r->pool, response.len);
if (b == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

ngx_memcpy(b->pos, response.data, response.len);
b->last = b->pos + response.len;
b->last_buf = 1;

out.buf = b;
out.next = NULL;

ngx_http_send_header(r);
return ngx_http_output_filter(r, &out);
}

21.3 编译模块

静态编译:

1
2
3
./configure --prefix=/opt/nginx-hello --with-debug --add-module=/path/to/ngx_http_hello_module
make -j$(nproc)
sudo make install

配置:

1
2
3
4
5
6
7
server {
listen 8080;

location /hello {
hello_nginx;
}
}

验证:

1
curl -i http://127.0.0.1:8080/hello

预期:

1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 19

hello nginx module

21.4 content handler 模块要点

必须理解:指令通过 ngx_command_t 注册;指令解析函数把 clcf->handler 设置为自己的 handler;handler 中要检查请求方法;HEAD 请求只发送响应头;响应体要通过 ngx_buf_tngx_chain_t 输出;内存从 r->pool 分配,请求结束后自动释放;不要在请求 handler 中执行阻塞操作。

22. 最小 header filter 模块开发

22.1 目标

实现一个 header filter,为响应添加:

1
X-Learn-Nginx: phase-7

22.2 核心代码骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

static ngx_int_t ngx_http_learn_header_filter(ngx_http_request_t *r);
static ngx_int_t ngx_http_learn_filter_init(ngx_conf_t *cf);

static ngx_http_output_header_filter_pt ngx_http_next_header_filter;

static ngx_http_module_t ngx_http_learn_filter_module_ctx = {
NULL,
ngx_http_learn_filter_init,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
};

ngx_module_t ngx_http_learn_filter_module = {
NGX_MODULE_V1,
&ngx_http_learn_filter_module_ctx,
NULL,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};

static ngx_int_t
ngx_http_learn_filter_init(ngx_conf_t *cf)
{
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_learn_header_filter;

return NGX_OK;
}

static ngx_int_t
ngx_http_learn_header_filter(ngx_http_request_t *r)
{
ngx_table_elt_t *h;

h = ngx_list_push(&r->headers_out.headers);
if (h == NULL) {
return NGX_ERROR;
}

h->hash = 1;
ngx_str_set(&h->key, "X-Learn-Nginx");
ngx_str_set(&h->value, "phase-7");

return ngx_http_next_header_filter(r);
}

22.3 filter 模块要点

  • filter 模块通常在 postconfiguration 阶段挂入 filter 链。
  • 需要保存原来的 top filter,然后把自己设置为新的 top filter。
  • 执行完成后通常调用 next filter。
  • header filter 不应该随意修改 body。
  • body filter 必须处理 chain 中每个 buffer。
  • filter 模块要特别注意顺序和兼容性。

23. 调试方法

23.1 用 debug log 追踪请求

1
2
3
4
5
error_log logs/error.log debug;

events {
debug_connection 127.0.0.1;
}

发起请求:

1
curl -H 'Host: test.local' http://127.0.0.1:8080/api/users

建议在日志中搜索:accepthttp wait request handlerhttp process request linehttp process request header linehttp init upstreamconnect tohttp upstream requesthttp output filterhttp log handler

23.2 用 gdb 调试 worker

1
2
ps -ef | grep nginx
sudo gdb -p <worker_pid>

常用 gdb 命令:

1
2
3
4
5
6
7
8
bt
info threads
break ngx_http_process_request
break ngx_http_proxy_handler
break ngx_http_upstream_connect
continue
print r->uri
print r->headers_in.host->value

注意:Nginx 是多进程,断点要打在处理请求的 worker 上;如果 worker 多个,建议实验时设置 worker_processes 1;;debug 编译更容易看到符号;gdb 会暂停 worker,不能直接在生产高峰使用。

23.3 用 strace 看系统调用

1
sudo strace -f -p <worker_pid> -e trace=network,file,epoll_wait,read,write

可观察 epoll_waitaccept4recvfromsendfilewritevconnectopenatclose。strace 适合验证 Nginx 是否卡在系统调用层,但会影响性能,生产慎用。

23.4 用 perf 看热点

1
2
3
sudo perf top -p <worker_pid>
sudo perf record -F 99 -p <worker_pid> -g -- sleep 30
sudo perf report

适合分析 CPU 是否耗在 TLS、gzip、正则匹配、日志格式化或异常第三方模块热点。

23.5 用源码搜索定位

1
2
3
4
5
rg "proxy_pass" src/http
rg "NGX_HTTP_ACCESS_PHASE" src/http
rg "ngx_http_top_header_filter" src/http
rg "upstream_response_time" src/http
rg "client intended to send too large body" src

技巧:用配置指令名搜索 ngx_command_t;用 error log 中的英文报错搜索源码;用变量名搜索变量注册表;用函数名前缀判断模块归属;用 objs/ngx_modules.c 查看实际编译进来的模块顺序。

24. 核心源码阅读路线

24.1 第一轮:建立地图

目标:知道目录和主链路,不追求细节。

阅读顺序:

1
2
3
4
5
6
7
src/core/nginx.c
src/core/ngx_cycle.c
src/os/unix/ngx_process_cycle.c
src/event/ngx_event.c
src/event/modules/ngx_epoll_module.c
src/http/ngx_http_request.c
src/http/ngx_http_core_module.c

输出要求:

  • 画出启动流程图。
  • 画出 master/worker 职责图。
  • 画出请求生命周期图。

24.2 第二轮:配置和 phase

目标:理解配置如何变成 handler。

阅读顺序:

1
2
3
4
5
6
src/core/ngx_conf_file.c
src/http/ngx_http.c
src/http/ngx_http_core_module.c
src/http/modules/ngx_http_rewrite_module.c
src/http/modules/ngx_http_access_module.c
src/http/modules/ngx_http_static_module.c

输出要求:

  • 能说明一个指令如何被解析。
  • 能说明 server/location 如何合并。
  • 能说明 phase handler 如何注册和执行。

24.3 第三轮:upstream

目标:读懂 proxy_pass 主链路。

阅读顺序:

1
2
3
4
src/http/modules/ngx_http_proxy_module.c
src/http/ngx_http_upstream.c
src/http/ngx_http_upstream_round_robin.c
src/event/ngx_event_connect.c

输出要求:

  • 画出 proxy 请求转发流程。
  • 能解释 connect/send/read timeout。
  • 能解释 upstream retry。
  • 能解释 $upstream_status$upstream_response_time

24.4 第四轮:filter

目标:理解响应如何输出。

阅读顺序:

1
2
3
4
5
src/http/ngx_http_header_filter_module.c
src/http/ngx_http_write_filter_module.c
src/http/modules/ngx_http_chunked_filter_module.c
src/http/modules/ngx_http_gzip_filter_module.c
src/http/modules/ngx_http_headers_filter_module.c

输出要求:

  • 能说明 header filter 和 body filter 的区别。
  • 能说明 filter 链如何串起来。
  • 能说明 ngx_buf_tngx_chain_t 在输出中的作用。

24.5 第五轮:内存和共享状态

目标:理解高并发下的资源管理。

阅读顺序:

1
2
3
4
5
6
7
8
9
src/core/ngx_palloc.c
src/core/ngx_buf.c
src/core/ngx_array.c
src/core/ngx_list.c
src/core/ngx_hash.c
src/core/ngx_slab.c
src/event/ngx_event_timer.c
src/http/modules/ngx_http_limit_req_module.c
src/http/ngx_http_file_cache.c

输出要求:

  • 能说明 pool 的生命周期。
  • 能说明 buffer/chain 的数据传递方式。
  • 能说明 shared memory zone 的用途。
  • 能说明 limit_req 或 proxy_cache 为什么需要共享内存。

25. 关键结构体速查

结构体 所在方向 作用
ngx_cycle_t core 当前运行周期的全局上下文
ngx_conf_t core 配置解析上下文
ngx_module_t core 模块定义
ngx_command_t core 配置指令定义
ngx_pool_t core 内存池
ngx_array_t core 动态数组
ngx_list_t core 链式数组,常用于 header
ngx_hash_t core hash 表
ngx_rbtree_t core 红黑树,timer、限流等使用
ngx_connection_t event TCP 连接对象
ngx_event_t event 读写事件对象
ngx_listening_t event/core 监听端口对象
ngx_http_request_t http 单个 HTTP 请求对象
ngx_http_connection_t http HTTP 连接上下文
ngx_http_core_main_conf_t http HTTP main 配置
ngx_http_core_srv_conf_t http server 级核心配置
ngx_http_core_loc_conf_t http location 级核心配置
ngx_http_upstream_t upstream 单次 upstream 请求上下文
ngx_http_upstream_srv_conf_t upstream upstream server 配置
ngx_buf_t output 一段内存或文件数据
ngx_chain_t output buffer 链表
ngx_table_elt_t http HTTP header 项

建议学习时自己维护一份结构体笔记,记录字段含义、生命周期和典型使用位置。

26. 从配置反查源码的方法

26.1 通过指令名找模块

例如要研究:

1
proxy_read_timeout 30s;

步骤:

1
rg "proxy_read_timeout" src

通常会找到 ngx_command_t 定义,然后继续看:指令属于哪个模块、允许出现在哪些上下文、写入哪个配置结构字段、字段在哪里被使用。

26.2 通过日志反查源码

例如 error log 出现:

1
upstream timed out (110: Connection timed out) while reading response header from upstream

可以搜索:

1
2
rg "upstream timed out" src
rg "while reading response header from upstream" src

然后定位到 upstream 读取响应头的超时处理路径。

26.3 通过变量名反查源码

例如要研究:

1
$upstream_response_time

搜索:

1
rg "upstream_response_time" src/http

关注变量在哪里注册、变量取值函数是什么、数据来源结构体是什么、多 upstream 重试时如何拼接。

26.4 通过模块顺序反查 filter

查看编译模块:

1
cat objs/ngx_modules.c

关注哪些模块被编译、filter 模块初始化顺序、第三方模块插入位置、动态模块加载顺序。

27. 与前六阶段知识的对应关系

前面阶段知识 源码对应主线
nginx -t 配置解析、模块指令、cycle 初始化
server_name HTTP server 配置、虚拟主机查找
location location 树、正则 location、phase find config
rewrite rewrite phase、script engine、内部跳转
try_files precontent 阶段、内部跳转
proxy_pass proxy module、upstream framework
负载均衡 upstream peer 选择、round robin
proxy_next_upstream upstream 失败重试
proxy_read_timeout upstream read event timer
HTTPS SSL 模块、OpenSSL 集成、握手事件
限流 preaccess phase、shared memory、rbtree
gzip body filter、buffer chain
add_header header filter
静态资源 static content handler、sendfile
open_file_cache 文件缓存、打开文件句柄缓存
access log log phase、变量系统
reload master 信号、cycle 重建、worker 优雅退出
高可用 进程模型、监听 fd、平滑切换基础

学习源码时要不断回到配置和生产问题,这样不会变成孤立的源码阅读。

28. 必做实验

28.1 实验一:debug 编译并启动

目标:获得可调试的 Nginx。

1
2
3
4
./configure --prefix=/opt/nginx-debug --with-debug --with-http_ssl_module --with-http_stub_status_module
make -j$(nproc)
sudo make install
/opt/nginx-debug/sbin/nginx -V

过关标准:能看到 --with-debug,能启动 Nginx,能通过 curl 访问默认页面。

28.2 实验二:跟踪启动流程

目标:理解 master/worker 如何启动。

1
2
3
ps -ef | grep nginx
pstree -p | grep nginx
sudo strace -f -o /tmp/nginx-start.log /opt/nginx-debug/sbin/nginx -c /opt/nginx-debug/conf/nginx.conf

观察 master PID、worker PID、listen socket 创建、fork worker、worker 进入 epoll_wait

28.3 实验三:跟踪一次 HTTP 请求

配置:

1
2
3
4
5
6
7
worker_processes 1;
error_log logs/error.log debug;

events {
worker_connections 1024;
debug_connection 127.0.0.1;
}

请求:

1
curl -i http://127.0.0.1:8080/

任务:找到 accept 日志、request line 解析日志、header 解析日志、content handler 相关日志、output filter 相关日志、request finalize 和 log 阶段日志。

28.4 实验四:跟踪 location 匹配

配置多个 location:

1
2
3
4
5
location = /exact { return 200 exact; }
location ^~ /static/ { return 200 static; }
location /static/test { return 200 prefix; }
location ~ \.php$ { return 200 php; }
location / { return 200 root; }

任务:请求不同 URI,对照 debug log 判断命中的 location,搜索 ngx_http_core_find_location,理解最长前缀和正则匹配的实际顺序。

28.5 实验五:跟踪 proxy_pass

配置:

1
2
3
4
5
6
7
8
9
10
upstream backend {
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}

location /api/ {
proxy_pass http://backend;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
}

任务:启动两个简单后端,请求 /api/,观察 upstream 连接日志,停掉一个后端观察失败重试,对照 $upstream_addr$upstream_status$upstream_response_time,阅读 ngx_http_proxy_handlerngx_http_upstream_init 的调用链。

28.6 实验六:观察 filter 链

任务:开启 gzip,添加 add_header,请求静态文本文件,对比开启/关闭 gzip 的响应头和 body,阅读 gzip filter 和 headers filter 初始化位置,理解 filter 插入顺序。

28.7 实验七:分析内存池

任务:在 gdb 中断到 ngx_http_create_request,打印 r->pool,在 content handler 中观察从 r->pool 分配 buffer,请求结束后观察 request pool 释放路径。

过关标准:能说明 request pool 生命周期,并能说明为什么模块不要把 request pool 中的指针保存到更长生命周期对象中。

28.8 实验八:观察 limit_req shared memory

配置:

1
2
3
4
5
6
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=1r/s;

location /api/ {
limit_req zone=req_zone burst=2 nodelay;
return 200 ok;
}

任务:连续快速请求触发限流,阅读 ngx_http_limit_req_module.c,找到 shared memory zone 初始化、key 查找、节点插入、过期淘汰逻辑。

28.9 实验九:编写 hello 模块

目标:实现最小 content handler 模块。任务包括创建模块目录,编写 config.c 文件,静态编译进 Nginx,配置 hello_nginx;,使用 curl -i 验证响应。

28.10 实验十:编写 header filter 模块

目标:理解 filter 链。任务包括编写最小 header filter,添加 X-Learn-Nginx 响应头,编译并启动,验证响应头存在,与 add_header 模块对比实现方式。

29. 常见源码阅读误区

29.1 一开始逐行读

Nginx 源码包含大量平台兼容、宏、错误处理和性能优化细节。逐行读容易迷失。正确方式是:

1
2
3
4
先读主线
-> 再读关键结构
-> 再读典型模块
-> 最后补细节

29.2 只读文章不跑实验

源码理解必须和运行现象对应。至少要配合 debug log、curl 请求、gdb 断点、strace 系统调用和 access log 变量,否则很容易只记住结论,不知道如何验证。

29.3 忽略配置合并

很多配置问题不是指令本身,而是继承和合并导致最终值不同。阅读模块时一定要看 create conf、merge conf、指令 set 函数,以及 handler 使用的最终 conf。

29.4 把 worker 当线程理解

worker 是进程,不是线程。不同 worker 的普通内存不共享。共享状态必须通过 shared memory、文件、socket 或外部系统实现。

29.5 在 handler 中写阻塞逻辑

自定义模块中不能随便做阻塞 DNS、阻塞 HTTP 调用、慢磁盘 IO 或长时间 CPU 计算。一个 worker 被阻塞,会影响该 worker 上所有连接。

29.6 错误处理不完整

Nginx 模块开发必须处理内存分配失败、非法配置、不支持的请求方法、请求提前关闭、upstream 失败、filter 链返回错误、reload 和退出场景。

29.7 错误使用 request pool 指针

r->pool 分配的内存只在请求生命周期内有效。不能把它保存到共享内存、全局变量或长生命周期结构中。

29.8 忽略 filter 的链式和异步特性

body filter 不能假设一次收到完整响应体,也不能只处理第一个 buffer。必须正确遍历 chain,并处理 last_bufflushsync 等标记。

30. 源码学习检查清单

30.1 启动与进程

  • 能找到 main 函数。
  • 能说明 ngx_init_cycle 做了什么。
  • 能说明 master 如何 fork worker。
  • 能说明 worker 如何进入事件循环。
  • 能说明 reload 的新旧 cycle 切换逻辑。

30.2 配置与模块

  • 能解释 ngx_command_t
  • 能解释 ngx_module_t
  • 能说明指令上下文校验。
  • 能说明 main/server/location 配置创建与合并。
  • 能说明静态模块和动态模块区别。

30.3 事件与连接

  • 能说明 ngx_connection_tngx_event_t 的关系。
  • 能说明 epoll 如何驱动 read/write handler。
  • 能说明 accept 新连接流程。
  • 能说明定时器如何管理超时。
  • 能说明非阻塞 IO 下 NGX_AGAIN 的意义。

30.4 HTTP 请求

  • 能说明请求行和请求头解析流程。
  • 能说明 request pool 生命周期。
  • 能说明 keepalive 请求如何复用连接。
  • 能说明 request finalize 的作用。
  • 能说明 access log 为什么在 log 阶段写入。

30.5 phase 与 location

  • 能列出主要 phase。
  • 能说明 rewrite/access/content/log 的执行位置。
  • 能说明 location 查找过程。
  • 能说明内部跳转为什么会重新匹配 location。
  • 能说明 try_files 与 rewrite 的阶段差异。

30.6 upstream 与 filter

  • 能说明 proxy 模块如何接入 upstream。
  • 能说明 upstream connect/send/read 流程。
  • 能说明 upstream retry 的触发条件。
  • 能说明 header filter 和 body filter 的区别。
  • 能说明 ngx_buf_tngx_chain_t 的作用。

30.7 内存与共享状态

  • 能说明 pool 小块和大块分配。
  • 能说明 cleanup 回调用途。
  • 能说明 shared memory zone 的用途。
  • 能说明 slab 为什么用于共享内存。
  • 能说明 limit_req 或 proxy_cache 的共享状态设计。

30.8 模块开发

  • 能写最小 content handler。
  • 能写最小 header filter。
  • 能正确声明配置指令。
  • 能正确处理 HEAD 请求。
  • 能避免阻塞 worker。
  • 能用 debug log 和 gdb 调试模块。

31. 推荐学习节奏

建议本阶段拆成 6 周完成:

周次 主题 产出
第 1 周 源码目录、编译、debug、启动流程 源码地图、debug 环境、启动流程图
第 2 周 配置解析、模块体系、server/location 合并 指令解析笔记、配置合并流程图
第 3 周 event、connection、timer、HTTP 请求解析 请求从 accept 到 phase 的链路图
第 4 周 phase handler、rewrite、access、content、log phase 表格、location 匹配实验记录
第 5 周 upstream、proxy、filter、buffer、chain proxy_pass 源码链路图、filter 实验记录
第 6 周 pool、shared memory、limit_req、模块开发 hello 模块、header filter 模块、总结文档

如果时间有限,至少完成下面最小闭环:

1
2
3
4
5
6
7
debug 编译
-> 跟踪一次普通静态请求
-> 跟踪一次 proxy_pass 请求
-> 阅读 phase handler
-> 阅读 upstream 主链路
-> 阅读 filter 链
-> 编写 hello 模块

32. 面试与生产问题思考

32.1 为什么 Nginx 高并发能力强

推荐回答主线:

1
2
3
4
5
6
7
master/worker 多进程
+ 每个 worker 单线程事件循环
+ 非阻塞 IO
+ epoll/kqueue 等事件通知
+ 内存池减少分配释放成本
+ sendfile/writev 等高效输出
+ 模块化 filter/upstream pipeline

不要只回答“因为用了 epoll”。epoll 只是其中一部分。

32.2 reload 为什么通常不中断请求

推荐回答主线:

1
2
3
4
5
6
master 收到 HUP
-> 解析新配置并创建新 cycle
-> 成功后启动新 worker
-> 旧 worker 关闭监听 socket
-> 旧 worker 不再接新连接
-> 旧 worker 处理完已有请求后退出

同时补充边界:长连接、慢请求、WebSocket、模块兼容、共享内存变化都可能影响 reload 表现。

32.3 502 和 504 在源码链路上怎么理解

  • 502 更偏 upstream 返回异常、连接失败、响应头非法、后端提前关闭等。
  • 504 更偏 upstream 超时,例如连接超时、发送超时、读取响应超时。
  • 源码关注 ngx_http_upstream.c 中连接、读写事件、超时处理和 finalize 逻辑。

32.4 proxy_read_timeout 是总耗时吗

不是简单的接口总耗时。它更接近两次读取 upstream 响应之间允许等待的最大时间。只要后端持续有数据返回,整体响应时间可能超过该值。

32.5 为什么自定义模块不能随便阻塞

因为 worker 通常是单线程事件循环。如果模块 handler 中执行阻塞调用,该 worker 上的其他连接都会受影响。高并发场景下,这会导致延迟抖动甚至大面积超时。

32.6 为什么多 worker 需要 shared memory

worker 是多进程,普通内存互不共享。限流计数、连接统计、cache 元数据等跨 worker 状态必须放到 shared memory 或外部系统中。

33. 最终过关标准

完成第七阶段后,应达到下面标准:

  • 能画出 Nginx 启动、配置解析、worker 事件循环的主流程。
  • 能解释 master/worker、cycle、module、connection、request 的关系。
  • 能从源码角度说明 Nginx 高并发能力来源。
  • 能说明 HTTP 请求从 accept 到 access log 的完整内部路径。
  • 能解释 server/location 匹配、rewrite、access、content、log 的阶段关系。
  • 能解释 proxy_pass 如何进入 upstream,并说明 connect/send/read/timeout/retry 流程。
  • 能解释 header filter、body filter、write filter 的协作方式。
  • 能说明内存池、buffer、chain、shared memory、slab 的用途和边界。
  • 能使用 debug log、gdb、strace、perf 辅助定位源码路径。
  • 能通过指令名、日志文本、变量名反查对应源码。
  • 能读懂常见 HTTP 模块的基本结构。
  • 能编写并编译一个最小 content handler 模块。
  • 能编写一个最小 header filter 或 log 阶段模块。
  • 能把源码理解反哺生产排障、性能优化和模块选型。

34. 本阶段总结

第七阶段是 Nginx 学习路线中最深入的一层。前六个阶段解决“如何正确使用 Nginx”,第七阶段解决“为什么 Nginx 会这样运行,以及复杂问题该从哪里定位”。

学习本阶段时要始终抓住几条主线:

1
2
3
4
5
6
7
8
启动主线:main -> cycle -> master -> worker
配置主线:directive -> command -> conf -> merge -> handler
事件主线:connection -> event -> epoll -> callback -> timer
请求主线:request line -> headers -> phase -> content -> log
代理主线:proxy module -> upstream -> peer -> backend -> retry
输出主线:header filter -> body filter -> write filter -> client
内存主线:pool -> buf -> chain -> slab -> shared memory
模块主线:module -> commands -> ctx -> init -> handler/filter

真正掌握第七阶段后,看到一个 Nginx 配置项,不仅知道怎么写,还能大致知道它属于哪个模块、在哪个阶段执行、使用哪些核心结构、可能带来哪些性能和稳定性影响。这就是从熟练使用者走向专家级理解的关键一步。