# 讓 Django 不直接傳送檔案,改由 Nginx 透過 X-Accel-Redirect 提升下載效能 大多數 Django 服務在傳送受保護檔案(僅登入使用者可下載、付款後可閱覽等)時,會使用 `FileResponse` 等方式,讓 Python 程式讀取檔案並將內容送給客戶端。若流量不大或僅是內部伺服器間通訊,這樣的做法足夠。 然而當檔案請求量激增時,**應用程式伺服器(Python)會被檔案傳輸工作佔用**,導致原本應執行的權限檢查、商業邏輯、API 處理等工作變得困難。此時「由 Django 處理權限檢查,將檔案傳輸交給 Nginx」的典型技術就是 **X-Accel-Redirect**。 --- ## 為什麼直接由 Python 傳送會造成瓶頸? Django 直接傳送檔案的流程大致如下: 1. 接收請求 2. 執行權限檢查 3. 從磁碟/儲存空間讀取檔案 4. 由應用程式進程將資料流式傳送到網路 問題在於 **第 3、4 步是「重負」**: * 檔案越大,傳送時間越長 * 同時下載數量增加,工作者/執行緒/進程會被佔用 * 最終導致 API 響應延遲、逾時、甚至需要擴充伺服器 相對地,**Nginx 已經為靜態檔案傳輸做了最佳化**,包括 kernel 層的 `sendfile`、高效事件迴圈、緩衝/區間請求處理等,專門針對「檔案交付」而設計。 --- ## X-Accel-Redirect 的核心概念 **Django 只做「檢查」**,Nginx 只做「傳送」。 ### 工作原理 1. 客戶端請求 `/download/123` 2. Django 只執行資料庫查詢與權限檢查 3. Django 回傳空內容,並在回應標頭寫入: ``` X-Accel-Redirect: /_protected/real/path/to/file.webp ``` 4. Nginx 看到此標頭後,內部尋找對應檔案並直接送給客戶端 這樣,Django 完全不需要讀取檔案內容,僅負責判斷「此使用者是否有權下載」。 --- ## 何時特別適合使用此方式? 以下情境會看到顯著效益: ### 1) 下載/圖片請求量大且併發高的服務 * 社群/訊息平台的圖片、附件、報告 PDF 下載 * 請求數多、邏輯簡單的模式,X-Accel-Redirect 能發揮最大效能 ### 2) 檔案較大或區間請求(Range)重要的服務 * 視訊/音訊/大容量壓縮檔 * 瀏覽器/播放器使用 `Range` 進行播放/續傳 → Nginx 能更穩定地處理此類傳輸 ### 3) 想降低應用伺服器成本 * Python 工作者成本高(記憶體/CPU),若被檔案傳輸佔用會「流失」效能 * 把傳輸交給代理層,讓應用伺服器專注於商業邏輯 --- ## 何時可以不使用? * 內部伺服器間通訊且流量低 * 檔案請求少,且 API/資料庫邏輯是瓶頸 * 檔案存放於 S3 等外部物件儲存,且已透過 CDN/預簽名 URL 處理 在這些情況下,直接使用 `FileResponse` 仍然足夠。 --- ## 實作範例:Django + Nginx ![Web Request Flow Diagram](/media/editor_temp/6/a1ad0374-979a-447e-b586-81ac02f2b447.png) ### Nginx 設定範例 關鍵在於 `internal`。設定為 `internal` 的 location 只能被 X-Accel-Redirect 內部重導,外部無法直接存取。 ```nginx # 真正提供受保護檔案的內部端點 location /_protected/ { internal; # 真實檔案所在目錄 alias /var/app/protected_media/; # 性能選項(依環境調整) sendfile on; tcp_nopush on; # 如需額外的快取/標頭控制 # add_header Cache-Control "private, max-age=0"; } ``` * 假設 `/var/app/protected_media/` 下有實際檔案 * 外部可見 URL 為 `/download/...`(由 Django 路由) * 內部傳輸路徑統一為 `/_protected/...` --- ### Django 視圖範例 Django 只確認權限,並回傳 X-Accel-Redirect 標頭。 ```python from django.http import HttpResponse, Http404 from django.contrib.auth.decorators import login_required from django.utils.encoding import iri_to_uri @login_required def download(request, file_id): # 1) 取得物件並檢查權限 obj = get_file_object_or_404(file_id) # 範例 if not obj.can_download(request.user): raise Http404 # 2) 構造內部路徑(對應 Nginx 的 /_protected/) internal_path = f"/_protected/{obj.storage_relpath}" # 3) 只設置 X-Accel-Redirect,內容留空 response = HttpResponse() response["X-Accel-Redirect"] = iri_to_uri(internal_path) # (可選)設定下載檔名/內容類型 response["Content-Type"] = obj.content_type or "application/octet-stream" response["Content-Disposition"] = f'attachment; filename="{obj.download_name}"' return response ``` 重點: * 沒有 `FileResponse(open(...))` 之類的檔案 I/O * 每個請求的處理時間極短,工作者不會被檔案傳輸佔用 --- ## 安全檢查清單 ### 1) 內部路徑必須由伺服器決定 * 防止客戶端輸入 `/_protected/../../etc/passwd` 等路徑穿越 * 只使用資料庫中「安全」的相對路徑,或採用白名單映射 ### 2) Nginx location 必須設為 `internal` * 若未設 `internal`,使用者可直接存取 `/_protected/...`,造成安全漏洞 ### 3) 權限檢查僅由 Django 執行 * Nginx 僅為傳輸引擎,所有存取控制由 Django 負責 --- ## 其他第三方服務替代方案 若成本不是問題,也可以考慮直接由第三方儲存服務提供檔案: * **CDN 快取**:公開檔案可先放在 CDN,效能提升更顯著 * **預簽名 URL(S3 等)**:若使用物件儲存,X-Accel-Redirect 可能不必要,直接使用預簽名 URL 更簡單 --- ## 結語 總結來說,將檔案傳輸交給 Nginx 的代理層,能顯著提升效能,因為 Nginx 已經為靜態檔案傳輸做了最佳化。這樣,應用伺服器就能專注於權限檢查與商業邏輯,即使下載流量激增,整體系統仍能保持穩定。 若流量不大,`FileResponse` 仍是乾淨且足夠的選擇;但當「檔案請求暴增」時,X-Accel-Redirect 是最快、最有效的解決方案。 只要記住一句話:**「權限由 Django 處理,傳輸由 Nginx 負責」**。