让 Nginx 代替 Django 直接传输文件:通过 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
  2. Django 仅执行数据库查询和权限校验
  3. Django 在响应头中写入: X-Accel-Redirect: /_protected/real/path/to/file.webp 并返回空体
  4. Nginx 读取该头部,内部定位文件并直接将内容返回给客户端

这样,Django 完全不需要读取文件内容,所有文件传输工作都交给 Nginx。


何时特别适合使用 X-Accel-Redirect?



  1. 下载/图片请求量大且并发高:社区/聊天图片、附件、报告 PDF 下载等。
  2. 文件体积大或 Range 请求重要:视频、音频、大文件压缩包等。
  3. 想降低应用服务器成本:Python 进程昂贵,文件传输占用资源时会导致成本飙升。

何时可以不使用?

  • 内部服务器间通信且流量低
  • 文件请求少,API/DB 逻辑是瓶颈
  • 文件存储在 S3 等外部对象存储,已通过 CDN 或预签名 URL 解决

在这些场景下,FileResponse 仍然足够。


实现示例:Django + Nginx

Web 请求处理流程图

Nginx 配置示例

关键是 internal 指令。被标记为 internal 的 location 只能通过内部重定向访问,外部客户端无法直接访问。

# 真实文件的内部服务端点
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 视图示例

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) 构造内部路径
    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:否则用户可以直接访问 /_protected/...
  3. 权限校验只在 Django 中完成:Nginx 只负责传输,安全性由 Django 保障。

通过第三方服务的替代方案

如果成本不是问题,也可以考虑直接从第三方存储提供文件:

  • CDN 缓存:公开文件可在 Nginx 前使用 CDN 缓存,效果更佳。
  • 预签名 URL(如 S3):对象存储可使用预签名 URL,省去 X-Accel-Redirect 的复杂性。

结语

总之,文件交付最好交给 Nginx 处理。Nginx 经过静态文件优化,能充分利用内核级别的 sendfile 等技术,从而显著提升性能。这样,应用服务器可以专注于权限校验和业务逻辑,即使下载流量激增,整体系统也能保持稳定。

如果流量不大,FileResponse 仍是干净且足够的选择。但当文件请求激增导致应用服务器崩溃时,X-Accel-Redirect 是最快、最有效的解决方案。

记住一句话:“权限由 Django 负责,传输由 Nginx 负责”