Nginx截断uwsgi+Django(Flask)大响应体的问题及解决

Posted 赖勇浩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nginx截断uwsgi+Django(Flask)大响应体的问题及解决相关的知识,希望对你有一定的参考价值。

目录

症状

昨天一个一直续费的老客户,说网站出问题了。他的网站只是简单的展示型公司官网,用 Django 做的,日常做放放产品,连交易都没有,是2016年做好一直没有动过的。年年续费很积极的优质客户反馈问题,赶紧问他咋了,他发了张图过来,说今天他编辑商品详情,发现保存按钮没有了,如下图:
可以看到这个页面已经结束了,但底部的保存按钮不见了。

正常的Django Admin管理后台商品编辑界面,下面有一排操作按钮。

找到关键原因

这很奇怪,为什么没改代码会突然这样。按我们程序员的思维,如果代码没改,那肯定是客户做了什么操作,问他,只是说补充了一些商品图片,然后就不行了。没办法,只好自己去后台看看。

首先怀疑是不是有什么Ajax请求没有成功,点开浏览器的网络请求一看,全是200响应,完全没有问题,这就奇怪了。

只好再仔细看看这“残缺”的商品界面,没成想,好家伙,一点开图片的下拉选择框,发现洋洋洒洒有几百上千个选项,当下就想咋这么多啊?然后右键点开看源码,是不其然,html文件有接近5000行,其中每一个图片选择的下拉框(Dropdown)的可选项都有上千个,而且源码确实残缺不全,Body、HTML等标签都没有闭合。

爬上服务器看日志去,先 tail -f error.log,发现在一些问题,每一次请求都有一个Permission denied,如下(敏感信息已用xxx代替):2022/06/29 12:20:45 [crit] 972#0: *11 open() "/var/lib/nginx/uwsgi/2/00/0000000002" failed (13: Permission denied) while reading upstream, client: xxx, server: www.xxx.com, request: "GET /admin/xxx/goods/75/ HTTP/1.1", upstream: "uwsgi://unix:///data/xxx/xxx.sock:", host: "www.xxx.com", referrer: "http://www.xxx.com/admin/xxx/goods/"
心下不由地疑惑,如果没权限的话不应该是整个网站跑不起来吗,这都运行好几年了,怎么还有没权限写入的问题?但也没有什么思路,只好按下不理,先去调试模式跑跑代码。

一句python manage.py runserver下去,Debug进程就呼啦一声跑起来,然后点开管理后台的商品编辑页面,没想到,完全没有问题!整个页面是完整的,inline admin挂载的十张八张图片、下面的一排操作按钮,都好好地躺在页面上,一刷新二刷新三刷新,都没有问题,这可把我给整懵了。

再点开浏览器的网络请求,突然发现/75这个请求返回大小足足的1.8MB,这HTML也太大了吧!对比访问nginx得到的“残缺网页”,仅55KB而已,看来的确是nginx没有返回整个响应体(response body)。点开网页源码一看,足足将近3万行,其中大部分是图片选择下拉框的选项:几千个文件重复了足足十来遍。

解决

于是开始放狗去搜,细细找过去,找到这篇问答nginx + uwsgi + django: Random upstream prematurely closed connection #1804(https://github.com/unbit/uwsgi/issues/1804),一看标题就很像,提前关闭了连接。细细看一下去,有个回答果然谈到一个方案,就是把 uwsgi 和 nginx之间的协议由 uwsgi 协议改为 http 协议。在 uwsgi 的配置中:

[uwsgi]
http=127.0.0.1:7020
# 其它略

在 nginx 配置中:

# 其它略
proxy_pass http://127.0.0.1:7020;

保存后重启 uwsgi 和 nginx,果然正常了。而且经过 nginx 的压缩传输,总数据量大小也从1.8MB降到300多KB,速度还是能让人满意的。

解决2

但是,技术人又怎么会满足于这样呢?我仔细研读上一篇问答,并调整搜索词,然后就找到了这篇文章flask server is truncating long json responses some of the times(https://stackoverflow.com/questions/53835990/flask-server-is-truncating-long-json-responses-some-of-the-times)里面描述的问题和我遇到的可以说是一模一样,连出错消息也都是Permission denied,细细看回答,说出现这个无权限问题原因是没有配置好nginx的proxy_temp_path,导致nginx往无权限的默认路径写入缓存而出错。

咦!有道理啊!看来可以揭开这个迷题了。然后去搜索proxy_temp_path,在这里nginx/1.17.9 randomly truncating some large proxy responses(https://trac.nginx.org/nginx/ticket/1950#comment:2)就找到了解释,As long as nginx cannot write temporary files, the result looks exactly as described: when a response is larger than what can be held in memory buffers, nginx tries to write extra data to a temporary file, writing fails, so nginx closes the connection. Exact amount of data sent to client may vary depending on the client bandwidth.

简单翻译来说,就是如果响应体很大,而 nginx 不能写入临时文件,那么 nginx 就会关闭链接,至于浏览器能收到多少数据,就看用户的带宽了。所以症状看起来就像随机返回一部分数据一样。
既然如此,那么只要好好地配置proxy_temp_path,就能解决问题。接下来参照nginx相关文档把它配置到正经的/tmp目录,然后改回使用uwsgi协议,但并没有解决问题,仍然是一样的错误。
觉得很奇怪,并继续用ngninx uwsgi proxy_temp_path关键词去搜索,找到nginx官文文档中的这一节,原来uwsgi对应的配置项应该是uwsgi_temp_path,改过来以后问题也就真正解决掉了。

解决3

这还不够完美。

考虑到用户的产品还会不停地增加,对应的图片也会不停地上传,以后图片下拉框的选项会进一步增长,Django模板引擎渲染1.8MB的HTML文件出来肯定很耗时间和内存,以后会更耗资源,需要解决它。

继续以django admin和外键相关的词去搜索,可以找到这一篇问答Many objects in Django admin with Foreign Key(https://stackoverflow.com/questions/27058986/many-objects-in-django-admin-with-foreign-key),细看问题,简直就是我们的翻版。有了好的问题,答案呼之欲出:raw_id_fields,它能够用弹窗来代替下拉框,使用以后看起来像这样:
用ID显示代替了下拉框,当点击右侧的搜索按钮,就出现一个弹窗,可以找到相应的资源并选择。使用raw_id_fields以后页面大小不到5KB,自然也没有NGINX写缓存的需要,问题从根源上得到解决。

推演

因为这个是 nginx 的缓存机制问题,所以理论上来说,不仅仅影响我们这种用 Django/Flask 开发的Python 程序,用php/Java/Go开发的可能也会影响到,只要它的app server和nginx的协议不会http估计都会有这样的问题,然后我用PHP去搜索了一下,的确能够找到相应的问答,在此提上一嘴,以便有其它语言开发者搜索到我这篇文章时,也能够有所帮助。

以上是关于Nginx截断uwsgi+Django(Flask)大响应体的问题及解决的主要内容,如果未能解决你的问题,请参考以下文章

Nginx截断uwsgi+Django(Flask)大响应体的问题及解决

手把手带你整得明明白白 Flask/Django+uWSGI+Nginx

Ubuntu18部署uwsgi+flask应用

centos部署flask+nginx+uwsgi之踩坑指南

Django+Uwsgi+Nginx部署

Django+uwsgi+Nginx上线最佳实战