Nginx典型配置错误复盘与优化

描述

问题背景

Nginx 是互联网生产环境中使用最广泛的反向代理和 Web 服务器之一。不管是做静态资源服务、API 网关,还是负载均衡器,Nginx 几乎是标准配置。很多运维工程师日常和它打交道,但真正能把配置细节说清楚的人不多——不是因为 Nginx 复杂,而是因为它的很多行为不符合直觉,配置项之间的相互作用容易被忽略。

实际生产环境里,大量线上故障的直接原因就是 Nginx 配置不当:有人把请求代理到了已经宕机的 upstream 导致 502,有人因为 location 匹配优先级问题导致静态资源 404,有人文件上传一直报 413 查了半天是 client_max_body_size 没配,有人升级 TLS 证书后浏览器报不安全是因为中间证书没一起部署。每一个坑都是真实踩出来的。

本文从一线运维工程师的视角,系统梳理 Nginx 配置中最容易出问题的十个场景。每个坑都按照「现象描述 → 根因分析 → 正确配置 → 验证方法 → 风险提醒」的闭环结构来讲,让你不仅知道怎么修,还知道为什么这样修,以及修完之后怎么确认生效。

适用场景

日常运维:接手新服务器、变更配置前检查、线上故障排查

上线变更:每次 Nginx 配置变更后的验证

面试准备:理解 Nginx 核心配置原理

性能优化:定位 Nginx 导致的响应慢或资源消耗异常

通用排查思路

在逐个拆解具体踩坑点之前,先建立一个通用的排查框架。每次遇到 Nginx 配置问题,按这个顺序走一遍,能排除大部分问题。

第一步:验证配置语法

Nginx 提供了一个内置的配置检查命令,任何修改配置之后、上线之前,必须先跑这一条:

 

nginx -t
# 典型输出:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

 

如果输出报错,它会指出具体文件和行号。常见错误比如拼写错误、分号缺失、括号不匹配等。

但要注意:-t 只检查语法和基本结构,不检查逻辑错误。比如你把 proxy_pass 指向了一个不存在的 upstream,-t 依然会通过,但实际跑起来会 502。所以 -t 通过只是必要条件,不是充分条件。

第二步:查看 error_log

error_log 是排查 Nginx 问题的第一手资料。很多工程师配置了日志却从来不看,等到出问题了才想起来去翻。

 

# 查看 error_log 位置(默认在 nginx.conf 里配置)
grep -r "error_log" /etc/nginx/

# 实时跟踪最新错误
tail -f /var/log/nginx/error.log

# 按时间过滤最近 5 分钟的日志
find /var/log/nginx/ -name "error.log" -mmin -5 | xargs tail -n 50

 

error_log 的级别可以在配置里指定,常见的有 debug、info、notice、warn、error、crit。建议生产环境用 warn 或 error,太细的级别会产生大量 IO 开销。

第三步:检查进程和连接状态

 

# 看 Nginx master 和 worker 进程
ps aux | grep nginx

# 看 Nginx 监听的端口
ss -tlnp | grep nginx

# 看当前连接数状态
netstat -an | awk '/:80s/ {s[$NF]++} END {for(k in s) print k, s[k]}'
# 或用 ss(更现代)
ss -s

# 看每个 worker 的连接数(看负载是否均衡)
ps -o pid,ppid,comm,%cpu,%mem --no-headers -e | grep nginx

 

如果 worker 进程 CPU 或内存异常高,往往意味着配置里有性能问题,比如 regex location 匹配或过于频繁的日志写操作。

第四步:reload 而非 restart

Nginx 支持不停机 reload 配置,这应该是线上变更的标准操作:

 

nginx -s reload
# 或者向 master 进程发信号
kill -HUP $(cat /var/run/nginx.pid)

 

reload 的逻辑是:master 进程加载新配置,启动新的 worker 进程处理新请求,旧的 worker 优雅退出(处理完现有请求后自动关闭)。这个过程不会中断现有连接。

但要注意:有些配置变更不支持 reload,必须 restart,比如绑定到新端口、修改 SSL 证书路径等。这类操作需要提前申请维护窗口。

第五步:建立配置变更管理机制

生产环境的 Nginx 配置变更是高风险操作,建议遵循以下原则:

所有配置变更走代码仓库(Git),禁止直接在生产环境手工改

变更前先在测试环境验证 nginx -t 通过

变更时先 push 配置文件,再用 ansible/salt 或者直接 cp 部署

部署后立即 nginx -t && nginx -s reload,不要离开,等观察几分钟确认正常再离开

准备好回滚脚本,保留上一版配置文件的备份

踩坑点一:location 匹配优先级混乱

现象

用户访问 /api/users 返回 404,或者请求本该走 /api 却走到了 / 的处理逻辑,导致返回了 HTML 页面而不是 JSON。这种问题在团队多人维护 Nginx 配置时特别常见,某人在 http 段新增了一个 catch-all location,导致原有的精细化匹配全部失效。

根因

Nginx 的 location 匹配规则是按优先级递进匹配的,不是按配置顺序,很多工程师以为「写在后面的 location 覆盖前面的」,这是一个根本性误解。Nginx location 匹配优先级从高到低如下:

location = /path —— 精确匹配,优先级最高

location ^~ /path —— 前缀匹配,找到最长匹配后不再做正则匹配

location ~ /path 或 location ~* /path —— 正则匹配(~ 区分大小写,~* 不区分大小写),按配置顺序匹配,第一个命中的就停止

location /path —— 普通前缀匹配,最长匹配原则

典型错误配置:

 

server {
    listen 80;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    location /api {
        proxy_pass http://127.0.0.1:8080;
    }

    # 下面的 catch-all 会导致 /api 也走这个分支,因为没有加 ^~
    location ~* .php$ {
        proxy_pass http://127.0.0.1:9000;
    }
}

 

正确配置

根据实际需求选择正确的匹配类型:

 

