LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

同事改了一行Nginx配置,大促当天系统崩—这5个教训,每一个都是真金白银换来的

admin
2026年3月27日 15:56 本文热度 51

本文是「Nginx 避坑」最后一篇


先说一句:这篇有点长,但建议你认真读完。因为这里说的每一个坑,都是在生产上真实发生过的事故,有的是我自己踩的,有的是我帮人排查了两个小时之后才找到的。看完之后,你大概率能在自己的配置里找到几个正在等着爆炸的隐患。

这些 Nginx 反向代理的坑,我替你踩完了(反向代理与负载均衡)

https://oa22.cn/bbs.asp?id=38197

你的Nginx正在裸奔:配了HTTPS却从没真正安全过

https://oa22.cn/bbs.asp?id=38196

写了几年Nginx配置,这些错误我见过N多次了

https://oa22.cn/bbs.asp?id=38195

同事改了一行Nginx配置,大促当天系统崩—这5个教训,每一个都是真金白银换来的

https://oa22.cn/bbs.asp?id=38194


那是一次大促。

倒计时结束,流量开始涌入。监控大屏的曲线往上走,看起来一切正常——直到第十分钟。

后端机器的CPU曲线集体开始往上飙。不是一台,是所有机器同时飙。请求超时告警像刷屏一样涌过来,客服那边的电话已经响了。

所有人盯着屏幕,没人知道发生了什么,因为代码没动、配置没变、容量也是提前评估过的

最后定位到的根因,只有六个字:keepalive没有配。

Nginx跟后端之间用的是短连接。大促一开闸,每一个请求都要新建TCP连接,后端的accept队列被打满,CPU全扑在握手上,业务处理能力直线下降,然后请求堆积,然后雪崩。

加上这几行配置只需要五分钟:

upstream backend {
    server192.168.1.10:8080;
    keepalive64;            # 每个 worker 保持的空闲长连接数
    keepalive_requests1000# 单连接最多处理请求数
    keepalive_timeout60s;   # 空闲连接超时时间
}

location / {
    proxy_pass http://backend;
    proxy_http_version1.1;          # HTTP/1.1 才支持 keepalive
    proxy_set_header Connection "";  # 清掉 Connection: close,让后端维持连接
}

但我们是在损失了百万流水之后才加上去的。


事故从来不是代码写坏了,往往只是一个参数没配,或者配了个错的。

今天这篇,把四篇系列里含金量最高的部分集中在一起:进阶配置、Docker/K8s特有问题、热升级姿势,以及五个真实事故从现象到根因到改法的完整复盘。每个坑都附上「正确做法」,对照着把自己的配置检查一遍。


01 日志里没有响应时间?出了事你都不知道慢在哪

Nginx默认的日志格式,你翻一天也找不到响应时间。请求进来了,状态码是什么,body多大——但完全不知道花了多久,慢在哪个环节。

出了性能问题,这种日志等于没有。

正确做法:自定义日志格式,把这四个字段加进去。

log_format detail '$remote_addr [$time_local] "$request$status '
                  'rt=$request_time '
                  'uct=$upstream_connect_time '
                  'uht=$upstream_header_time '
                  'urt=$upstream_response_time '
                  '"$http_x_forwarded_for"';

access_log /var/log/nginx/access.log detail;

搞清楚这四个字段的关系,排查问题能少走很多弯路:

  • rt
    (request_time):用户感知到的总延迟
  • uct
    (upstream_connect_time):建立到后端连接花了多久
  • uht
    (upstream_header_time):后端收到请求到开始回响应头
  • urt
    (upstream_response_time):整个后端处理时间

排障公式rt远大于urt→问题在发送响应的网络;urt高但uct低→后端业务慢;uct本身就高→后端连接队列有压力,或者网络有问题。

有了这四个字段,很多"不知道慢在哪"的问题,一条日志就能定位到方向。

另外,日志切割也是生产必选。高流量系统access_log一天几十GB很正常,不切割磁盘迟早撑爆。用logrotate做每日切割,切完发USR1信号重新打开文件——注意是USR1,不是reload,发reload会重新加载配置,长连接会被影响。


