Py学习  »  NGINX

同事改了一行Nginx配置,大促当天喜提P0故障!

DBAplus社群 • 14 小时前 • 12 次点击  


那是一次大促。


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


后端机器的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特有问题、热升级姿势,以及五个真实事故从现象到根因到改法的完整复盘。每个坑都附上「正确做法」,对照着把自己的配置检查一遍。


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


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会重新加载配置,长连接会被影响。


二、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,后端不保存任何连接状态,这样轮询分到哪台都没关系,还能横向扩展。


三、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.confexec 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 结果}


四、 `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. 确认新版本正常后,关掉老 masterkill -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老进程 ——这会瞬间断掉所有老连接,完全失去热升级的意义。


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


事故一: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里对照一遍。出了事才补,代价太高了。


作者丨IT安装手册
来源丨公众号:IT安装手册(ID:itinstall1024)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/197171