背景与现象
- 背景:在C知汇项目(GPT私有库问答系统)中,我们期望GPT的回答通过流式返回,采用了SSE(Server-Sent Events)的服务端推送技术做流式传输。
- 现象:本地运行没有问题,但是上线之后发现有时是流式,有时是一次性返回。
分析过程
1、首先确认技术选型是否存在问题。流式响应有常见的两种方案,WebFlux和SSE,我们使用WebFlux响应式编程技术替代SSE,写最小demo排除业务的影响,发现仍然有问题。可以暂时排除代码层面的问题。
2、分析数据传输链路,用户发送提问并成功响应时,回答通过流式返回链路:GPT -> C知汇后台 -> 后台Nginx -> C知汇前端 -> 前端Nginx -> 浏览器 我们可以选择从源头开始验证。 1)后台打印GPT响应,收到GPT的响应是流式的,没有问题。 2)通过直连后台服务容器IP排除Nginx的影响,发现问题消失了。
3、验证猜想:查阅nginx配置资料,发现nginx有缓冲区的相关配置 proxy_buffers。查看鲸云Nginx配置如下图所示。通过自建nginx验证了猜想。那么为什么线上环境有时能复现,有时又是正常的呢?原来是响应内容的大小决定的,如果响应内容没有达到缓冲区容量,那么会一次返回,如果超过了缓冲区容量,那么缓冲区容量以外的数据会流式返回。
解决方案
方案一:取消网关的响应缓冲。可能需要修改网关配置,然而修改网关配置是一个不小的动作,而且可能对其他应用造成影响。
方案二:绕过nginx,直连访问。但是鲸云我们通常采用滚动更新镜像的方式,所以容器IP是经常会变更的。后台可以开放获取IP接口,客户端可以先通过访问后台接口获取IP,然后再直连,这样也能使用到网关的负载均衡。主要的缺点有:1、不通过网关过滤直连有一定的安全风险。2、访问记录不会被网关日志收集,会对数据的统计和问题的排查造成一定的影响。
方案二的方式有点hack,可以直接排除,我们尝试从方案一尝试入手解决。
尝试解决
关闭响应缓冲的方式主要有两种
1)将Nginx的proxy_buffers配置设置为off。
2)将请求头X-Accel-Buffering设置为 no,Cache-Control设置为no-cache。
1、我们在应用代码上去手动设置请求头,发现仍然不行。我们查看请求返回的响应头里没有我们设置的X-Accel-Buffering,而使用Arthas执行watch,看到X-Accel-Buffering是设置进去了的。
2、怀疑是配置优先级的问题,如果我们将Nginx的proxy_buffers设置为on,请求头X-Accel-Buffering设置为no,会以哪一个为准?我们通过自建Nginx验证发现,Nginx 会遵循请求头中的指示,所以是请求头X-Accel-Buffering优先。
3、我们验证了正常情况下使用X-Accel-Buffering是可以生效的且优先级比较高的。回到第1点,如果我们在APISIX设置X-Accel-Buffering响应头,那么会不会透传回客户端呢?测试表明该响应头仍然丢失了。
4、我们去掉前端部分再对整条链路进行分析,GPT -> 应用后台 -> APISIX -> 客户端,我们在后台和APISIX都设置了响应头,但是响应里却没有出现X-Accel-Buffering头。那么我们可以猜测,APISIX和客户端之间还有一些链路,导致X-Accel-Buffering没有被透传。一般实际生产中,Nginx也不止一层,那么需要运维协助排查解决。
协调资源解决问题
通过运维协助,展开了业务端不可见的完整链路: GPT -> C知汇后台 -> APISIX -> 雷池 WAF -> 负载均衡 F5 -> 浏览器
- 具体原因:雷池 WAF(网络应用防火墙)底层为 Nginx 变体,默认开启响应处理并强制写入缓冲区(由于需要等待检测结果才能放行),导致
X-Accel-Buffering在此处失效并停止下传。 - 解决方法:关闭 WAF 的响应处理。
- 风险影响:会失去雷池 WAF 对响应内容的部分监控功能,如识别软件报错信息、代码泄漏、Webshell 执行及信息泄漏检测等。