server {
    listen 80;

    # 精确匹配,首页
    location = / {
        root /usr/share/nginx/html;
        index index.html;
    }

    # 前缀匹配,不允许正则覆盖,用于静态资源目录
    location ^~ /static/ {
        root /data/www;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 前缀匹配,用于 API 代理
    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # 正则匹配,按顺序排在普通前缀匹配之后
    location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        root /data/www;
        expires 7d;
        access_log off;
    }

    # 默认 catch-all
    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

 

关键原则:正则 location(~ 或 ~*)会按配置文件中的顺序匹配,一旦匹配就停止。普通前缀 location(/path)采用最长前缀匹配原则,即选择匹配路径最长的那个。如果某个前缀 location 不想被后续的正则匹配覆盖,需要加 ^~ 修饰符。

验证方法

用 curl 模拟请求,观察返回的响应头和状态码:

 

# 测试精确匹配
curl -I http://localhost/
# 期望:返回 index.html,状态码 200

# 测试 /api 路径
curl -I http://localhost/api/users
# 期望:代理到 8080,返回 API 响应,不是 404

# 测试静态资源(确认走缓存路径)
curl -I http://localhost/static/logo.png
# 期望:返回 200,且有 Cache-Control 头

# 用 -v 查看详细处理过程(需要开启 rewrite log)
curl -v http://localhost/api/test 2>&1 | head -30

 

如果配置了 rewrite_log on 和 notice 级别的 error_log,可以在 error.log 中看到详细的 location 匹配过程:

 

error_log /var/log/nginx/error.log notice;
rewrite_log on;

 

风险提醒

修改 location 匹配规则是高风险操作,因为线上 API 路径、静态资源路径可能在代码层有依赖。变更前需要:

确认所有请求路径的用途

在测试环境全量回归

保留旧配置文件备份

变更后密切监控 404 和 502 错误率

踩坑点二:proxy_pass 末尾带不带 / 的区别

现象

配置了 proxy_pass http://127.0.0.1:8080; 和 proxy_pass http://127.0.0.1:8080/;,表面看起来一样,但实际代理出去的请求路径完全不同。

具体来说:如果原始请求是 GET /api/users HTTP/1.1,访问目标是 http://your-domain.com/api/users:

proxy_pass http://127.0.0.1:8080;(无尾部斜杠):代理后请求变成 GET /api/users HTTP/1.1(完整路径保留)

proxy_pass http://127.0.0.1:8080/;(有尾部斜杠):代理后请求变成 GET /users HTTP/1.1(/api 部分被替换掉了)

这个差异会导致后端收到完全不同的路径,引发 404 或业务逻辑错误。

根因

这是 Nginx 中最反直觉的配置之一。Nginx 在处理 proxy_pass 时,会根据是否指定了 URI(路径部分)来决定是否做路径替换:

proxy_pass http://127.0.0.1:8080; —— 没有指定 URI,Nginx 将原始请求的完整路径传递给 upstream

proxy_pass http://127.0.0.1:8080/; —— 指定了 URI(根路径),Nginx 会把匹配 location 时使用的前缀部分从请求路径中移除,剩下的部分拼接到新的 URI 上

用具体例子说明:

 

# 场景:原始请求 /api/users -> proxy_pass http://127.0.0.1:8080/

# 写法一:不带 /
location /api {
    proxy_pass http://127.0.0.1:8080;
    # 代理后:GET /api/users -> upstream 收到 /api/users
}

# 写法二:带 /
location /api {
    proxy_pass http://127.0.0.1:8080/;
    # 代理后:GET /api/users -> upstream 收到 /users
}

 

正确配置

根据业务需求选择正确的写法,并配合 rewrite 或 proxy_redirect 做路径调整:

 

# 场景一:后端路径与前端路径一致,不需要替换
location /api {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

# 场景二:后端路径有前缀,且 location 使用了精确前缀匹配,需要替换
location /api/ {
    proxy_pass http://127.0.0.1:8080/;
    # /api/users -> /users
    # /api/login -> /login
}

# 场景三:需要保留部分路径,或路径映射关系复杂
location /app/v1/ {
    rewrite ^/app/v1/(.*) /$1 break;
    proxy_pass http://127.0.0.1:8080;
    # /app/v1/users -> /users
    # /app/v1/orders -> /orders
}

# 场景四:后端要求 Host 头为特定值
location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host "backend.example.com";
}

 

验证方法

在后端服务上开启请求日志,观察收到的请求路径:

 

# 在 upstream 服务的 access log 中查看
# 如果是 Node.js/Express:
console.log(req.method, req.url);

# 如果是 Python/Flask:
print(request.method, request.path, request.url)

# 用 curl 本地测试(同时在前端和后端抓包)
curl -v http://localhost/api/users 2>&1
# 看 Host 头、X-Real-IP 头、以及实际的 URL 是什么

 

一个完整的调试流程:

 

# 1. 检查 proxy_pass 写法
grep -n "proxy_pass" /etc/nginx/conf.d/*.conf

# 2. 开启调试日志查看 rewrite 过程
tail -f /var/log/nginx/error.log

# 3. 用 nc/strace 在 upstream 端口抓包
# nc -l 8080  # 监听本地 8080,看收到的请求行

# 4. 确认 upstream 服务日志中的路径
# 这是最直接的方式——upstream 日志里的 path 是什么,就是 Nginx 实际传过去的路径

 

风险提醒

修改 proxy_pass 的 URI 写法会影响所有匹配该 location 的请求路径。如果后端有多个微服务共用一个 upstream 块,尤其要注意。建议:

先在测试环境用完整请求路径做回归

确认后端 API 是否做了路径校验(比如 Spring Cloud Gateway 会严格校验路由前缀)

配合监控,观察上线后 404 错误率变化

踩坑点三:try_files 使用不当导致死循环

现象

访问某些 URL 时浏览器报 "Too many redirects"(重定向次数过多),或者直接返回 500 Internal Server Error。用户在浏览器里看到的是一片空白或者错误页面。

根因

try_files 是 Nginx 用来检查文件是否存在的重要指令,但它和 alias、rewrite、rewrite ^/(.*) $1 last 等指令组合时,行为往往出乎意料。

最常见的死循环配置:

 

location / {
    root /data/www;
    try_files $uri $uri/ /index.html;
}

 

这个配置的意图是:先尝试找对应的文件,再找同名目录下的 index.html,最后 fallback 到根目录的 index.html。看起来没问题。

但如果同时在另一个 location 里用 rewrite 把请求重定向回去:

 

location / {
    try_files $uri $uri/ /index.html;
}

location = /index.html {
    root /data/www;
    rewrite ^ / permanent;  # 这行导致 /index.html 永久重定向到 /,死循环
}

 

另一种常见错误是 try_files 和 alias 混用时的路径问题:

 

location /static/ {
    alias /data/static/;
    try_files $uri /static/404.html;
    # 当文件不存在时,try_files 会追加 /static/ 前缀去找 /static/404.html
    # 但 alias 指令已经映射了路径,导致路径拼接混乱
}

 

正确配置

try_files 的正确用法,关键是理解它按顺序尝试每个参数,最后一个参数是 fallback URI:

 

# 基础用法:先找文件,再找目录 index,再 fallback
location / {
    root /data/www;
    index index.html index.htm;
    try_files $uri $uri/ /fallback.html;
}

# PHP FastCGI 场景:找文件 -> 找目录 -> 传给 PHP
location / {
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ .php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

# 带命名的 fallback
location / {
    try_files $uri @backend;
}

location @backend {
    proxy_pass http://127.0.0.1:8080;
}

 

与 alias 配合时需要注意路径不要重叠:

 

# alias 会改变 root 的映射关系,try_files 的 $uri 是基于 alias 后的路径
location /static/ {
    alias /data/static/;
    try_files $uri =404;
    # 不要写成 try_files $uri /static/404.html,
    # 因为 /static/ 这个前缀会被 alias 再次处理
}

 

验证方法

用 curl 模拟各种文件存在/不存在的场景:

 

# 测试文件存在的情况
curl -I http://localhost/static/exists.png
# 期望:200 OK,有 Content-Type 头

# 测试文件不存在的情况(看 fallback 是否生效)
curl -I http://localhost/nonexistent/path
# 期望:返回 fallback 的响应,状态码可能是 200 或 302

# 测试重定向循环(如果配置有误,会在 curl 输出中看到循环)
curl -v http://localhost/ 2>&1 | grep -E "(< HTTP|< Location)"
# 如果看到连续多个 302 -> / 的重定向,就是死循环了

# 在错误日志中搜索 redirect 循环
grep -i "redirect" /var/log/nginx/error.log | tail -20

 

风险提醒

try_files 的死循环问题在上线初期可能不会触发——只有当某些特定文件不存在、且 fallback 路径又依赖于被 try_files 检查的路径时才会暴露。建议在上线前用不存在的路径做一次全量测试:

 

# 批量测试不存在的路径是否都有合理的 fallback
for path in /api/noexist /static/noexist/file.js /images/noexist.png; do
    status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost${path})
    echo "${path} -> ${status}"
done
# 期望:所有路径都返回非 500、非 301 循环的有效响应

 

踩坑点四:upstream keepalive 配置不足

现象

业务高峰期大量用户反映接口响应慢,甚至出现 502 Bad Gateway。但后端 Java/Python 服务的 CPU 和内存使用率都不高,数据库连接池也没满。用 netstat 查看网络连接状态,发现服务器有大量 TIME_WAIT 连接:

 

netstat -an | awk '/:80s/ {print $NF}' | sort | uniq -c | sort -rn
# 输出中 TIME_WAIT 数量成千上万

 

同时后端 upstream 服务报错日志里出现了类似 "too many connections" 或 "connection refused" 的信息。

根因

这是 Nginx 与 upstream 之间使用短连接导致的问题。在没有配置 keepalive 的情况下,Nginx 每转发一个请求都会新建一个 TCP 连接到 upstream,请求结束后连接立即关闭。这会产生两个问题:

TIME_WAIT 堆积:大量短连接关闭后,端口会进入 TIME_WAIT 状态(默认持续 60 秒),消耗文件描述符和端口号。在高并发场景下,如果 upstream 端口复用太快,可能出现端口耗尽。

后端连接数暴涨:Nginx worker 进程数 × 每个 worker 的活跃请求数 = 实际需要的 upstream 连接数。如果每个请求都新建连接,upstream 服务的连接池会很快耗尽。

典型错误配置:

 

upstream backend {
    server 127.0.0.1:8080;
    # 没有 keepalive 配置,每个请求都是新建连接
}

server {
    location / {
        proxy_pass http://backend;
        # 没有配置 proxy_http_version 1.1,也没有 proxy_set_header Connection ""
    }
}

 

正确配置

Nginx upstream keepalive 配置包含三个关键部分:

 

upstream backend {
    server 127.0.0.1:8080 weight=5 max_fails=3 fail_timeout=30s;
    # weight: 权重
    # max_fails: 失败多少次后认为该 server 不可用
    # fail_timeout: 失败后多少秒内不再向该 server 发请求

    # 关键:开启 keepalive 连接池
    # keepalive 连接数建议设置为 worker_connections 的 10%~20%
    keepalive 32;
    keepalive_requests 1000;
    keepalive_timeout 60s;
}

server {
    listen 80;
    # 必须使用 HTTP/1.1 才能支持 keepalive
    proxy_http_version 1.1;

    location / {
        proxy_pass http://backend;
        # 关键:清除 Connection 头,让连接保持 alive
        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_connect_timeout 5s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}

 

keepalive 32 表示 Nginx 会为这个 upstream 保持 32 个空闲长连接。当请求进来时,Nginx 优先使用空闲连接,只有当空闲连接不够用时才新建连接。如果配置了 keepalive 0 或不配置 keepalive,则每个请求都走新建的短连接。

验证方法

查看 upstream 连接状态,确认连接被复用:

 

# 方法一:在 upstream 服务端查看连接数(如果 upstream 是 Node.js/Java 等,可以查看其连接池状态)
# 以 Java Spring Boot 为例,看日志中 HikariCP 的连接池状态

# 方法二:在 Nginx 端查看连接状态
ss -tn | grep :8080 | awk '{print $4}' | sort | uniq -c
# keepalive 正常时,8080 端口的连接数会比较稳定,不会随 QPS 线性增长

# 方法三:查看 error_log 中是否有 upstream 报错
grep -i "upstream|connection" /var/log/nginx/error.log | tail -50

# 方法四:对比修改前后的 TIME_WAIT 数量
# 修改前:大量 TIME_WAIT
netstat -an | grep TIME_WAIT | wc -l
# 修改后:TIME_WAIT 数量大幅下降

 

一个简单的压测对比(用 ab 或 wrk):

 

# 用 ab 压测,观察连接建立情况
ab -n 1000 -c 100 http://localhost/api/test

# 如果没有 ab
wrk -t10 -c100 -d30s http://localhost/api/test

# 观察结果中的 Time taken for tests、Requests per second
# keepalive 生效后,QPS 应该明显提升,延迟明显下降

 

风险提醒

keepalive 配置如果设置过大,会占用过多 Nginx 内存,因为每个 keepalive 连接都会占用一个 socket 缓存。一般建议 keepalive 数量设为 worker_connections 的 10%~20%。另外,如果 upstream 服务不支持 HTTP/1.1(比如一些老的 RPC 服务),keepalive 不会生效。

踩坑点五:client_max_body_size 未设置或设置过小

现象

用户上传文件时,Nginx 直接返回 413 Request Entity Too Large,但后端服务的文件上传限制其实很大。运维工程师去查后端配置,发现后端没有做任何限制,困惑不已。

这是 Nginx 默认的请求体大小限制在作祟:client_max_body_size 默认值是 1MB。任何超过这个大小的请求,Nginx 会在读取请求体的阶段就直接拒绝,根本不会向后端转发。

根因

Nginx 在接收请求体的阶段就会检查大小限制,如果超过 client_max_body_size,直接返回 413,不会到 proxy_pass 或 fastcgi_pass 那一层。这个限制是在 Nginx 层面硬拦截的。

常见错误场景:

根本没有配置 client_max_body_size,用的是默认值 1MB,上传图片或文档立刻超限

配置了 client_max_body_size 但位置不对——放在了 http 段而不是 server 段或 location 段

配置了但值设置得过小,没有预估业务增长

正确配置

根据业务需求设置合理的请求体大小限制:

 

http {
    # 全局默认限制,可以设置得相对保守
    client_max_body_size 10m;

    server {
        listen 80;
        server_name example.com;

        # 对特定业务设置更大的限制
        location /upload/ {
            # 文件上传业务,需要较大限制
            client_max_body_size 100m;
            # 同时调整超时,因为大文件上传耗时长
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
            proxy_connect_timeout 75s;
            proxy_pass http://upload-backend;
        }

        location /api/ {
            # 普通 API 请求,1m 就够了
            client_max_body_size 1m;
            proxy_pass http://api-backend;
        }

        # 也可以在 error_page 中自定义 413 错误页面
        error_page 413 = /413.html;
        location = /413.html {
            root /data/www/errors;
            internal;
        }
    }
}

 

验证方法

用 dd 生成指定大小的测试文件,然后上传:

 

# 生成一个 2MB 的测试文件
dd if=/dev/zero of=/tmp/test_2mb.bin bs=1M count=2

# 生成一个 15MB 的测试文件
dd if=/dev/zero of=/tmp/test_15mb.bin bs=1M count=15

# 测试上传 2MB 文件(应该成功)
curl -X POST -F "file=@/tmp/test_2mb.bin" http://localhost/upload/ -w "
HTTP Status: %{http_code}
"

# 测试上传 15MB 文件(如果限制是 10m,应该返回 413)
curl -X POST -F "file=@/tmp/test_15mb.bin" http://localhost/upload/ -w "
HTTP Status: %{http_code}
"

# 观察 error_log 中的 413 记录
grep "client intended to send too large body" /var/log/nginx/error.log | tail -10

 

风险提醒

设置过大的 client_max_body_size 会带来安全风险——攻击者可能通过上传超大文件来耗尽服务器磁盘或内存。最佳实践是:

根据实际业务需求设置,宁可保守也不要太大

配合后端做二次校验,前端限制不可信

上传目录单独挂载,限制磁盘配额

配合 rate limiting 防止频繁上传

 

# 配合 limit_rate 限制上传速度,防止恶意占用带宽
location /upload/ {
    client_max_body_size 100m;
    limit_rate 1m;  # 限制上传速度 1MB/s
    proxy_pass http://upload-backend;
}

 

踩坑点六:gzip 压缩配置不当

现象

服务器带宽使用率很高,CPU 负载也上去了,但用户反映页面加载还是很慢。排查发现虽然配置了 gzip,但 response header 里没有 Content-Encoding: gzip。用浏览器的开发者工具看 network 面板,发现传输的文件体积很大,没有被压缩。

或者反过来,gzip_comp_level 设置得太高,CPU 占用率直接拉满,压缩效果却没提升多少。

根因

gzip 是 Nginx 中提升传输效率的重要手段,但默认 gzip 是关闭的,而且 gzip_types 默认只包含 text/html。如果只加了 gzip on; 就以为万事大吉了,但实际上大部分请求的 MIME 类型没有被加入压缩列表。

另一个常见问题是 gzip_vary 头没有开启,导致代理缓存(如 CDN、Varnish)给不同客户端返回了不正确的压缩版本。

正确配置

一个完整的 gzip 配置:

 

http {
    # 开启 gzip
    gzip on;

    # 压缩级别:1(最快,压缩率低)到 9(最慢,压缩率高),默认 1
    # 生产环境建议 4~5,平衡 CPU 开销和压缩率
    gzip_comp_level 5;

    # gzip_buffers 定义压缩时使用的缓存大小
    gzip_buffers 16 8k;

    # gzip_http_version 指定支持的 HTTP 版本
    gzip_http_version 1.1;

    # 开启 Vary 头,代理缓存要识别 User-Agent
    gzip_vary on;

    # 限制最小压缩长度,太小的内容压缩反而增加开销
    gzip_min_length 1024;

    # 对指定 MIME 类型启用压缩
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/x-javascript
        application/octet-stream
        image/svg+xml;

    # gzip 对部分代理缓存的兼容性配置
    # 确保代理层能正确处理压缩和非压缩响应

    server {
        listen 80;

        location /static/ {
            # 静态资源在响应头里加 Expires,浏览器会缓存
            expires 7d;
            add_header Cache-Control "public, no-transform";
            # 关闭 access_log 减少 IO
            access_log off;
        }

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

 

gzip 压缩的效果通常非常显著——JSON 响应可以压缩到原来的 20%~30%,CSS/JS 可以压缩到 30%~40%。但注意:

图片(PNG、JPG、WebP)和视频、音频不要压缩——这些格式本身就是压缩格式,gzip 反而徒增 CPU

太小的文件(< 1KB)不值得压缩,压缩开销可能大于节省的传输量

不要对已压缩的内容二次压缩

验证方法

 

# 用 curl 的 range 头或 Accept-Encoding 测试压缩
curl -I -H "Accept-Encoding: gzip" http://localhost/api/data 2>/dev/null
# 看响应头是否有:
# Content-Encoding: gzip
# Vary: Accept-Encoding

# 看原始响应大小 vs 压缩后大小
# 先获取原始大小
curl -s http://localhost/api/data | wc -c

# 获取 gzip 后大小
curl -s -H "Accept-Encoding: gzip" http://localhost/api/data | wc -c

# 用 ab 压测,对比开启/关闭 gzip 的 QPS
ab -n 1000 -c 10 -H "Accept-Encoding: gzip" http://localhost/api/data

# 查看 gzip 统计(需要编译时带了 --with-http_gzip_static_module)
# 在 error_log 中会有相关统计输出

 

风险提醒

gzip_comp_level 超过 6 之后,压缩率提升非常有限,但 CPU 消耗成倍增加。生产环境慎用高压缩级别。

有些老版本 CDN 或代理服务不理解 gzip Vary 头,可能导致缓存错乱。如果使用了 CDN,需要确认 CDN 是否支持 Vary: Accept-Encoding。

gzip 对 CPU 的消耗在高并发场景下不可忽视。如果服务器 CPU 本来就很紧张,可以适当降低 gzip_comp_level 或减少 gzip_types。

踩坑点七:SSL/TLS 配置不完整

现象

用户在浏览器访问网站时,看到「您的连接不是私密连接」或者证书错误页面。用 curl -v https://example.com 显示证书链不完整,或者只配了证书文件没有配中间证书(chain cert)。又或者虽然配置了 SSL,但用的加密套件是 TLS 1.0,已被主流浏览器标记为不安全。

根因

HTTPS 配置看似简单,但坑很多。常见的错误包括:

证书链不完整:只配了公钥证书(server.crt),没有配中间证书(chain.cert)。浏览器在验证证书链时找不到中间 CA,就会报错。

私钥和证书不匹配:证书是用另一个私钥签发的。

使用了不安全的协议版本:启用了 SSLv3、TLS 1.0、TLS 1.1,这些已被废弃。

使用了不安全的加密套件:比如 RC4、3DES,或者允许 NULL 加密。

证书文件路径不存在:Let's Encrypt 证书续期后路径变了,但 Nginx 配置没更新。

正确配置

一个安全且完整的 HTTPS 配置:

 

server {
    listen 443 ssl http2;
    server_name example.com;

    # 证书文件(公钥 + 中间证书要合并在一个文件里,或者分别指定)
    ssl_certificate /etc/nginx/ssl/example.com.fullchain.pem;
    # 私钥文件
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # TLS 版本控制,禁用不安全的旧版本
    ssl_protocols TLSv1.2 TLSv1.3;

    # 安全加密套件配置(推荐使用 Mozilla 的现代套件)
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256ECDHE-ECDSA-AES256-GCM-SHA384ECDHE-ECDSA-CHACHA20-POLY1305DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

    ssl_prefer_server_ciphers on;

    # 开启 OCSP Stapling,加快证书验证速度
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # 证书有效期提醒(可选项,Nginx 1.19.10+ 支持)
    # ssl_conf_command CertificateSpkiCheck on;

    # HSTS 头(严格传输安全,启用前确保全站 HTTPS)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # 其他安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    location / {
        root /data/www;
        index index.html;
    }
}

# HTTP 到 HTTPS 的强制跳转
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

 

关于证书链的合并:

 

# 正确的顺序:服务器证书 -> 中间证书 -> 根证书(根证书可选)
# Let's Encrypt 证书合并方式:
cat /etc/letsencrypt/live/example.com/fullchain.pem > /etc/nginx/ssl/example.com.fullchain.pem
cat /etc/letsencrypt/live/example.com/privkey.pem > /etc/nginx/ssl/example.com.key.pem

# 验证证书链是否完整:
openssl s_client -connect example.com:443 -servername example.com
# 看 "Certificate chain" 部分是否完整

 

验证方法

 

# 检查证书信息
openssl s_client -connect localhost:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -subject -issuer

# 检查证书链
openssl s_client -connect localhost:443 -showcerts /dev/null | grep -E "subject=|issuer="

# 使用 curl 验证(curl 会检查证书链完整性)
curl -v https://localhost/api/ 2>&1 | grep -E "SSL|HTTP|Server:

# 使用 testssl.sh(全量 TLS 安全检测工具)检查所有加密套件和协议版本
testssl.sh --protocols --ciphers --headers https://example.com

# 浏览器开发者工具 -> Security 面板 -> 查看证书详情
# 在 Chrome 中访问 chrome://flags 搜索 "Certificate" 看有没有相关警告

# 检查 SSL Labs 评分(在线工具)
# 访问 https://www.ssllabs.com/ssltest/ 输入域名查看评级

 

风险提醒

购买或续期证书后,一定要测试 openssl s_client -connect 确认证书链完整再上线

启用 HSTS(Strict-Transport-Security)后,用户浏览器会强制 HTTPS,且 max-age 设置很长。如果证书有问题,影响范围会很大。建议先用较短的 max-age 测试,确认无误后再放大。

TLS 1.3 在 Nginx 1.13+ 支持,但如果 client 是旧版 Android 或 Java 6/7,可能不支持。如果业务用户群体中有这种情况,需要保留 TLS 1.2。

踩坑点八:worker_processes 与 worker_connections 配合错误

现象

Nginx 配置里 worker_connections 设置得很大,比如 65535,但实际并发量稍微高一点就开始 502。用 netstat 或 ss 查看连接数,明明远远没到 65535,Nginx 却报 "too many connections"。这是系统层的文件描述符限制没有同步调整导致的。

根因

Nginx 每个 worker 进程能够处理的最大连接数由 worker_connections 控制(默认 512)。但这个值受限于操作系统的文件描述符上限(ulimit -n)。如果系统的文件描述符上限只有 1024,而 Nginx 的 worker_connections 设置的是 65535,Nginx 实际能打开的连接数也只有 1024 左右。

同时,Nginx 处理连接时,每个连接除了占用一个文件描述符,还需要分配一定的内存。如果内存不足,大并发也会出问题。

正确配置

分两层配置:

Nginx 配置层:

 

# /etc/nginx/nginx.conf

worker_processes auto;  # 自动等于 CPU 核心数
worker_rlimit_nofile 65535;  # 允许每个 worker 打开的最大文件描述符数

events {
    # 每个 worker 的最大连接数,受限于系统 ulimit -n
    worker_connections 65535;
    # 使用 epoll(Linux)提高并发处理效率,FreeBSD 用 kqueue
    use epoll;
    # 允许一次接受多个连接
    multi_accept on;
}

http {
    # 打开文件缓存,减少磁盘 IO
    open_file_cache max=65535 inactive=60s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # 客户端keepalive超时
    keepalive_timeout 65;
    # 单客户端最大请求数(防止单个客户端占用过多连接)
    keepalive_requests 1000;
}

 

系统配置层:

 

# 查看当前文件描述符限制
ulimit -n

# 临时修改(立即生效,但重启后失效)
ulimit -n 65535

# 永久修改(编辑 /etc/security/limits.conf)
# 在文件末尾添加:
# * soft nofile 65535
# * hard nofile 65535
# root soft nofile 65535
# root hard nofile 65535

# 编辑 /etc/sysctl.conf(修改网络参数)
echo"fs.file-max = 1000000" >> /etc/sysctl.conf
sysctl -p

# 编辑 /etc/pam.d/common-session(确保 PAM 读取 limits.conf)
# 确认有:session required pam_limits.so

# 修改 nginx systemd 服务文件(如果用 systemd 管理)
# /lib/systemd/system/nginx.service
# 在 [Service] 段添加:
# LimitNOFILE=65535
# 然后执行:systemctl daemon-reload && systemctl restart nginx

 

系统层面的完整调优:

 

# /etc/sysctl.conf 网络相关参数
cat >> /etc/sysctl.conf << 'EOF'
# 网络连接追踪
net.netfilter.nf_conntrack_max = 1048576
net.nf_conntrack_max = 1048576

# TIME_WAIT 复用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1

# 监听队列长度
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535

# 本地端口范围
net.ipv4.ip_local_port_range = 1024 65535

# 内存优化(视实际情况)
net.ipv4.tcp_mem = 786432 1048576 1572864
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
EOF

sysctl -p

 

验证方法

 

# 查看 Nginx worker 进程的文件描述符使用情况
ps -p $(pgrep nginx | head -1) -o pid,comm,nlwp,drs
# nlwp = number of light-weight processes/threads

# 查看实际打开的文件描述符数量
ls /proc/$(pgrep -f "nginx: worker" | head -1)/fd | wc -l

# 查看系统级别的文件描述符限制
cat /proc/sys/fs/file-max
ulimit -n

# 压测验证配置生效
# 用 ss 观察并发连接数上限
ss -s
# 或 ab 压测
ab -n 10000 -c 5000 http://localhost/api/test

# 观察 error_log 中是否有 "too many connections" 错误
grep "too many connections" /var/log/nginx/error.log

 

风险提醒

文件描述符设置过大可能导致系统内存泄漏或耗尽。每个文件描述符在 Linux 内核中都有对应的 struct file 结构,约占 2KB 内存。设置 65535 个约需 130MB 物理内存。

somaxconn 设置过大可能在遭受 SYN Flood 攻击时放大攻击效果。根据实际业务调整。

系统层面的修改需要 root 权限,修改前务必记录原值,修改后立即验证,异常时能快速回滚。

踩坑点九:日志配置不当导致磁盘爆满

现象

服务器突然变得很慢,SSH 登录后敲命令卡顿。用 df -h 一看,根分区 100% 满了。进一步排查发现 /var/log/nginx/ 下的 access.log 或 error.log 达到了几十 GB。上一次 logrotate 执行不知道什么时候,或者是 logrotate 配置根本没有生效。

根因

Nginx 的 access_log 默认对每个请求都写一行,高并发场景下日志量增长极快。如果 access_log 没有配置合理的轮转策略,磁盘空间迟早会被耗尽。

常见问题:

access_log 记录了太多无用信息(特别是启用了详细的 log_format 包含大量 header)

error_log 级别设置为 debug,产生了巨量调试日志

logrotate 配置了但频率太低(每天一次,而日志每小时就能写满磁盘)

日志被写到了系统根分区而不是独立挂载的日志分区

压缩后的旧日志没有被删除

正确配置

Nginx 日志配置:

 

http {
    # 定义日志格式(不要记录过长的 header 内容)
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';

    # 访问日志(生产环境建议按虚拟主机拆分)
    access_log /var/log/nginx/access.log main buffer=16k flush=2m;

    # 错误日志(生产环境不要用 debug,会写大量日志)
    error_log /var/log/nginx/error.log warn;

    # Gzip 压缩日志(减少日志体积)
    # 需要在 http 或 server 段开启
    # gzip off;  # 如果不需要 gzip 日志可以关闭
}

server {
    server_name example.com;
    access_log /var/log/nginx/example.com.access.log main;
    error_log /var/log/nginx/example.com.error.log;

    # 关闭不需要的日志,减少 IO
    location /health {
        access_log off;
        return 200 "OK";
    }

    # 静态资源可以关闭 access_log
    location /static/ {
        root /data/www;
        access_log off;
        expires 7d;
    }
}

 

logrotate 配置:

 

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily              # 每天轮转一次
    missingok          # 日志不存在也不报错
    rotate 14          # 保留 14 份旧日志
    compress           # 压缩旧日志(gzip)
    delaycompress      # 延迟压缩,等下一次轮转再压缩
    notifempty         # 空日志不轮转
    create 0640 nginx nginx  # 轮转后创建新文件的权限
    sharedscripts      # 所有日志轮转完再执行一次 postrotate

    # 向 master 进程发 reload 信号,而不是 restart
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

 

定时清理过大的日志文件(紧急处理用):

 

# 如果磁盘已经满了,先紧急清理日志释放空间
# 1. 先找到最大的日志文件
du -sh /var/log/nginx/*.log | sort -rh | head -10

# 2. 截断日志文件(不是删除,避免 Nginx 还在写这个 inode)
# 危险操作,先确认你要截断的是哪个文件
truncate -s 0 /var/log/nginx/access.log

# 3. 如果 Nginx 还在写,用 kill -USR1 通知它重新打开日志
kill -USR1 $(cat /var/run/nginx.pid)

# 4. 确认磁盘空间释放
df -h /var/log

 

验证方法

 

# 检查日志轮转是否正常工作
logrotate -d /etc/logrotate.d/nginx  # 模拟执行(不实际轮转)

# 检查日志大小
ls -lh /var/log/nginx/

# 检查磁盘使用情况(按大小排序)
du -ah /var/log/ | sort -rh | head -20

# 监控系统日志目录的大小增长
watch -n 5 "df -h /var/log; ls -lhS /var/log/nginx/*.log"

# 开启日志后定期检查是否有异常(比如某个 IP 频繁请求导致日志暴增)
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 统计访问量最大的 IP,如果某个 IP 请求量异常大,可能是被爬或被攻击

 

风险提醒

绝对不要 rm 删除正在被 Nginx 写入的日志文件——删除后 Nginx 会继续写这个已被删除的 inode,导致磁盘空间持续消耗且无法通过删除文件释放。正确做法是 truncate -s 0 或 > /var/log/nginx/access.log。

设置 logrotate 时确认 postrotate 里发的是 -USR1 信号而非 -HUP 或 restart。-USR1 让 Nginx 重新打开日志文件,不中断现有连接;-HUP 会重载配置,可能触发 worker 进程重启。

access_log 关掉后无法做流量分析和异常排查,建议只关静态资源的日志,保留 API 和核心业务的 access_log。

踩坑点十:隐藏 server 块版本号未生效

现象

用 curl -I http://example.com 或浏览器开发者工具查看响应头,发现 Server: nginx/1.18.0(或具体版本号)。渗透测试报告里指出这是信息泄露漏洞——攻击者可以据此判断 Nginx 版本,从而查找对应的 CVE 漏洞。

你明明在 nginx.conf 里配置了 server_tokens off;,但版本号还在显示。

根因

server_tokens off; 的作用范围是有限的,它只关闭 Nginx 在 error_page 响应和 Location 响应头中的版本号显示,但如果你通过 include /etc/nginx/conf.d/*.conf 加载了其他 server 块文件,或者使用了第三方模块(如 OpenResty),配置可能不生效。

另一个常见问题是:server_tokens off 在 http 段配置了,但 server 段里又重写了 error_page,或者 upstream 响应头里还带着版本信息。

正确配置

基础配置:

 

http {
    # 关闭版本号显示(在 error 页面和响应头中都生效)
    server_tokens off;

    # 如果需要完全自定义 Server 头,可以用这个(需要 ngx_http_headers_more 模块)
    # more_set_headers 'Server: MyServer';

    server {
        listen 80;
        server_name example.com;

        # 自定义错误页面,同时隐藏版本
        error_page 404 /404.html;
        error_page 500 502 503 504 /50x.html;

        location / {
            root /data/www;
        }

        # 自定义错误页面内容,避免暴露 Nginx 版本
        location = /50x.html {
            root /data/www/errors;
        }
    }
}

 

完全隐藏 Nginx 标识(需要编译第三方模块):

Nginx 默认的 Server 头由 HttpHeadersMore 模块控制,但开源版 Nginx 本身不提供关闭 Server 头的方法。如果需要完全隐藏,可以:

使用 OpenResty 的 header 电子:

 

# 需要安装 OpenResty 或 ngx_http_headers_more 模块
header_filter_by_lua_block {
    ngx.header.server = "MyServer"
}

 

或者在代码层面——后端服务在响应头里覆盖:

 

# 后端服务返回的响应头会带上自定义 Server
proxy_set_header Server "MyServer";

 

编译时修改:

 

# 编译 Nginx 时指定默认 Server 头
./configure --with-http_realip_module 
            --with-http_stub_status_module 
            --http-client-body-temp-path=/var/lib/nginx/client-body 
            --http-proxy-temp-path=/var/lib/nginx/proxy 
            --with-http_gzip_static_module 
            --add-module= # 第三方模块路径

# 编译后修改 Objs/nginx.c 中的默认字符串
# 查找 "ngx_http_server_string[]" 并修改 "nginx/" 为自定义值

 

验证方法

 

# 查看响应头中的 Server 字段
curl -I http://localhost/ 2>/dev/null | grep -i server
# 期望输出只有 Server: nginx,没有版本号

# 测试 error_page 响应是否还带版本号
curl -s http://localhost/nonexistent_path | grep -i "server|nginx"
# 期望:不包含 nginx 版本号

# 检查是否有其他地方泄露了版本信息
# 比如 upstream 返回的响应头
curl -I http://localhost/api/ 2>/dev/null | grep -i server
curl -s http://localhost/api/ -H "Accept: text/html" | grep -i "nginx|server"

# 用 nikto 扫描(Nginx 指纹识别工具)
nikto -h http://localhost -Friendly

 

风险提醒

完全隐藏 Server 头在实际安全收益上有限——有经验的攻击者通过 SSL 证书、对特定路径的响应特征等方式依然可以识别出 Nginx。但如果合规要求(如等保)要求隐藏版本号,这是必要的配置。

如果使用 OpenResty 或第三方模块,确保该模块来源可信,避免引入新的安全风险。

修改 Server 头后,如果业务依赖第三方安全扫描工具判断 Web 服务器类型,可能导致扫描工具误报。记得同步更新资产清单。

综合排查路径:502/504 故障排查流程

当 Nginx 返回 502 Bad Gateway 或 504 Gateway Timeout 时,按以下路径逐层排查:

排查路径图

 

Nginx 502/504
  ├── 1. 检查 upstream 是否存活
  │     ├── curl 本地测试后端端口是否响应
  │     │     curl http://127.0.0.1:8080/health
  │     └── 检查 upstream 进程状态和端口监听
  │           ps aux | grep backend
  │           ss -tlnp | grep 8080
  │
  ├── 2. 检查 Nginx 与 upstream 之间的网络连通性
  │     ├── telnet 127.0.0.1 8080
  │     ├── ping 127.0.0.1
  │     └── iptables/selinux 是否拦截
  │
  ├── 3. 检查 upstream 连接数和超时配置
  │     ├── upstream keepalive 是否足够
  │     ├── proxy_connect_timeout 是否太小
  │     ├── proxy_read_timeout 是否太小
  │     └── upstream 服务的连接池是否耗尽
  │
  ├── 4. 检查 upstream 进程/容器状态
  │     ├── 进程是否 OOM 被杀
  │     │     dmesg | grep -i oom
  │     │     journalctl -u backend --since "10 minutes ago"
  │     ├── 容器是否被重启
  │     │     docker ps -a | grep backend
  │     └── Kubernetes: pod 状态
  │           kubectl get pods -n default | grep backend
  │
  ├── 5. 检查 error_log 中的具体错误信息
  │     ├── "connect() failed"
  │     ├── "Connection refused"
  │     ├── "Connection timed out"
  │     └── "no live upstreams"
  │
  └── 6. 检查 Nginx upstream 配置是否正确
        ├── upstream 块中的 server 地址是否正确
        ├── proxy_pass 是否指向了正确的 upstream
        └── upstream 是否所有 server 都 down 了

 

常用诊断命令汇总

 

# 一分钟内快速诊断 502 问题
echo"=== 1. Nginx error_log ===" && tail -100 /var/log/nginx/error.log | grep -i "502|upstream|connect"

echo"=== 2. Nginx upstream 进程状态 ===" && ps aux | grep -E "java|node|python|php" | grep -v grep

echo"=== 3. 端口监听状态 ===" && ss -tlnp | grep -E "8080|3000|5000|9000|3306"

echo"=== 4. 本地 upstream 健康检查 ===" && curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/health

echo"=== 5. 系统资源 ===" && free -h && df -h / && uptime

echo"=== 6. 近期系统日志(OOM 等)===" && dmesg | tail -50 | grep -iE "oom|killed|nginx|java|node"

 

特殊状态码处理

状态码 含义 常见原因 排查命令
400 Bad Request 请求头过大、语法错误 grep "400" /var/log/nginx/error.log
413 Request Entity Too Large client_max_body_size 超限 增大 client_max_body_size
444 Nginx 特有,连接直接关闭 被配置拦截,未返回响应 常见于配置了 if (condition) { return 444; }
499 Client Closed Request 客户端主动断开,upstream 处理过慢 增加 proxy_read_timeout
502 Bad Gateway upstream 进程挂了或无响应 逐层检查 upstream 进程和端口
503 Service Temporarily Unavailable upstream 达到 max_fails 暂时不可用 等 30s 自动恢复或检查 fail_timeout
504 Gateway Timeout upstream 处理超时 增加 proxy_read_timeout / proxy_send_timeout

生产环境变更 checklist

每次 Nginx 配置变更前,必须完成以下检查项:

变更前

 

# 1. 备份当前配置
cp -r /etc/nginx /etc/nginx.bak.$(date +%Y%m%d%H%M%S)

# 2. 在测试环境验证语法
nginx -t -c /etc/nginx/nginx.conf

# 3. 确认变更影响的站点和路径
grep -n "proxy_pass|upstream|listen" /etc/nginx/conf.d/*.conf

# 4. 确认新配置中的 IP、端口、路径没有错误
# 特别是 upstream 的 server 地址和端口

# 5. 准备回滚脚本
cp /etc/nginx/nginx.conf /tmp/nginx.conf.backup

 

变更中

 

# 1. 部署新配置文件
cp /path/to/new/nginx.conf /etc/nginx/nginx.conf

# 2. 验证语法
nginx -t

# 3. reload 而非 restart
nginx -s reload

# 4. 确认 master 进程还在,worker 进程正常
ps aux | grep nginx

# 5. 立即观察 error_log
tail -f /var/log/nginx/error.log

 

变更后

 

# 1. 全量健康检查(测试所有站点)
curl -s -o /dev/null -w "%{http_code} %{url_effective}
" 
    http://localhost/ 
    http://localhost/api/ 
    http://localhost/static/

# 2. 监控 502/504 错误率(变更后 5 分钟内每分钟检查一次)
for i in {1..5}; do
    echo"=== Check $i ==="
    grep -c "502|504" /var/log/nginx/access.log
    sleep 60
done

# 3. 确认新旧 worker 进程平滑切换(旧 worker 优雅退出)
ps aux | grep "nginx: worker" | wc -l
# 确认 worker 数量等于配置中的 worker_processes * worker_connections 的 worker 数

# 4. 如果有问题,立即回滚
cp /tmp/nginx.conf.backup /etc/nginx/nginx.conf
nginx -t && nginx -s reload

 

回滚方案

 

#!/bin/bash
# rollback_nginx.sh - 紧急回滚脚本

BACKUP_FILE="/tmp/nginx.conf.backup"
NGINX_CONF="/etc/nginx/nginx.conf"

if [ ! -f "$BACKUP_FILE" ]; then
    echo"ERROR: Backup file not found: $BACKUP_FILE"
    exit 1
fi

echo"Rolling back Nginx configuration..."
cp "$BACKUP_FILE""$NGINX_CONF"

if nginx -t 2>&1 | grep -q "syntax is ok"; then
    nginx -s reload
    echo"Rollback successful. Nginx reloaded."
else
    echo"ERROR: Rollback config failed syntax check. Manual intervention required."
    exit 1
fi

 

总结

Nginx 配置的十个踩坑点,总结起来有一个共同规律:Nginx 的配置项之间不是孤立的,而是相互影响、相互制约的。proxy_pass 的尾部斜杠影响路径传递,location 的修饰符决定匹配顺序和正则行为,worker_processes 和系统 ulimit 共同决定实际并发上限,gzip 压缩和 CPU 消耗是一对矛盾体。

一个合格的运维工程师,不仅要记住每个配置项怎么写,更要理解配置项之间的协作关系。以下几点核心原则,贯穿了所有的踩坑场景:

原则一:先测试后上线。nginx -t 能排除大部分语法错误,但不是所有。上线前在测试环境完整跑一遍业务流程,用 curl 覆盖所有关键路径。

原则二:小步快走,留好回滚。 每次变更只改一个配置项,改完后立即验证。多个配置项一起改,出问题都不知道是哪一项导致的。

原则三:关注资源边界,不只看配置值。 worker_connections 再大,系统 ulimit 不够也用不上;client_max_body_size 再大,磁盘满了也存不了。系统层和 Nginx 配置层要一起调。

原则四:日志是运维的眼睛。 access_log 和 error_log 不只是出问题时才看,日常要看基线、观趋势,发现异常苗头及时处理。

原则五:安全配置要完整。 HTTPS 证书链、TLS 版本、加密套件、版本隐藏,这些不只是安全合规要求,也是运维的基本功。

Nginx 的配置不算复杂,指令也就几十个,但用对、用好、用稳,需要在实践中不断积累经验。每一个踩过的坑,都是真实生产环境里的教训。希望这十个场景能帮你少走弯路,遇到问题时能快速定位、精准修复。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分