Nginx超时参数分层:五层设置与止损线

Nginx 的超时参数是运维里最容易被「随手调大」的东西。502 报错,把 proxy_read_timeout 从 60 调到 300;上游偶尔卡顿,把 fastcgi_read_timeout 调到 600。调完页面暂时不出错了,于是这次改动就被当成解决方案留在了配置里,一年之后没人记得当初为什么要这么写。

直到某天上线一个新版本,PHP 那边有个接口死锁了,请求全部挂在那里。正常应该 30 秒后网关返回 504 把连接释放掉,但因为读超时是 600 秒,六百秒内这些连接全都攥着 PHP-FPM 的 worker 不放。worker 池很快被占满,整个站点——注意,不只是那个死锁的接口,是整站——一起变成 504。这时候你才会意识到:超时不只是一个「等多久」的参数,它是你在故障发生时保护自己的止损线。

这篇把 Nginx 的超时拆成五层,讲清每一层在等什么、卡住时会发生什么、以及一台普通个人站服务器上到底该填多少。

一、先把超时分成五层,别混着调

Nginx 里跟时间有关的参数有几十个,但真正决定「一个请求能活多久」的只有五层,从上到下依次是:

  • 客户端连接层:client_header_timeout、client_body_timeout、send_timeout
  • 客户端请求体总时长:client_body_timeout 只管「两次读之间的间隔」,不是总时长,这个区别后面会讲
  • 与上游建立连接:proxy_connect_timeout(或 fastcgi_connect_timeout)
  • 与上游读写:proxy_send_timeout、proxy_read_timeout(FastCGI 是 fastcgi_send_timeout / fastcgi_read_timeout)
  • keepalive 空闲回收:keepalive_timeout、keepalive_requests

把这五层的语义分清之后,绝大多数「调了没用」和「调完更糟」都能解释。比如同样报 504,如果是 proxy_connect_timeout 触发,那说明上游连不上(进程没起、防火墙、端口错),调 proxy_read_timeout 一点用都没有,只是在延长痛苦。

二、连接超时必须短,读超时才可以长

这一条是整篇文章最重要的一句话:连接建立是「能不能连上」的问题,读超时是「连上了但没回话」的问题,两者的合理取值范围差了两个数量级。

连接本机的 PHP-FPM 或同机房的另一台机器,正常情况是毫秒级。给 3 到 5 秒已经非常宽容了——如果 3 秒还连不上,那不是慢,是挂了。所以:

proxy_connect_timeout 3s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

反过来,proxy_read_timeout 的默认值是 60 秒,很多人觉得太长就改成 10 秒,结果导出报表、生成站点地图这类本来就要跑 30 秒的接口全挂了。60 秒对绝大多数网站接口是合适的:正常接口响应在 500 毫秒以内,超过 60 秒基本可以断定这个请求出了问题,没必要继续等。

唯一该被单独放宽的是「确实需要长时间计算」的路由,比如后台的导出任务、图片批量处理。这种路由不该全局放宽,而是用 location 单独覆盖:

location ^~ /admin/export/ {
    proxy_read_timeout 900s;
    proxy_send_timeout 900s;
}

location / {
    proxy_read_timeout 60s;
}

这样即使导出接口卡死,被占用的也只是它自己那几个连接,不会把整站拖下水。

三、客户端层的三个超时:防的是慢速攻击,不是正常用户

client_header_timeout 和 client_body_timeout 的默认值都是 60 秒,作用对象是「正在往你这里发数据的客户端」。它们的语义经常被误读:这两个参数不是限制整个请求的总时长,而是限制「两次读操作之间的最大间隔」。

也就是说,一个客户端用极慢的速度每秒发 1 个字节,只要每个字节的间隔都没超过 60 秒,Nginx 就会一直陪着它——这正是 Slowloris 类慢速攻击的原理。它不需要带宽,只需要占着你的连接数。

所以这两个值应该往小调,个人站配 10 到 20 秒足够:

client_header_timeout 15s;
client_body_timeout   15s;
keepalive_timeout     30s;
send_timeout          30s;

另外还有一道更硬的闸门,很多人只配了超时没配它:

client_max_body_size   20m;
client_body_buffer_size 128k;
client_header_buffer_size 4k;

client_max_body_size 决定上传上限,超过直接 413 拒绝,比等它慢慢传完再报错便宜得多。如果站上有后台上传图片的需求,把它设成业务上限的 1.2 倍,别用默认的 1m 也别用 0(不限)。

四、上游卡住的时候,真正吃掉资源的是「等」

理解这一层很关键:Nginx 等上游的时候,自己不占多少 CPU,也不占 PHP 进程——但上游那一边是实打实被占用的。

假设 PHP-FPM 池有 20 个 worker,每个请求正常耗时 200 毫秒,理论上每秒能吃下 100 个并发请求。现在数据库慢查询导致每个请求要 30 秒,那么 20 个 worker 全部被占满之后,第 21 个请求只能在 socket 队列里排队,排 30 秒之后超时,用户看到 504。

这件事的传染性在于:worker 一旦被慢请求占满,连本该 20 毫秒返回的首页也会排队。一个后台报表接口能把整站的响应时间从 200 毫秒拉到 30 秒,这是小站最常见的「整站挂了」根因。

所以 proxy_read_timeout 的真正意义不是「给上游多少时间」,而是「最多允许一个坏请求占用上游资源多久」。60 秒意味着最坏情况下单个 worker 会被占用 60 秒,20 个 worker 意味着最坏情况全站不可用 60 秒。想清楚这个换算,你就知道为什么不该随手写 600。

配套要做的是让 Nginx 在超时的时候留下证据。proxy_next_upstream 只对多上游有用,单机场景更该关注日志:

log_format timeout_log '$remote_addr [$time_local] "$request" '
                       '$status $request_time $upstream_response_time '
                       '$upstream_addr';
access_log /var/log/nginx/access.log timeout_log;

$upstream_response_time 就是在告诉你「上游到底花了几秒」,它和你设的读超时对照着看,一眼能分清是上游慢还是客户端慢。

五、keepalive:省的是握手,坑的是并发上限

keepalive_timeout 决定一条空闲连接保留多久。keepalive_requests 决定一条长连接上最多复用多少次请求(默认 100)。这两个参数和前面四个不是一回事,但它们决定你的连接数账单。

反过来,向上游开 keepalive 时,还有一个极易配错的地方——upstream 块里的 keepalive 参数必须配合 proxy_http_version 1.1 和清空 Connection 头才生效:

upstream php_backend {
    server 127.0.0.1:9000;
    keepalive 32;
}

server {
    location ~ \.php$ {
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass http://php_backend;
    }
}

少了 proxy_set_header Connection "",Nginx 会把客户端的 Connection: close 透传给上游,上游每次都关连接,keepalive 32 等于白写。这个坑我踩过,症状是「配了 keepalive 但 TCP 连接数一点没降」。

六、一次真实的排查推演

把上面几层串起来看一个完整案例。某天下午三点,监控告警说站点 504 率上升到 8%,同时服务器负载从 0.3 涨到 6。

第一步看 Nginx 日志里 504 那一行的 $upstream_response_time:全是 60.0 秒上下,说明是 proxy_read_timeout 触发,且上游确实没在 60 秒内回应——不是客户端的问题,也不是连接问题。

第二步看 PHP-FPM 慢日志(request_slowlog_timeout 已经开着),发现卡住的都是同一个查询,涉及一张 300 万行的表,SQL 不带索引。

第三步看进程状态:ps aux | grep php-fpm | wc -l 显示 worker 数已经打满,pm.max_children 是 20,也就是说所有 worker 都在等这个 SQL。

到这里结论已经清楚:不是超时参数的问题,是慢 SQL 把池子占满了。正确的处置顺序是——先给这条查询加索引或者临时把该接口的下限保护打开(比如限制每日导出次数),让 worker 释放;然后再回头审视超时配置:proxy_read_timeout 从当时的 600 改回 60,因为 600 秒意味着故障发生时站点要瘫十分钟,而 60 秒只需要瘫一分钟。

如果当时超时就是 600,这次事故的持续时间会是现在的十倍。这就是为什么第一节说要把它当止损线看:它不能防止故障,但能决定故障的规模。

七、移动端与弱网:另一种「超时」

服务端超时解决的是「后端卡住」,还有一类 504 来自客户端——移动网络下的用户上传图片,传到一半信号断了,Nginx 那边看到的是一个已经半死不活的连接。client_body_timeout 设成 15 秒会在这里误伤:网速慢的用户每 20 秒才传完一个分片,会被判超时。

如果站点有移动端上传需求,稳妥的做法是前端分片上传(每片 1~2MB),让每片的传输时间远小于超时阈值;或者对上传路径单独放宽 client_body_timeout 60s,同时保留 client_max_body_size 作为硬上限。两件事一起做才安全:单靠放宽超时等于给慢速攻击开门,单靠限制体积会让正常用户传不上来。

另外提醒一句,CDN 层通常还有自己的一套超时(常见 30 秒到 60 秒回源超时),并且CDN 的超时不会因为你在 Nginx 里调大而变长。所以调整顺序永远是「先把 CDN 的值搞清楚,再配源站」,否则源站给 120 秒、CDN 60 秒就断开,用户看到的还是 504,你却以为源站没问题。

八、一份可以直接抄的配置与验收

综合前面的结论,一个普通个人站服务器(单机 Nginx + PHP-FPM + MySQL)的起点配置大概是下面这样,抄过去之后按业务微调:

http {
    client_header_timeout   15s;
    client_body_timeout     15s;
    keepalive_timeout       30s;
    send_timeout            30s;
    client_max_body_size    20m;

    proxy_connect_timeout   3s;
    proxy_send_timeout      60s;
    proxy_read_timeout      60s;

    fastcgi_connect_timeout 3s;
    fastcgi_send_timeout    60s;
    fastcgi_read_timeout    60s;

    # 必须配套:让慢请求留痕
    fastcgi_buffering on;
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
}

其中 fastcgi_buffering on 那一组值得单独说:如果后端返回的响应体超过缓冲区,Nginx 会写到临时文件;关掉 buffering 则每个请求都要长时间占着一个 Nginx 连接同步转发。小站开着 buffering 更省资源,但要注意 fastcgi_temp 目录的磁盘空间——日志把磁盘吃满的事故里,有相当一部分是临时文件堆出来的。

改完之后不要只做 nginx -t,那是语法检查,不验证行为。验收要跑三件事:

  1. 正常页面能在 1 秒内返回,curl -w '%{time_starttransfer}' 连打十次看波动范围;
  2. 故意请求一个不存在的上游端口,确认 3 秒左右就报 502 而不是等 60 秒;
  3. 用 ab 或 wrk 打 50 并发持续 60 秒,观察 php-fpm worker 数量和 Nginx active connections 是否能回到基线。

第三件事最重要:超时配置的正确性,只有在有压力的时候才看得出来。没有压力测试,你永远不知道 60 秒的读超时在真实故障里意味着什么。

九、小结

超时参数不是「越大越安全」,它是故障时的止损线。记住三条:连接超时给几秒,读超时给一分钟,客户端超时给十几秒;长时间任务用 location 单独放宽,别动全局;每改一个值,先算清楚「最坏情况下会有多少个上游进程陪它一起等」。把这三条落实到配置里,你会发现自己再也不需要为了压 502 去调参数了——因为大部分 502 和 504 的根因根本不在 Nginx 的超时设置上,而在于某个没有索引的查询、某个忘关的连接、或者某个每次都重新初始化的 worker。

Last modification:November 6th, 2026 at 09:27 pm

Leave a Comment