序言
架构变动,组织调整,人事地震,都是神仙在斗法,哪有那么多的新花招,不过是新来一批要分蛋糕的人,怎么占领地盘而已。
一个东西再熟悉,接触的场景多了,也会有解决不了的问题,而且可能解决问题的时间越来越长,例如在运维的时候,出现延迟抖动,例如在配置的时候,出现一些莫名其妙的报错。
nginx在sse场景下的报错
1 背景
在某天运维的时候,突然收到一个故障,在nginx代理进行上传一个文件的时候,文件稍微大点在十几M的时候,就会出现error_http2_PROTOCOL,但是在小文件上传的时候,就不会有任何问题,粗略一看,还以为是证书问题,但是仔细一听,小文件又没有问题。
2 nginx woker oom
这个报错也算是比较熟悉了,一般在上传文件的时候,如果占用的内存过多未释放,那么就基本上判定是内存导致的oom,因为nginx是运行在容器中,内存是有限制,从而有可能出现woker process被杀死,从而出现这个问题,但是检查了一下错误日志,里面并没有发现process exited的字样,看了下监控,也没出现woker process oom,从而排除此问题。
查看error log之后,发现里面有具体的报错信息:
sendfile() failed(9 bad file descriptor) while sending
request to upstream
3 sendfile报错
根据这个报错信息,基本上判定了是和sendfile参数有关,sendfile参数主要是用来优化提高nginx的性能的,默认基本上都会打开,但是这个的优化主要是为了优化静态文件的传输,从而直接0拷贝,nginx发送系统调用sendfile直接从内核中将数据发送给网卡,减少用户空间到内核空间的拷贝。
这是一个上传请求,post请求,并且body比较大,而且nginx开启了proxy_request_buffering,也就是会将body进行临时存储到磁盘中,在这种情况下,才会触发sendfile的调用,否则动态请求是不会用sendfile调用的。
4 修改参数查看对应的报错信息
首先构造了客户端对应的curl请求,从而能更好的模拟客户端的报错信息,在查看curl的报错的时候,报错如下:
curl: (18)transfer closed with outstanding read data remaining
报错信息表示在读取数据的时候,传输通道被关闭了。
禁止sendfile调用,配置如下:
sendfile off;
再次进行请求,发现一切变得正常。
关闭客户端的临时落盘,配置如下:
proxy_request_buffering off;
再次进行请求,发现一切也会变得正常,此时虽然开启了sendfile配置,但是在日志中查看,实际上没有进行sendfile调用的,直接客户端和服务端进行交互。
5 搞不清楚为啥,问问AI
现在的AI那么多,有chatgpt,有aws的,都问了一遍,发现都是牛头不对马嘴,例如说要检查文件目录的权限,检查临时文件的权限,emmm,AI也是一个搜索,必须根据你的关键词才能回答到正确的地方。
在使用curl请求的时候,发现这个请求会有不停的响应,一问才知道这是一个SSE的场景,也就是和websocket差不多,服务端会主动推送信息到客户端中,从而再次问AI,会答的就基本差不多了。
主要报错的原因是:sendfile()的异步执行特性与nginx的同步资源管理机制产生竞态条件,在SSE长连接的场景中,临时文件的描述符过早被关闭。
5 验证
虽然AI已经给出了答案,但是并不一定可靠,瞎说的AI见得太多了,能编造出花里胡哨的答案,从而如何进行验证也是一个问题。
重新编译openresty,将--with-debug模块加入到其中,从而使用debug的日志信息来查看对应的日志,实际上看不到明显的东西,只能看到sendfile报错。
使用streace -TTTf -p pid,追踪对应的系统调用,主要在查看对应句柄的打开与关闭中,会发现有其中的状态。
接受到upstream发过来的SSE响应
close关闭临时文件句柄,其实这个时候就是nginx提前关闭了连接
nginx发送响应给客户端(access 日志记录的是200响应)
sendfile继续将临时文件发送给upstream,但是临时文件被删除
写入日志alert 信息
关闭upstream连接
找一个干净的环境,进行debug调试,否则debug日志太多,你根本无法分辨对应的信息,从而也就证明了,临时文件被提前删除,从而导致了sendfile失败,主要还是因为sendfile是异步的,nginx无法感知这个已经结束了,本来这个临时文件的清理工作是在请求结束之后再清理的,但是在这个SSE长连接场景中,nginx会收到响应,从而误认为这个请求结束了,进行关闭了临时文件,这是一种竞争关系,什么时候清理生成的临时文件,也是一个考验。
在同时,也进行了一下抓包,在抓包文件中,当upstream发送信息的时候,nginx会不断地发送reset,重置连接,也就是关闭了连接。谁发送reset包,大概率情况下就不是一个好人了。
在其中,其实临时文件不被删除,也不能解决这个问题,使用配置:
client_body_in_single_buffer on;
client_body_in_file_only on;
启用这两个配置之后,临时文件会一直保存在对应的目录中,也可以用来调试调试。
其实还有一种解法,就是在nginx后端再弄个网关,网关一般的配置都是禁用了proxy_request_buffering和proxy_buffering,从而也能完美的解决这个问题。