
前言
下载报告这个场景大家应该都遇到过。现在下载报告经常会出现请求超时的现象,最终发现问题:报告是实时生成的,但是当数据量太大时,在组装数据的阶段会比较耗时,可能会超出网关设置的超时时间(例如:网关设置超时10s,做数据组装耗时20s)。
阻塞式接口,客户端请求发过去了,一直没有收到响应,此时需要如何处理?
几种方案
- 最简单的,调整超时时间,不过这个治标不治本,不推荐。
- 提前处理,预生成需要的文件。优点:实现简单,接口体验好;缺点:存储成本高,不适合实时数据。
- 异步方案(任务+轮询/回调)。比较好的处理方案,优点:可控、稳定、可扩展,适合大数据量;缺点:系统复杂度高。
- 流式处理,一旦连接建立就立即返回
header,后端边查边算边写 response body 。优点:网关不会因为无响应而超时,改动小;缺点:传输过程中的错误无法有效告知客户端,一旦开始写response body,HTTP 状态码基本定了(通常 200),header已经发出,可能会出现用户下载到一半,文件损坏。
本篇只介绍流式处理的方案。
流式处理
核心思路是尽快把响应头返回,并在后续持续写入响应体,让网关/负载均衡感知到后端在持续输出,从而不触发"长时间无响应"的超时。网关超时不是以"总耗时"为准,而是以"长时间无响应"为准;只要持续有数据输出,就能保活。
优点:
- 有效绕过网关超时:只要持续向客户端输出数据,网关就不会判定"长时间无响应"。
- 支持实时生成、边算边传:不需要等全部数据准备好,首字节时间极短。
- 内存友好:不用一次性加载全量数据,适合大数据量下的报告等输出。
- 用户体验:用户可以立刻开始下载,而不是长时间等待。
缺点:
- 错误处理能力弱:一旦开始输出,基本无法再返回标准错误信息。
- 任务不可恢复:中断即失败,只能重新生成。
- 长连接占资源:持续占用 HTTP 连接、服务线程、数据库等。
- 实现复杂度更高:代码模型需要改成"可迭代 + 分批输出"。
简单示例
go 代码 main.go:go run main.go 启动程序
go
package main
import (
"time"
"github.com/gin-gonic/gin"
)
// 测试:当使用流式写入时,http请求会超时吗?例如:当 nginx 网关设置了1分钟超时,这时候调用下载接口去下载数据,若数据量大,组织数据的时间超过了1分钟,使用流式和非流式的区别。
func main() {
r := gin.Default()
r1 := r.Group("/api")
r1.GET("/download", func(c *gin.Context) {
time.Sleep(time.Second * 10)
c.Writer.Write([]byte("Hello, World!"))
c.Writer.Flush()
})
r1.GET("/download/stream", func(c *gin.Context) {
for i := 0; i < 10; i++ {
// 手动构造 4k 数据
// buf := bytes.NewBuffer(make([]byte, 1024*4))
// buf.WriteString(strings.Repeat("Hello, World!", 4))
// bytes := buf.Bytes()
bytes := []byte("Hello, World!")
c.Writer.Write(bytes)
c.Writer.Flush() // 如果这里不 Flush 掉,因为没到达缓存 4kb 的上限,是不会返回给客户端的,需要手动 Flush
time.Sleep(time.Second * 1)
}
c.Writer.Flush()
})
r.Run(":8080")
}
nginx 的 nginx.conf 配置:
bash
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 8093;
server_name localhost;
# 接口代理的location块(核心配置区域)
location /api/ {
# 后端接口地址
proxy_pass http://127.0.0.1:8080;
# ========== 核心超时配置 ==========
# 连接后端服务器超时时间
proxy_connect_timeout 9s;
# 等待后端响应超时时间
proxy_read_timeout 9s;
# 向后端发送数据超时时间
proxy_send_timeout 9s;
# 可选:传递客户端真实IP等头部
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
启动 go 服务和 nginx 服务后,访问:
http://127.0.0.1:8093/api/download阻塞http://127.0.0.1:8093/api/download/stream流式
结论:
http://127.0.0.1:8093/api/download 超时报错,http://127.0.0.1:8093/api/download/stream 能够正常下载。
总结
流式处理的关键是尽快首包 和持续输出,用"边查边算边写"替代"先算完再返回"。它能有效规避网关的无响应超时,改动成本低,但要接受"错误不易回传、失败感知滞后"的代价。
注意:这只是一种折中的方案,有不少缺点,通常不使用。