02 WebSocket连接隔几分钟就断?八成是这两个地方没配

做过实时功能的同学大概都遇到过:WebSocket连接建立没问题,但过一会就断了,然后客户端重连,再断,反复循环。

最常见的原因一:超时时间没改。 Nginx的proxy_read_timeout默认60秒。WebSocket是长连接,可能几分钟甚至十几分钟没有数据(靠心跳维持),60秒一到,Nginx直接断掉。

最常见的原因二:协议升级头没配齐。 很多人只加了Upgrade头,没加Connection头,或者Connection头写死了固定值,导致普通HTTP请求也受影响。

正确的完整配置:

# 在 http 块加这个 map,动态判断是否需要升级协议
map$http_upgrade$connection_upgrade {
    default upgrade;
    ''      close;  # 普通 HTTP 请求没有 Upgrade 头,Connection 用 close
}

location /ws/ {
    proxy_pass http://websocket_backend;

    # 协议升级三件套(缺一不可)
    proxy_http_version1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 超时一定要调长,根据你的心跳间隔来定
    proxy_read_timeout3600s;
    proxy_send_timeout3600s;
}

还有一个经常被忽略的细节:WebSocket加了负载均衡,连接会乱跳。 WebSocket是有状态的,同一用户的连接被轮询到另一台后端,上下文就没了。

短期解法是upstream用ip_hash,但在用户走NAT或CDN的场景下会退化成单点。真正推荐的方案是让后端无状态化:把连接上下文存Redis,后端不保存任何连接状态,这样轮询分到哪台都没关系,还能横向扩展。


03 Docker里Nginx莫名其妙吃内存?查一下这个参数

容器里用Nginx有一个很隐蔽的坑。

你给容器分配了2个CPU,但Nginx配的是worker_processes auto。结果Nginx读了宿主机的/proc/cpuinfo,看到64核,于是启动了64个worker进程。内存直接上去了,而且这64个worker互相竞争资源,效果反而比2个worker更差。

正确做法:在启动脚本里动态注入核心数。

#!/bin/sh
# entrypoint.sh
# nproc 在容器里会正确读到 cgroup 分配的 CPU 数,不是宿主机核心数
CORES=$(nproc)
sed -i "s/worker_processes auto/worker_processes ${CORES}/" /etc/nginx/nginx.conf
exec nginx -g "daemon off;"

注意最后要用exec启动,让Nginx直接成为PID 1。如果用普通方式启动,PID 1是shell,容器收到SIGTERM时shell不会把信号转发给Nginx,在途请求全部强制中断,你以为是优雅退出,其实不是。

K8s里还有另一个坑:DNS缓存导致upstream失效。 Nginx启动时解析了Service域名,缓存起来,之后Pod滚动更新,IP变了,但Nginx还在往老IP打流量,502一直报。

解法是用resolver指令配合变量形式的proxy_pass,强制每次请求都重新走DNS:

resolver kube-dns.kube-system.svc.cluster.local valid=10s;

location / {
    set $backend "my-service.default.svc.cluster.local:8080";
    proxy_pass http://$backend;  # 变量形式,不缓存 DNS 结果
}

04 `nginx -s reload`你用了多少年?它其实不是完全无损的

很多人以为reload是无损操作。对于普通HTTP短连接来说,确实几乎无感知。但如果你有WebSocket、大文件下载、长轮询这类长连接——

reload之后,老的worker进程会等待已有连接处理完,但等待有上限。 超过worker_shutdown_timeout,老worker强制退出,正在处理的长连接被中断。

正确做法:根据业务连接时长配这个参数,并在低峰期做变更。

# 老 worker 最多等待多久(有长连接的服务要设长一点)
worker_shutdown_timeout 3600s;

热升级(不停服替换Nginx二进制)的步骤也有讲究,很多人在这里犯错:

# 正确的热升级步骤
OLD_PID=$(cat /var/run/nginx.pid)

# 1. 启动新版本 master,新旧两套共存
kill -USR2 $OLD_PID

# 2. 老 worker 优雅退出,不再接新连接
kill -WINCH $OLD_PID

