Nginx-02 配置文件结构与核心指令
前置阅读:Nginx-01 入门与安装
Nginx 的配置文件是一门小型 DSL。搞清楚它的块结构和继承规则,配置就从「抄配置片段」变成「知道自己在写什么」。
1. 基本语法
配置文件由 指令(directive) 组成,分两类:
简单指令:名称 参数...;,必须以分号结尾。
worker_processes auto;
listen 80 default_server;
块指令(上下文 context):名称 参数 { ... },用大括号包一组指令。
events {
worker_connections 10240;
}
其他规则:
#开头是注释,只支持整行注释,不支持行尾之后的块注释。- 参数中带空格、分号、大括号的必须加引号:
log_format main '$remote_addr - $status'; include可以引入其他文件,支持通配符:include /etc/nginx/conf.d/*.conf;- 大小写敏感。
- 值支持单位后缀:
k/K(KB)、m/M(MB)、g/G(GB);时间用ms/s/m/h/d/w/M/y。
2. 块的层级结构
这是全篇最重要的一张图,闭着眼睛也要能画出来:
main(全局块,没有大括号,就是文件顶层)
│ user / worker_processes / error_log / pid / worker_rlimit_nofile
│
├── events { } 连接处理相关
│ worker_connections / use / multi_accept / accept_mutex
│
├── http { } HTTP 七层配置
│ │ include mime.types / log_format / sendfile / keepalive_timeout / gzip
│ │
│ ├── upstream name { } 后端服务器组
│ │ server / least_conn / ip_hash / keepalive
│ │
│ └── server { } 虚拟主机(一个站点)
│ │ listen / server_name / root / ssl_certificate
│ │
│ └── location { } URI 路由规则
│ │ proxy_pass / try_files / return / rewrite
│ │
│ └── if { } 条件块(能不用就不用,见第 05 篇)
│
├── stream { } 四层 TCP/UDP 代理(需要 --with-stream)
│ ├── upstream name { }
│ └── server { }
│
└── mail { } 邮件代理,用得少
关键点:http 和 stream 是平级的两套体系,http 里的指令在 stream 里基本都不能用,反之亦然。很多人在 stream 块里写 proxy_set_header 报错就是因为这个——四层代理没有 HTTP 头的概念。
3. 指令的继承规则
子块会继承父块的指令,但继承方式分两种,这是面试和排查问题的重点。
3.1 值继承(覆盖式)
绝大多数指令是这种:子块没写就用父块的值,子块写了就完全覆盖父块。
http {
gzip on; # http 层开启
server {
# 这里没写 gzip,继承 http 的 on
location /api/ {
gzip off; # 这个 location 关闭,覆盖父级
}
}
}
3.2 数组式继承(“要么全继承,要么全不要”)
proxy_set_header、add_header、access_log、fastcgi_param 这类可以写多条的指令,规则是:子块只要写了任意一条,父块的所有同名指令就全部失效。
这是最容易踩的坑:
server {
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
location /api/ {
add_header Cache-Control no-cache;
# ⚠️ 这里 X-Frame-Options 和 X-Content-Type-Options 都没了!
# 因为 location 层写了 add_header,server 层的两条被整体丢弃
}
}
解法有两个:
# 方案一:在子块重复写全(笨但明确)
location /api/ {
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header Cache-Control no-cache;
}
# 方案二:抽成片段用 include(推荐)
# /etc/nginx/snippets/security-headers.conf
location /api/ {
include snippets/security-headers.conf;
add_header Cache-Control no-cache;
}
同理,proxy_set_header 也是这样。如果在 http 层设了公共 header,某个 location 又写了一条自己的,公共的全会丢。
add_header还有一个坑:默认只在响应码为 200/201/204/206/301/302/303/304/307/308 时才生效。想让 4xx/5xx 也带上,要加always参数:add_header X-Foo bar always;
4. main 块核心指令
# 运行 worker 进程的用户和用户组。master 仍是 root。
user nginx nginx;
# worker 进程数。auto = CPU 核心数,生产环境就用 auto。
# 为什么等于核心数?因为 worker 是单线程事件循环,多了只会增加切换开销。
worker_processes auto;
# 把 worker 绑定到指定 CPU 核,减少 CPU 缓存失效和进程迁移。
# auto 让 nginx 自动分配,比手写 bitmask 省事。
worker_cpu_affinity auto;
# 单个 worker 能打开的最大文件描述符数。
# 每个连接(客户端 + 上游)都占 fd,这个值不够会报 "too many open files"。
# 必须同时调高系统的 ulimit -n,否则这里设了也没用。
worker_rlimit_nofile 65535;
# worker 进程的 nice 值,-20 到 19,越小优先级越高。
worker_priority -5;
# 错误日志:路径 + 级别。级别从低到高:
# debug < info < notice < warn < error < crit < alert < emerg
# 生产用 warn 或 error,debug 需要编译时带 --with-debug
error_log /var/log/nginx/error.log warn;
# master 进程的 pid 文件
pid /var/run/nginx.pid;
# 动态模块加载(1.9.11+,官方包默认带一些 .so)
# load_module modules/ngx_http_image_filter_module.so;
# reload 时老 worker 的最长存活时间,防止长连接把老 worker 卡住不退
worker_shutdown_timeout 30s;
worker_processes 到底设多少? 答案是 auto(等于 CPU 核心数)。Nginx worker 是单线程事件循环,CPU 密集的活(TLS 握手、gzip 压缩)会占满一个核,所以核数就是并行上限;再多的 worker 只会互相抢 CPU 并增加上下文切换。唯一的例外是有大量阻塞磁盘 I/O 的场景,但正确解法是开 aio threads 线程池,而不是加 worker。
5. events 块
events {
# 单个 worker 的最大连接数。
# 理论最大并发 = worker_processes × worker_connections
# 但做反向代理时,每个客户端请求要占 2 个连接(客户端一个、上游一个),
# 所以实际能扛的客户端数要除以 2。
worker_connections 10240;
# I/O 多路复用模型。Linux 用 epoll,BSD/macOS 用 kqueue。
# 不写的话 nginx 会自动选当前平台最优的,一般不用管。
use epoll;
# 一个 worker 被唤醒后,是只 accept 一个连接还是尽可能 accept 所有待处理连接。
# 高并发短连接场景开启能减少唤醒次数;长连接场景意义不大。
multi_accept on;
# 惊群控制。1.11.3+ 默认 off(因为默认用了 SO_REUSEPORT 或 EPOLLEXCLUSIVE)。
# 详见第 03 篇。
accept_mutex off;
}
worker_connections 和 worker_rlimit_nofile 的关系:前者是 Nginx 自己的软限制,后者是操作系统层面的 fd 上限。worker_rlimit_nofile 必须 ≥ worker_connections,否则会报 worker_connections exceed open file resource limit。
6. http 块核心指令
http {
# ---------- 基础 ----------
include /etc/nginx/mime.types; # 扩展名到 Content-Type 的映射
default_type application/octet-stream; # 找不到映射时的默认类型
charset utf-8;
# 隐藏 Server 头里的版本号(只隐藏版本,不隐藏 "nginx" 字样)
server_tokens off;
# ---------- 日志 ----------
log_format main escape=json
'{"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"ua":"$http_user_agent",'
'"xff":"$http_x_forwarded_for"}';
access_log /var/log/nginx/access.log main buffer=32k flush=5s;
# ---------- 高效传输 ----------
# 零拷贝:文件数据直接从内核页缓存送到 socket,不经过用户态。
sendfile on;
# 配合 sendfile 使用:等响应头和文件开头凑够一个 MSS 再发,减少小包。
tcp_nopush on;
# 关闭 Nagle 算法,小包立即发送,降低延迟。
# tcp_nopush 和 tcp_nodelay 看起来矛盾,但 nginx 内部会协调:
# 传输文件主体时用 nopush 攒包,最后一个包用 nodelay 立即发。
tcp_nodelay on;
# ---------- 连接 ----------
keepalive_timeout 65; # 客户端长连接空闲超时
keepalive_requests 1000; # 单个长连接最多处理多少请求后关闭
reset_timedout_connection on; # 超时连接直接 RST,快速释放内存
client_header_timeout 15s; # 读请求头超时
client_body_timeout 15s; # 读请求体超时
send_timeout 15s; # 两次写操作之间的超时(不是整个响应的超时)
# ---------- 缓冲区 ----------
client_header_buffer_size 4k; # 请求头缓冲,一般 4k 够
large_client_header_buffers 4 16k; # 超大请求头(长 Cookie、长 URL)用这个
client_body_buffer_size 128k; # 请求体缓冲,超了写临时文件
client_max_body_size 50m; # 请求体上限,超了返回 413
# ---------- 压缩 ----------
gzip on;
gzip_vary on; # 加 Vary: Accept-Encoding,让缓存正确区分
gzip_min_length 1k; # 小于 1k 不压缩(压缩后可能更大)
gzip_comp_level 5; # 1-9,5 是性价比拐点
gzip_proxied any; # 对代理来的请求也压缩
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
image/svg+xml font/woff2;
# 注意:text/html 是默认就压的,不需要也不能写在 gzip_types 里
# ---------- 文件描述符缓存 ----------
# 缓存打开过的文件的 fd、大小、修改时间,减少 open/stat 系统调用。
# 静态资源服务器上效果明显。
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# ---------- 引入子配置 ----------
include /etc/nginx/conf.d/*.conf;
}
6.1 几个容易配错的超时
| 指令 | 含义 | 常见误解 |
|---|---|---|
send_timeout |
两次成功写操作之间的最长间隔 | 不是整个响应的总时长 |
keepalive_timeout |
长连接空闲多久后关闭 | 设太大会占着 worker_connections |
client_body_timeout |
两次读请求体之间的间隔 | 同样不是总时长,慢速上传大文件不会被误杀 |
proxy_read_timeout |
从上游两次读之间的间隔 | 后端慢查询超时用这个,默认 60s |
注意这几个都是「两次 I/O 操作之间的间隔」而不是「总时长」。这个设计是对的:一个正常但很慢的大文件上传,每次都能读到数据,就不该被超时干掉。
7. server 块
server {
# ---------- 监听 ----------
listen 80;
listen 443 ssl;
listen [::]:80; # IPv6
listen 80 default_server; # 没有 server_name 匹配上时的兜底
listen 80 reuseport; # 开启 SO_REUSEPORT,内核层面分发连接
listen unix:/var/run/nginx.sock; # Unix socket
# ---------- 域名匹配 ----------
server_name example.com www.example.com;
# server_name *.example.com; # 通配符(只能在开头或结尾)
# server_name ~^www\d+\.example\.com$; # 正则,~ 开头
# server_name ""; # 匹配没有 Host 头的请求
# server_name _; # 无效域名,配合 default_server 做兜底
root /var/www/example;
index index.html index.htm;
# ---------- 错误页 ----------
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html { root /usr/share/nginx/html; }
location / {
try_files $uri $uri/ /index.html;
}
}
7.1 server_name 的匹配优先级
一个请求进来,Nginx 用 Host 头在同一个 listen 地址下的所有 server 里选一个,顺序是:
- 精确匹配:
example.com - 前置通配符(最长的优先):
*.example.com - 后置通配符(最长的优先):
www.example.* - 正则(按配置文件出现顺序,第一个匹配的赢):
~^www\d+\. default_server,没标 default_server 则用该listen下的第一个 server
兜底 server 是必须配的。不配的话,别人把域名 A 记录解析到你的 IP,你的第一个 server 就会莫名其妙给他提供服务:
# 显式丢弃所有未知 Host 的请求
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/dummy.crt;
ssl_certificate_key /etc/nginx/ssl/dummy.key;
return 444; # nginx 特有:直接关闭连接,不返回任何响应
}
return 444 是 Nginx 自定义的非标准状态码,效果是直接断开 TCP 连接,一个字节都不回。对付扫描器很好用。
7.2 root 和 alias 的区别
这是高频面试题,也是最常写错的配置。
location /static/ {
root /var/www;
# 请求 /static/a.png → 实际文件 /var/www/static/a.png
# root 是「拼接」:root 值 + 完整 URI
}
location /static/ {
alias /var/www/assets/;
# 请求 /static/a.png → 实际文件 /var/www/assets/a.png
# alias 是「替换」:把 location 匹配的那部分换成 alias 值
}
记忆方法:root 加法,alias 减法(替换)。
alias 的两个坑:
- 如果
location以/结尾,alias也必须以/结尾,否则会拼出/var/www/assetsa.png。 alias不能和正则location随便混用,正则 location 里用alias必须在 alias 值里带上捕获组。
# ❌ 危险写法:路径穿越
location /static { # 注意没有结尾斜杠
alias /var/www/assets/;
}
# 请求 /static../etc/passwd 可能穿越出去
# ✅ 正确
location /static/ {
alias /var/www/assets/;
}
8. 内置变量
Nginx 变量是配置里的「胶水」,日志、代理、判断都靠它。常用的按类别整理:
8.1 请求相关
| 变量 | 含义 | 示例 |
|---|---|---|
$request |
完整请求行 | GET /a/b?x=1 HTTP/1.1 |
$request_method |
方法 | GET |
$scheme |
协议 | http / https |
$host |
优先取 Host 头,没有则用 server_name | example.com |
$http_host |
原始 Host 头(可能带端口,可能为空) | example.com:8080 |
$server_name |
匹配到的 server_name | example.com |
$uri |
解码并规范化后的 URI,不含查询串;rewrite 后会变 | /a/b |
$request_uri |
原始完整 URI,含查询串,不解码 | /a/b?x=1 |
$document_uri |
同 $uri |
/a/b |
$args / $query_string |
查询串 | x=1 |
$arg_名字 |
取某个查询参数 | $arg_x → 1 |
$http_名字 |
取任意请求头(横杠转下划线、转小写) | $http_user_agent |
$cookie_名字 |
取某个 Cookie | $cookie_sessionid |
$request_length |
请求总长度(行 + 头 + 体) | 312 |
$content_type |
Content-Type 头 | application/json |
$is_args |
有查询参数时为 ?,否则空 |
拼 URL 时常用 |
$uri vs $request_uri 是必考点:$uri 是解码规范化后的、会被 rewrite 改变、不含 ? 后面的部分;$request_uri 是客户端发来的原样字符串。做 301 跳转时用 $request_uri 才能保住查询参数:
return 301 https://$host$request_uri; # ✅ 保留 ?a=1
return 301 https://$host$uri; # ❌ 丢掉查询串
8.2 客户端与连接
| 变量 | 含义 |
|---|---|
$remote_addr |
客户端 IP(有代理时是上一跳的 IP) |
$remote_port |
客户端端口 |
$remote_user |
HTTP Basic Auth 的用户名 |
$binary_remote_addr |
二进制形式的客户端 IP,限流 zone 的 key 必须用这个(省内存) |
$server_addr / $server_port |
服务端 IP / 端口 |
$connection |
连接序号 |
$connection_requests |
当前连接上已处理的请求数 |
$proxy_add_x_forwarded_for |
原 XFF 头 + , $remote_addr |
8.3 响应与耗时
| 变量 | 含义 |
|---|---|
$status |
响应状态码 |
$body_bytes_sent |
发给客户端的响应体字节数 |
$bytes_sent |
总字节数(含响应头) |
$request_time |
Nginx 视角的总耗时:从读到第一个字节到写完最后一个字节,含客户端网络时间 |
$upstream_response_time |
后端处理耗时 |
$upstream_connect_time |
与后端建连耗时 |
$upstream_header_time |
收到后端响应头的耗时 |
$upstream_addr |
实际处理的后端地址(重试会有多个,逗号分隔) |
$upstream_status |
后端返回的状态码 |
$upstream_cache_status |
缓存命中状态:HIT/MISS/EXPIRED/BYPASS/STALE/UPDATING/REVALIDATED |
排查慢请求的黄金三件套:$request_time、$upstream_response_time、$upstream_connect_time。
request_time大、upstream_response_time小 → 客户端网络慢,或者响应体太大upstream_response_time大 → 后端业务慢,去查后端upstream_connect_time大 → 后端连接队列满 / 网络问题 / TCP backlog 溢出
8.4 时间与其他
| 变量 | 含义 |
|---|---|
$time_iso8601 |
2026-08-03T10:30:00+08:00 |
$time_local |
03/Aug/2026:10:30:00 +0800 |
$msec |
Unix 时间戳,毫秒精度 |
$request_id |
每个请求唯一的 32 位十六进制串,做全链路追踪必备 |
$hostname |
机器 hostname |
$nginx_version |
Nginx 版本号 |
$pid |
worker 进程 pid |
$document_root |
当前请求的 root 值 |
$realpath_root |
解析软链后的真实 root 路径 |
$request_id 很实用,一行配置就能打通 Nginx 日志和后端日志:
proxy_set_header X-Request-ID $request_id;
add_header X-Request-ID $request_id always;
8.5 用 map 自定义变量
map 是纯配置层面做「查表映射」的工具,比 if 干净得多,而且是惰性求值(用到才算):
http {
# 根据 UA 判断设备类型
map $http_user_agent $device {
default "desktop";
~*android|iphone "mobile";
~*ipad|tablet "tablet";
}
# 只对特定路径关闭访问日志
map $request_uri $loggable {
default 1;
~^/health$ 0;
~^/metrics$ 0;
}
# WebSocket 升级头的标准写法
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
access_log /var/log/nginx/access.log main if=$loggable;
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}
map 的匹配规则:先精确字符串,再 ~(区分大小写正则)/~*(不区分),最后 default。还支持 hostnames 参数做域名通配、include 从文件读映射表(适合几万条 IP 白名单)。
9. 一份可直接用的生产配置模板
user nginx;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65535;
worker_shutdown_timeout 30s;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 10240;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
server_tokens off;
log_format main escape=json
'{"time":"$time_iso8601","id":"$request_id",'
'"addr":"$remote_addr","xff":"$http_x_forwarded_for",'
'"host":"$host","method":"$request_method","uri":"$request_uri",'
'"status":$status,"bytes":$body_bytes_sent,'
'"rt":$request_time,"urt":"$upstream_response_time",'
'"uaddr":"$upstream_addr","ustatus":"$upstream_status",'
'"ref":"$http_referer","ua":"$http_user_agent"}';
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
aio threads;
directio 8m;
keepalive_timeout 65;
keepalive_requests 1000;
reset_timedout_connection on;
client_header_timeout 15s;
client_body_timeout 15s;
send_timeout 15s;
client_max_body_size 50m;
client_body_buffer_size 128k;
large_client_header_buffers 4 16k;
gzip on;
gzip_vary on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_proxied any;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss image/svg+xml;
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# 兜底 server:丢弃所有未知 Host
server {
listen 80 default_server;
server_name _;
return 444;
}
include /etc/nginx/conf.d/*.conf;
}
10. 配置组织的最佳实践
/etc/nginx/
├── nginx.conf 只放全局配置 + include
├── conf.d/
│ ├── 00-default.conf 兜底 server
│ ├── api.example.com.conf 一个域名一个文件
│ └── www.example.com.conf
├── snippets/ 可复用片段
│ ├── proxy-headers.conf 代理公共头
│ ├── security-headers.conf 安全头
│ ├── ssl-common.conf TLS 公共参数
│ └── letsencrypt.conf 证书续期路径
└── upstreams/
└── backend.conf upstream 定义
snippets/proxy-headers.conf 示例:
proxy_http_version 1.1;
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_set_header X-Request-ID $request_id;
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 16k;
用的时候一行搞定:
location /api/ {
include snippets/proxy-headers.conf;
proxy_pass http://backend;
}
排查 include 嵌套问题用 nginx -T,它会输出所有 include 展开后的最终配置:
nginx -T | less
nginx -T | grep -n "proxy_pass" # 找某个指令到底在哪生效了
11. 面试题
Q:root 和 alias 的区别?
root 是路径拼接:最终路径 = root 值 + 完整 URI。alias 是路径替换:把 location 匹配到的前缀替换成 alias 值。alias 只能用在 location 里,且 location 以 / 结尾时 alias 也必须以 / 结尾。
Q:$uri 和 $request_uri 的区别?
$uri 是解码并规范化后的路径,不含查询串,会被 rewrite 修改;$request_uri 是客户端原始请求行里的完整 URI,含查询串,不解码、不随 rewrite 改变。做跳转保留参数要用 $request_uri。
Q:为什么 location 里写了一条 add_header,server 层的 header 就全没了?
add_header、proxy_set_header 这类数组式指令的继承是「全有或全无」:子层级只要出现任意一条同名指令,父层级的所有同名指令一律不继承。解决办法是子层重复写全,或者抽成 include 片段。
Q:worker_processes 设成多少合适?为什么不是越多越好?
设 auto(= CPU 核心数)。worker 是单线程事件循环,核数就是并行上限;超出核数只会增加上下文切换和 CPU 争抢。磁盘 I/O 阻塞的场景应该用 aio threads 线程池解决,而不是加 worker。
Q:Nginx 单机最大并发连接数怎么算?
理论上限 = worker_processes × worker_connections。但做反向代理时每个请求要消耗 2 个连接(客户端侧一个 + 上游侧一个),所以实际可服务的客户端连接数要除以 2。此外还受 worker_rlimit_nofile、系统 ulimit -n、fs.file-max、端口范围(net.ipv4.ip_local_port_range)等限制。
Q:sendfile、tcp_nopush、tcp_nodelay 三个一起开不矛盾吗?
不矛盾。sendfile 是零拷贝发文件;tcp_nopush(TCP_CORK)让内核攒够一个完整的包再发,减少小包数量;tcp_nodelay(关 Nagle)让小包立即发。Nginx 内部会协调:传文件主体时用 nopush 攒包,到最后一个包时切成 nodelay 立即发出去,两者是接力关系而非冲突。
上一篇:Nginx-01 入门与安装 | 下一篇:Nginx-03 进程模型与事件驱动原理
xingliuhua