讓 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

Nginx 設定範例

關鍵在於 internal。設定為 internal 的 location 只能被 X-Accel-Redirect 內部重導,外部無法直接存取。

# 真正提供受保護檔案的內部端點
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 標頭。

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 負責」