Nginx截斷uwsgi+Django(Flask)大響應體的問題及解決

語言: CN / TW / HK

目錄

昨天一個一直續費的老客戶,說網站出問題了。他的網站只是簡單的展示型公司官網,用 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,速度還是能讓人滿意的。

但是,技術人又怎麼會滿足於這樣呢?我仔細研讀上一篇問答,並調整搜尋詞,然後就找到了這篇文章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協議,問題也解決掉了。

這還不夠完美。

考慮到使用者的產品還會不停地增加,對應的圖片也會不停地上傳,以後圖片下拉框的選項會進一步增長, 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去搜索了一下,的確能夠找到相應的問答,在此提上一嘴,以便有其它語言開發者搜尋到我這篇文章時,也能夠有所幫助。