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は静的ファイル配信に最適化 されていて、カーネルレベル最適化(sendfile)、効率的なイベントループ、バッファリング/レンジリクエスト処理など「ファイル配信」に特化した機能を活用します。


X-Accel-Redirectの核心アイデア

Djangoは「検査のみ」し、Nginxは「転送のみ」する

動作原理

  1. クライアントが /download/123 のようなURLをリクエスト
  2. DjangoがDB照会/権限チェックのみ実行
  3. Djangoがレスポンスヘッダーに以下のように記載し、空ボディで返却

X-Accel-Redirect: /_protected/real/path/to/file.webp 4. Nginxがこのヘッダーを見て 内部的にファイルを探し、クライアントへ直接送信

  • Djangoはファイル内容を直接読み取らない

つまり、Djangoは「このユーザーがこのファイルを取得してもよいか?」だけを担い、実際のファイル転送はNginxに委ねます。


この手法が特に有効なケース

以下の状況で効果が大きくなります。

1) ダウンロード/画像リクエストが多く、同時性が高いサービス

  • コミュニティ/メッセンジャー画像、添付ファイル、レポートPDFダウンロード
  • 「リクエスト数は多いがロジックは単純」なパターンではX-Accel-Redirectが光ります。

2) ファイルサイズが大きい、またはRangeリクエストが重要なサービス

  • 動画/音声/大容量圧縮ファイル
  • ブラウザ/プレイヤーが Range(区間リクエスト)で再生/再取得する場合、Nginxがこれをより安定して処理します。

3) アプリサーバコストを抑えたいとき

  • Pythonワーカーは高価(メモリ/CPU)で、ファイル転送に絡むと「お金が漏れる」構造になります。
  • ファイル転送をプロキシ層に任せることで、アプリサーバはロジック処理に集中できます。

逆に、使わなくてもよいケース

  • 内部サーバ間の通信でトラフィックが低い
  • ファイルリクエストが少なく、ほとんどがAPI/DBロジックがボトルネック
  • ファイルがローカルディスクではなくS3など外部オブジェクトストレージで、既にCDN/プリサインURLで解決できる構造

このような場合は FileResponse でも運用上十分です。必要になったときに導入しても遅くはありません。


実装例:Django + Nginx

ウェブリクエスト処理フローチャート

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は権限だけ確認し、ファイル内容を読み取らずヘッダーだけを返します。

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) DB照会 + 権限チェック
    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はありません。 - Djangoはリクエストごとの処理時間が非常に短くなり、ワーカーがファイル転送でロックされません。


セキュリティチェックリスト

1) 内部パスは必ず「サーバが決定」

  • クライアント入力で /_protected/../../etc/passwd のようなパスが生成されないように
  • DBに保存された「安全な相対パス」だけを使用するか、ホワイトリストベースでマッピングしてください。

2) Nginx location は必ず internal

  • internal が無ければ、ユーザーが /_protected/... を直接叩いて回避ダウンロードできてしまいます。

3) 権限チェックロジックは Django のみで信頼

  • Nginx は「転送エンジン」役で、アクセス制御は Django が担う構造が安全です。

第三者サービスを利用する代替案

コストに余裕があれば、最初からファイルを第三者ストレージで提供する設計も可能です。コストはかかりますが、安定性が高く、サーバリソースを節約できます。プロジェクトチームの状況に合わせて選択してください。

  • CDNキャッシュ:公開ファイルなら Nginx 前に CDN キャッシュがより効果的
  • プリサインURL(S3 等):オブジェクトストレージベースなら X-Accel-Redirect の代わりにプリサインURLがよりシンプルな場合もあります

まとめ

一般に、ファイル配信はウェブアプリケーション側で行うより、Nginx にオフロードしたほうが性能面で有利です。静的配信に最適化された Nginx がカーネル最適化まで活用して処理するためです。 その結果、アプリサーバは「権限チェック + ビジネスロジック」に集中でき、ダウンロードトラフィックが増えても全体システムがはるかに安定します。

トラフィックが大きくない場合は FileResponse でも十分です。ただし「ファイルリクエストが急増したときにアプリサーバが崩壊するパターン」は非常に一般的で、その際に最も速く効果を得られるカードが X-Accel-Redirect です。

キーワードを一つ覚えておくと良いでしょう: 「権限は Django、転送は Nginx」


連関ポスト