接口是流式的,前端拿到一个字符就往页面上打一个。上线之后,回答先在服务端憋十几秒,然后整段一次性蹦出来。本地直连后端是正常的,一个字一个字往外挤。中间只多了一层nginx,配置是照抄的默认值。默认值这三个字,就是问题所在。
nginx的proxy_buffering默认是on,它会把上游返回的数据先收进自己的缓冲区。缓冲区大小由proxy_buffers定,默认八个四k或八k,看平台。缓冲区填满一块才往下游发一块,收完才发最后那一块。上游是SSE这种长连接,数据稀稀拉拉地来,一块缓冲区几个钟头都填不满。于是浏览器那边一直等着,等到连接关闭,所有内容一次到达。效果和没有流式一样,还多占了一份内存。time_starttransfer这个指标会直接暴露它,首字节时间和总时间贴在一起,就是攒完才发的铁证。
把proxy_buffering设成off,一行就够。指令要落在处理这个请求的location里,写在server块顶层,被location里的同名指令覆盖,等于没写。改完nginx -t,reload,再开一次页面。如果站上还有别的接口是一次性返回的,这条改动对它们没有影响,只是少了缓冲,源站压力略增。
不想动nginx,也可以让后端自己说。响应头里加一条X-Accel-Buffering: no,nginx看到它就给这个响应关掉缓冲。这个头是nginx专用的,别的网关不认。好处是代码跟部署环境解耦,换一台机器不用再改配置。坏处是这条逻辑藏在业务代码里,下一个人排查时想不到。两处都加也行,nginx的配置优先级更高。
缓冲不止这一处。proxy_cache如果开着,第一次请求的内容会落盘,后面的人拿到的是缓存。gzip默认只压text/html,SSE的text/event-stream不在列表里,所以一般不是它。proxy_http_version默认是1.0,上游要是不支持长连接,响应照样攒完再发。设成1.1,上游才走分块传输。前面挂了CDN的话,那一层也会缓冲,你在源站改是没用的。Cloudflare这类还会把SSE整段收完再下发,得在面板里单独关。
回答长的时候还有第二个坑,proxy_read_timeout默认六十秒。模型吐到一半,连接被nginx掐断,前端看到的是半句话。把它调到三百秒,或者按业务自己定。php-fpm那边还有request_terminate_timeout,两层要对齐。
验证分两路,一路直连后端,一路走nginx。curl加-N参数,关掉自己的缓冲,屏幕上能看出数据是不是一块一块到的。再带-w参数打两个时间,time_starttransfer和total,两个数差不多就说明整段都攒住了。直连时前者远小于后者,走nginx时两个数贴在一起,结论就出来了。
线上现在是proxy_buffering off加X-Accel-Buffering: no,两层都写。顺手把proxy_read_timeout提到一百八十秒,够一次长回答。上线那天盯着time_starttransfer,首字能在半秒内出来才算真流式。这个数掉到和total一样,就是又被哪一层接住了。
申请创业报道,分享创业好点子。点击此处,共同探讨创业新机遇!
