目录

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 { }                邮件代理,用得少

关键点:httpstream 是平级的两套体系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_headeradd_headeraccess_logfastcgi_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_connectionsworker_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 里选一个,顺序是:

  1. 精确匹配example.com
  2. 前置通配符(最长的优先):*.example.com
  3. 后置通配符(最长的优先):www.example.*
  4. 正则(按配置文件出现顺序,第一个匹配的赢):~^www\d+\.
  5. 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 的两个坑:

  1. 如果 location/ 结尾,alias 也必须以 / 结尾,否则会拼出 /var/www/assetsa.png
  2. 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_x1
$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:rootalias 的区别?

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_headerserver 层的 header 就全没了?

add_headerproxy_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 -nfs.file-max、端口范围(net.ipv4.ip_local_port_range)等限制。

Q:sendfiletcp_nopushtcp_nodelay 三个一起开不矛盾吗?

不矛盾。sendfile 是零拷贝发文件;tcp_nopushTCP_CORK)让内核攒够一个完整的包再发,减少小包数量;tcp_nodelay(关 Nagle)让小包立即发。Nginx 内部会协调:传文件主体时用 nopush 攒包,到最后一个包时切成 nodelay 立即发出去,两者是接力关系而非冲突。


上一篇:Nginx-01 入门与安装 | 下一篇:Nginx-03 进程模型与事件驱动原理