# 3. 确认新版本正常后,关掉老 master
kill -QUIT $(cat /var/run/nginx.pid.oldbin)

# 如果新版本有问题需要回滚:
# kill -HUP $(cat /var/run/nginx.pid.oldbin)  # 重启老 worker
# kill -QUIT $(cat /var/run/nginx.pid)         # 关掉新 master

热升级中间绝对不能kill -9老进程 ——这会瞬间断掉所有老连接,完全失去热升级的意义。


05 五个真实事故,每一个事后看都"这么简单"

事故一:upstream写了个下线的IP,全站502持续五分钟。

有人手动改了生产配置,写了一个已经下线的机器IP,reload后Nginx开始往那台机器打流量,三次失败后被动健康检查才把它标记掉,期间所有请求都在超时。

最佳实践:配置变更走Git,不允许直接改生产文件;upstream写服务发现的域名,不写死IP;把健康检查参数收紧(max_fails=2 fail_timeout=10s),让发现时间从分钟级降到十几秒。


事故二:证书过期,全站挂了40分钟。

手动申请的年度证书,没设任何续期提醒。某天早上九点用户开始投诉,紧急申请、替换、reload,40分钟期间网站完全打不开。

最佳实践:能用Let's Encrypt自动续期就用,加上cron每天运行certbot renew;同时配独立的到期监控,提前30天告警,不能把"续期成功"作为唯一的保障。有条件就直接上云托管证书,自动续期、自动部署,什么都不用操心。

# 加到 crontab,每天检查一次
0 8 * * * DAYS=$(openssl x509 -in /etc/nginx/ssl/cert.pem -noout -enddate \
  | cut -d= -f2 | xargs -I{} date -d {} +%s \
  | xargs -I{} expr {} - $(date +%s) | xargs -I{} expr {} / 86400); \
  [ "$DAYS" -lt 30 ] && echo "证书还有${DAYS}天过期" | mail -s "SSL证书告警" ops@yourco.com

事故三:一个if指令,POST请求全部404,排查了两个多小时。

有同学想按请求方法把流量分到不同后端,在location里用if写了分流逻辑,结果POST请求走到了一个没有对应接口的后端,全部404。查了后端代码、路由、数据库,全部没问题,最后才想到去查Nginx配置。

最佳实践:Nginx的if在location块里行为极其反直觉,能不用就不用。按请求属性做流量分发,用map实现:

# ❌ 别这样写
location /api/ {
    if ($request_method = GET) {
        proxy_pass http://backend_a;  # 这里的行为不可预测
    }
    proxy_pass http://backend_b;
}

# ✅ 这样写
map$request_method$api_backend {
    GET     http://backend_a;
    default http://backend_b;
}
location /api/ {
    proxy_pass$api_backend;
}

事故四:gzip压了JSON,前端fetch拿到乱码。

Nginx对application/json开了gzip,gzip_proxied配成了any,会无条件压缩所有响应。前端的自定义fetch封装没带Accept-Encoding: gzip请求头,但收到了压缩响应,直接拿二进制解析JSON,全是乱码。

最佳实践:gzip是协商机制,服务端应该尊重客户端的声明。gzip_proxied不要设any,应该检查请求头:

# ❌ 无条件压缩
gzip_proxied any;

# ✅ 只对声明了支持 gzip 的客户端压缩
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;

事故五:就是开头说的那次大促——keepalive没配。

最佳实践:keepalive不是可选项,是高并发场景的基础配置。而且光配还不够,要在压测里验证。压测脚本要模拟真实的连接复用行为,不要只测接口吞吐量,要看TIME_WAIT的积累速度。大促前必须做压测,不能靠猜。


最后说一句

这些坑,事后看每一个都显而易见。但出事的时候,告警在刷屏、用户在投诉、领导在问,你根本没有时间静下来思考。

所以这些事要提前做。 在配置review的时候过一遍,在压测的时候验证一遍,在大促前的checklist里对照一遍。出了事才补,代价太高了。


👇 如果这篇对你有帮助,转发给还在踩这些坑的同事吧。


阅读原文:原文链接


该文章在 2026/3/27 16:37:36 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved