前段时间线上碰到一个很奇怪的问题,部分导出接口偶尔返回 502,重试又大概率正常。后端服务本身日志没有报错,看网关层的状态码是 Nginx 抛出来的,一开始以为是后端超时,调大 proxy_read_timeout 之后,问题依旧存在。
先简单说下当时的链路:用户请求到 Nginx,Nginx 作为反向代理转发到后端应用,后端会生成一份比较大的文本响应,不是分片流式返回,是一次性组装好再输出。
翻 Nginx 的 error 日志,抓到关键一行:upstream sent too big header while reading response header from upstream。
很多人看到这个日志第一反应就是调大 proxy_buffer_size,我最开始也是这么干的,改完之后,短时间稳定,过几天又复现。
这里就有一个很容易混淆的点:proxy_buffer_size 控制的是响应头的缓冲区 ,而响应体的缓冲区,由 proxy_buffers 控制。
很多教程只说了 header 过大调 proxy_buffer_size,却很少讲清楚两者的分工。响应头本身一般不会特别大,真正容易爆缓冲区的,是响应体。
默认配置下,proxy_buffers 有数量和单块大小限制。当后端返回的响应体比较大,Nginx 来不及把内容转发给客户端,缓冲区写满之后,Nginx 就会主动断开和 upstream 的连接,直接返回 502。这个断开动作对后端来说是突然的连接关闭,后端应用很多时候感知不到异常,也就不会打印错误日志,这就是最头疼的地方 ------ 后端无报错,故障表象很隐蔽。
还有一个容易踩的误区:proxy_buffering。
默认它是 on,也就是开启缓冲。Nginx 会把后端返回的数据先存到缓冲区,再转发给浏览器。好处是后端可以尽快释放连接,客户端慢也不会拖住后端;坏处就是大响应场景下,缓冲区占用内存,一旦超出限制就断连。
有同学想解决导出、大文件这类场景,直接关掉 proxy_buffering。关掉之后,Nginx 不再缓存后端响应,收到数据直接透传给客户端。这个方案确实能避开缓冲区溢出的问题,但它有副作用:客户端网速很慢的时候,Nginx 和后端之间的连接会一直占用,后端连接会被长时间挂住,并发上来之后,后端连接数很快打满。所以它不是万能解药,只适合特定场景。
后面又发现另一个叠加问题:proxy_max_temp_file_size。
当缓冲区内存放不下全部响应,Nginx 会把超出部分写到临时磁盘文件。如果这个值设为 0,就禁止临时文件,所有数据都必须塞在内存缓冲区里,大响应直接触发断开。如果磁盘 IO 高、临时目录权限不足或者磁盘满,写临时文件也会失败,同样会出现随机 502。这种故障随机性很强,和当时的磁盘负载挂钩,复现概率很低。
梳理清楚之后,我们没有简单粗暴地把缓冲区参数无限调大。无限放大缓冲区本身是隐患,大响应并发上来,Nginx 内存会被吃光。而是按业务场景做了拆分:
普通查询接口,保持默认缓冲策略,适度调大 proxy_buffer_size,避免 header 溢出;
导出、大文本这类接口,单独在 location 里做配置:关闭 proxy_buffering,同时单独设置 proxy_read_timeout,拉长读取超时时间。
另外补充两个容易忽略的细节。
一是 proxy_temp_path,要保证这个目录单独挂载、有足够空间,不要和系统盘混在一起,磁盘满是很多线上隐形故障的源头。
二是不要把 proxy_buffers 的单块大小设置得特别大,单块太大,内存利用率会很差,大量小请求也会白白占用内存。一般是小块、多块的组合更稳妥。
还有一种情况,和这个很像但容易混淆:后端主动关闭连接。如果后端的容器有输出缓冲区限制,或者容器的输出流被强制截断,日志表现和 Nginx 缓冲区溢出很像,排查的时候要区分清楚:看 Nginx 日志里明确的提示文字,是 header 超限,还是读取响应体的时候连接被重置。
Nginx 代理的缓冲区参数,属于平时很少改、一出问题就很难定位的一类配置。很多人只记住超时参数,忽略缓冲这一套。它的核心矛盾就是:Nginx 想做中转缓冲提升吞吐,但是大响应场景下,缓冲机制本身就会变成瓶颈。
最好的方案其实不是堆缓冲区参数,业务层面优先改成流式输出,后端边生成边返回,不要等全部数据组装完成再一次性吐出。从根源上减少单块超大响应,这比调 Nginx 参数靠谱得多。参数调整只能当作临时兜底手段。