1. 背景:C10K 之痛与 Nginx 的诞生
1.1 为什么需要 Nginx
1999~2004 年,Web 从「每天几万次访问」进入「每秒上万并发连接」时代。Apache 为代表的进程/线程每连接模型遇到两个硬瓶颈:
| 模型 | 机制 | 瓶颈 |
|---|---|---|
| Apache prefork | 一个进程处理一个连接 | 进程上下文切换开销巨大,内存占用随连接数线性膨胀(C10K 时内存直接耗尽) |
| Apache worker | 一个线程处理一个连接 | 线程栈 + 锁竞争,高并发下依然力不从心 |
| Nginx | 事件驱动 + 多 worker 进程 | 单 worker 用 epoll 非阻塞复用海量连接,内存占用与连接数解耦 |
C10K(10000 并发连接)问题的本质:阻塞式 I/O 下,一个连接至少需要一个线程/进程 ,而线程/进程都是有成本的。Nginx 的答案是:用事件驱动 + 非阻塞 I/O 让一个进程同时服务数十万连接。
1.2 设计哲学:为什么 Nginx 快
Nginx 由俄罗斯工程师 Igor Sysoev 于 2002 年开始编写,2004 年 10 月发布 0.1.0。其核心设计承诺:
- 事件驱动非阻塞:worker 进程内用 epoll(Linux)/ kqueue(BSD/macOS)监听海量 fd,连接空闲时不占任何执行资源;
- 进程模型而非线程模型 :master 管理 + N 个 worker 各自独立事件循环,无锁或少锁,天然利用多核,一个 worker 崩溃不影响其他 worker;
- 模块化架构:核心极小,HTTP、Stream、Mail 等能力全部以模块扩展,官方 + 第三方模块生态(如 ngx_http_lua_module、nginx-rtmp-module);
- 内存池与零拷贝:ngx_pool_t 内存池一次分配、随请求释放;sendfile 让静态文件直接从内核页缓存发到网卡,不经用户态。
2. 核心概念速览
2.1 进程模型:master / worker
master(root 权限,不处理业务)
/ | \ (fork 出 N 个 worker)
worker0 worker1 worker2(普通用户权限,各自独立事件循环)
- master:负责读取/校验配置、fork worker、监听信号(HUP 重载 / USR1 重开日志 / USR2 平滑升级 / QUIT 优雅退出)、平滑升级时拉起新二进制;
- worker:真正处理连接。每个 worker 独立 epoll 事件循环,互不共享连接状态(少量共享内存如限流计数、SSL 会话缓存除外);
- 为什么不用多线程:多线程共享地址空间 → 锁竞争、cache line 伪共享、单线程崩溃拖垮全部;多进程隔离性好,worker 数 = CPU 核数时性能最佳。
2.2 事件驱动:epoll 到底快在哪
传统 select/poll 每次调用都要把 fd 集合从用户态拷贝到内核态、线性扫描全部 fd;epoll 只注册一次,内核维护红黑树 + 就绪链表,epoll_wait 只返回有事件发生的 fd,O(1) 复杂度。Nginx 对每个 worker 只有少数几个 epoll_wait 调用点(ngx_process_events_and_timers),连接读写全部回调驱动:
连接到达 → epoll 就绪 → accept → 挂读事件 → 请求头读完 → 解析 → 挂写事件 → 响应写完 → 关闭/复用连接
每个连接的生命周期内,CPU 只在「数据真的来了」时才介入,空闲连接零成本。
2.3 模块化体系
Nginx 的「模块」与 CMake 的「target」类似,是架构的基本单位:
- core 模块:进程管理、配置解析、事件循环框架(ngx_core_module);
- event 模块:epoll/kqueue/select 等事件后端;
- http 模块:HTTP 核心(请求解析、location 路由)+ 扩展(ngx_http_proxy_module 反向代理、ngx_http_upstream_module 负载均衡、ngx_http_ssl_module、ngx_http_gzip_module、ngx_http_limit_req_module 限流、ngx_http_cache 缓存等);
- stream 模块:四层(TCP/UDP)代理、负载均衡;
- 编译时可裁剪:./configure --without-http_rewrite_module --with-stream 等,按需构建。
3. API 说明:配置指令体系
Nginx 的「API」是声明式配置指令,遵循严格的上下文(context)嵌套规则。理解上下文是掌握 Nginx 的第一步。
3.1 配置上下文层级
main(全局)
├── events { ... } # 事件模型、worker_connections
├── http { ... } # HTTP 全局:mime、gzip、upstream、日志
│ ├── upstream xxx { ... }# 后端服务器组(仅 http/stream 内)
│ ├── server { ... } # 虚拟主机:listen、server_name、ssl
│ │ └── location / { ... } # URI 路由:root、proxy_pass、limit_req
│ └── include mime.types;
└── stream { ... } # 四层代理(TCP/UDP)
规则:指令只能出现在允许的上下文,写错位置 nginx -t 直接报错。例如 upstream 不能写在 server 里,location 不能出现在 http 层。
3.2 高频指令速查表
| 指令 | 上下文 | 作用 | 典型值/说明 |
|---|---|---|---|
| worker_processes | main | worker 进程数 | auto(=CPU 核数) |
| worker_connections | events | 每 worker 最大连接数 | 1024 默认,生产 10240+ |
| worker_rlimit_nofile | main | 每进程 fd 上限 | 需同步调 ulimit -n |
| listen | server | 监听地址端口 | listen 80; listen 443 ssl http2; |
| server_name | server | 虚拟主机名 | server_name api.example.com; |
| root / alias | server/location | 静态文件根目录 | root html; vs alias /data/files/; |
| index | location | 默认首页 | index index.html; |
| location | server | URI 路由 | location /api/ { proxy_pass ...; } |
| proxy_pass | location | 反向代理目标 | proxy_pass http://backend; |
| upstream | http/stream | 后端组定义 | upstream backend { server 127.0.0.1:8080; } |
| limit_req_zone + limit_req | http / location | 漏桶限流 | limit_req zone=api:10m rate=10r/s; |
| limit_conn_zone + limit_conn | http / location | 并发连接数限制 | limit_conn conn 20; |
| gzip | http/server/location | 压缩 | gzip on; gzip_types ...; |
| sendfile | http | 内核零拷贝 | sendfile on; |
| ssl_certificate | server | TLS 证书 | ssl_certificate /etc/nginx/cert.pem; |
| access_log / error_log | 全局 | 日志 | access_log logs/access.log main; |
| include | 任意 | 拆分配置文件 | include conf.d/*.conf; |
3.3 location 匹配优先级(必考知识点)
1. = 精确匹配 location = /exact { ... }
2. ^~ 前缀匹配且不再查正则 location ^~ /static/ { ... }
3. ~ / ~* 正则匹配(按书写顺序,第一个命中生效) location ~ \.php$ { ... }
4. 普通前缀匹配(最长匹配优先)
5. 全部不中 → /
结论 :正则匹配优先级高于普通前缀匹配,只有 ^~ 能阻止正则。这是最容易踩坑的地方------location /api/ 与 location ~ /api 同时存在时,正则先赢。
3.4 变量系统
Nginx 内置大量变量:host、uri、args、request_method、remote_addr、http_user_agent、upstream_addr、upstream_response_time 等,可用于日志格式、if 判断、map 映射。第三方语言绑定(如 OpenResty Lua)也基于变量系统取请求上下文。
4. 详细使用说明
4.1 最小可运行:静态文件服务器
# /etc/nginx/nginx.conf(核心片段)
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
server {
listen 80;
server_name localhost;
root /var/www/html; # 静态站点根
location / {
index index.html index.htm;
}
location /assets/ {
expires 7d; # 静态资源浏览器缓存 7 天
add_header Cache-Control "public, max-age=604800";
}
}
}
验证:nginx -t(测试配置)→ nginx -s reload(平滑重载)→ curl http://localhost/。
4.2 反向代理 + 负载均衡(工业数采网关部署核心)
场景:Go Gin 采集网关(:8080)+ Python FastAPI 分析服务(:8081)统一走 api.example.com,Nginx 做入口与负载均衡。
http {
upstream gateway {
# 默认加权轮询;权重可做灰度:server 192.168.1.11:8080 weight=9; 新版本 weight=1
server 192.168.1.11:8080 weight=9 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 weight=9;
server 192.168.1.13:8080 weight=1 backup; # backup:仅当主全挂时启用
keepalive 32; # 到后端的空闲连接池,避免每次新建 TCP
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/api.crt;
ssl_certificate_key /etc/nginx/ssl/api.key;
location /api/ {
proxy_pass http://gateway; # 不带 URI:原样透传完整请求 URI
proxy_http_version 1.1; # 必须,配合 keepalive
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时三件套:后端慢/挂时不至于拖死客户端
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
location /ws/ { # WebSocket 代理
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
负载均衡策略速查:
| 策略 | 指令 | 适用 |
|---|---|---|
| 轮询 | 默认 | 后端同质 |
| 权重 | weight=N | 机器性能不均 / 灰度发布 |
| IP 哈希 | ip_hash; | 需要会话保持(sticky) |
| 最少连接 | least_conn; | 长连接任务不均匀 |
| 通用哈希 | hash $request_uri; | 按 URI 一致性路由 |
4.3 限流与并发限制(保护后端的最后防线)
http {
# 限流:按客户端 IP 分桶,rate=10r/s,10m 共享内存
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# 连接数限制:每 IP 最多 20 个并发连接
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay; # burst=20:瞬时放行 20 个排队,nodelay 不延时
limit_conn conn 20;
proxy_pass http://gateway;
}
}
}
语义:rate=10r/s 是长期平均速率;burst 是突发令牌;nodelay 表示突发请求立即处理而非排队延时(不设 nodelay 时超出平均速率的请求会按延时释放)。返回 503 表示被限流。
4.4 HTTPS + HTTP/2(边缘网关安全基线)
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on; # OCSP 装订,加快握手
# HTTP → HTTPS 跳转
if ($scheme != "https") { return 301 https://$host$request_uri; }
}
4.5 缓存:静态资源与 API 响应
http {
# 代理缓存目录:levels 分级目录、keys_zone 元数据、max_size 上限、inactive 过期
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m;
server {
location /api/report/ {
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri"; # 注意含 query
proxy_cache_valid 200 302 5m; # 成功响应缓存 5 分钟
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout; # 后端故障时允许用过期缓存兜底
proxy_pass http://gateway;
}
}
}
4.6 四层代理(stream 模块):TCP/UDP 负载均衡
工业数采场景:多台采集网关把数据经 TCP 推到统一入口,Nginx stream 做四层转发(不解包七层,性能接近裸转发):
stream {
upstream mqtt_ingress {
server 192.168.1.21:1883;
server 192.168.1.22:1883;
}
server {
listen 1883;
proxy_pass mqtt_ingress;
proxy_timeout 60s;
}
}
5. 底层实现剖析
5.1 master/worker 生命周期
- master 启动:读配置 → 校验(nginx -t 即干这个)→ ngx_init_cycle 初始化全局状态 → fork N 个 worker;
- worker 初始化各自事件循环(ngx_event_core_module)后进入 ngx_worker_process_cycle 无限循环:ngx_process_events_and_timers() → 处理事件 → 处理定时器(超时/限流);
- master 收到信号后:HUP → 重新解析配置,若成功则逐 worker 发 QUIT 平滑退出并 fork 新 worker(重载不中断请求);USR2 → 拉起新二进制进程做平滑升级;QUIT → 优雅退出(处理完当前请求再退)。
5.2 事件循环核心:epoll 与惊群
cpp
// 简化伪代码:worker 主循环
for (;;) {
ngx_event_t *rev, *wev;
ngx_process_events_and_timers(cycle); // 内部 epoll_wait
// 遍历就绪事件 → accept / read / write 回调 → 每个阶段状态机推进
}
- 每个 listener 默认只在一个 worker 上 accept(accept_mutex),避免多个 worker 同时被同一个新连接唤醒(惊群);
- 现代内核可用 SO_REUSEPORT 让内核直接做多 worker 负载均衡;
- deferred accept(TCP_DEFER_ACCEPT)延迟到数据到达才 accept,减少无效连接唤醒。
5.3 内存池:ngx_pool_t
Nginx 为每个连接/请求创建内存池(默认 4KB 块),小块分配从池里切,大块单独 malloc;请求结束整个池释放,无逐对象 free,从根上规避内存碎片与泄漏。这也是 Nginx 长期运行内存稳定的关键。
5.4 零拷贝路径:sendfile
传统路径:磁盘 → 内核缓冲 → 用户态 buffer → 内核 socket 缓冲 → 网卡(2 次拷贝 + 2 次切换)
sendfile:磁盘 → 内核缓冲 → 网卡(0 次用户态拷贝,1 次 DMA 拷贝)
sendfile on 对静态文件(大屏资源、报表 PDF、固件包下载)吞吐提升显著;注意它与 gzip 冲突(压缩必须经过用户态,两者不能同时作用于同一响应)。
5.5 upstream 连接池与 keepalive
upstream keepalive 32 维护到后端的空闲连接池:连接用完不关闭,放回池中复用,省去每次请求的 TCP 三次握手 + 慢启动。前提是 proxy_http_version 1.1 且清掉 Connection 头(见 4.2 示例),否则后端按 HTTP/1.0 语义关连接,keepalive 失效。
6. 常错点 / 坑(25 条)
| 坑点 | 现象 | 正确姿势 | |
|---|---|---|---|
| 1 | proxy_pass 带 URI 与不带 URI 混用 | 路径被替换/丢失,404 | proxy_pass http://backend;(不带 URI)原样透传;带 URI(如 http://backend/api/;)会**替换** location 匹配到的前缀 |
| 2 | location 优先级误解 | 正则意外命中 | 记住:正则 > 普通前缀;需阻止正则用 ^~ |
| 3 | alias 当 root 用 | 静态资源 404 | root /data; 拼的是 root + 完整URI;alias /data/; 替换的是 location 匹配段 |
| 4 | if 滥用 | 不可预期行为 | 只用 if 做 return/rewrite;条件处理优先 map + try_files |
| 5 | worker_processes 设满核 × 2 | 上下文切换飙升 | auto 或 = 物理核数;IO 密集可适度多 1~2 个 |
| 6 | worker_connections 与 ulimit -n 不匹配 | accept 失败/连接被拒 | worker_rlimit_nofile 与系统 ulimit -n 同步调大 |
| 7 | 改配置直接 nginx -s reload | 语法错误导致全站挂 | 先 nginx -t 校验 |
| 8 | 后端挂了不配 max_fails/fail_timeout | 请求打到坏节点 502 | 配 max_fails=3 fail_timeout=30s + 主动健康检查模块 |
| 9 | 未配 proxy_http_version 1.1 + 清 Connection | keepalive 不生效,连接频繁新建 | 见 4.2 固定三件套 |
| 10 | 限流 burst/nodelay 语义误解 | 突发被粗暴 503 / 速率形同虚设 | burst 放突发、nodelay 立即处理;无 nodelay 则排队延时 |
| 11 | limit_req_zone 写进 server | nginx -t 报错 | zone 定义必须在 http 上下文 |
| 12 | 证书链不完整(缺中间证书) | 移动端/部分客户端握手失败 | 证书拼接 fullchain(leaf + 中间 + 根) |
| 13 | 证书过期 | 全站 https 报错 | openssl x509 -enddate -noout -in cert.pem 监控到期日 |
| 14 | HTTP/2 只在 listen 写了 ssl 没写 http2 | 无 h2 效果 | listen 443 ssl http2; |
| 15 | 缓存 key 不含 query | 不同参数返回同一缓存 | proxy_cache_key 显式含 $args 或按业务裁剪 |
| 16 | proxy_cache_valid 不写状态码范围 | 304/404 也进缓存 | 明确 200 302、404 等 |
| 17 | gzip 与 sendfile 同时启用 | 静态文件未压缩或走用户态 | 按场景二选一或确认 gzip 对动态响应生效 |
| 18 | server_name 匹配优先级搞错 | 请求进错虚拟主机 | 精确 > 通配前缀 *.a.com > 通配后缀 a.* > 正则 > default_server |
| 19 | listen 80; 与 listen 443 不写 default_server | 无匹配 server_name 时随机 | 设 listen 80 default_server; |
| 20 | 日志不轮转 | 磁盘打满 | logrotate 配 /var/log/nginx/*.log,配合 USR1 重开日志 |
| 21 | WebSocket 代理忘配 Upgrade 头 | ws 连接 400/立即断开 | proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; |
| 22 | if 里写 proxy_pass | 变量解析时机会导致 404 | 同 4:多 server 拆分 + map |
| 23 | 中文文件名/URI 未编码 | 静态资源 404 | 浏览器自动编码;服务端配置保持 UTF-8,避免 alias 拼接中文路径 |
| 24 | 重启代替 reload | 连接中断、服务闪断 | 日常变更用 nginx -s reload;二进制升级才走 USR2 平滑流程 |
| 25 | 日志格式里变量名拼错 | access_log 记录 - 或空 | nginx -t + 查看 error_log 运行时告警 |
7. 性能优化清单(10 条)
- worker 数 = CPU 核数,worker_connections 按 ulimit -n 上限调大(生产 10240+);
- 静态资源走 sendfile + open_file_cache:open_file_cache max=10000 inactive=20s; 缓存 fd/元数据,省去每次 stat;
- gzip 开对范围:gzip_types text/plain text/css application/json application/javascript image/svg+xml,JSON API 压缩收益极大;gzip_comp_level 5~6 平衡 CPU;
- upstream keepalive 连接池 + HTTP/1.1(见 4.2),消灭三次握手;
- 缓存分层:浏览器缓存(expires)→ Nginx proxy_cache → 后端 DB 缓存,逐层削峰;
- TLS 会话复用:ssl_session_cache shared:SSL:10m,同客户端重复握手零往返;
- 限流前置:limit_req/limit_conn 放最外层,保护上游所有服务;
- 日志降噪:健康检查请求单独 location 或 access_log off;,避免日志风暴;
- 四层转发用 stream 模块:不需要七层能力时,TCP 转发比 HTTP 代理吞吐更高;
- 监控三件套:stub_status 模块暴露 active/accepts/handled 指标,接入 Prometheus(呼应既有 Prometheus 篇)与 Grafana 面板。
8. 工业数采落地建议
以用户工业数采项目(Kafka 采集、IPQC、FANUC/三菱 CNC 直连)为例,Nginx 在链路中的典型位置:
CNC/PLC 设备 → 采集网关(Go/C++) → Kafka → TDengine/分析
↓
Nginx 反向代理入口(api.example.com:443)
├── /api/ → Gin/FastAPI 服务集群(限流+负载均衡)
├── /ws/ → 实时监控 WebSocket 代理
├── /report/ → 大屏/报表静态资源 + 代理缓存
└── stream :1883 → MQTT 采集接入四层负载均衡
- API 网关:HTTPS 终结 + 限流 + 灰度(upstream weight);
- 边缘网关反向代理:多台采集网关服务聚合到统一域名,故障节点自动摘除;
- 大屏/报表:sendfile + expires 加速,proxy_cache 缓存分析结果;
- 协议接入:MQTT/自定义 TCP 走 stream 四层负载均衡,横向扩采集服务;
- 配置即代码:include conf.d/*.conf 拆分 + Git 管理(呼应既有 Git 内部原理篇),nginx -t 进 CI 流水线。
9. 总结五要点
- Nginx = 事件驱动 + 多进程:epoll 复用海量连接、worker 无锁并行,是 C10K 时代的正确答案;
- 配置是声明式的树:main → http → server → location 上下文决定指令合法位置,location 匹配优先级是面试与踩坑双高频;
- 反向代理三件套:proxy_pass(URI 语义要分清)+ upstream(权重/策略/keepalive)+ 超时与头传递,就够上线一个生产网关;
- 安全与限流:HTTPS/HTTP2、limit_req/limit_conn、缓存兜底是网关的默认配置,不是可选项;
- 与既有网络篇目互补:Drogon/Gin/FastAPI 写业务,Nginx 做入口;Asio/Beast 实现协议,Nginx 是完整服务器;libcurl 是客户端,Nginx 是服务端。
10. FAQ 速查(10 条)
| 问题 | 答案 |
|---|---|
| nginx -s reload 会断连接吗 | 不会。master 重读配置,worker 处理完当前请求后优雅退出并重建,属平滑重载 |
| 502 一般是什么原因 | 后端无进程/端口未监听/超时。查 error_log、$upstream_addr、后端服务状态 |
| 504 是什么 | 后端响应超过 proxy_read_timeout,调大或排查后端慢查询 |
| 静态文件 403 常见原因 | worker 用户(通常 nginx)无目录读/执行权限;目录缺 x 权限 |
| alias 与 root 怎么选 | 想「替换匹配前缀」用 alias;想「拼接完整 URI」用 root;alias 末尾 / 要小心拼接 |
| HTTP/2 需要什么条件 | listen 443 ssl http2; + 现代 OpenSSL + 客户端支持 |
| 如何查看 Nginx 版本与编译模块 | nginx -V |
| 如何平滑升级二进制 | 编译新版本 → make 后替换二进制 → master 发 USR2 → 新 master 接管 → WINCH/QUIT 收尾 |
| worker 崩溃会影响其他 worker 吗 | 不会。进程隔离,master 会自动重启崩溃的 worker |
| Nginx 与 Apache 现在怎么选 | 新项目默认 Nginx;Apache 保留 .htaccess 目录级配置等生态存量场景 |