bigywater.tech
Django · 22

StreamingHttpResponse 流式响应

StreamingHttpResponse 是「边生成边发」还是「只是换了个写法」?这篇在真实的 gunicorn 上量首字节时间和大响应的内存占用,再看它跟普通响应在属性上的不同。

1507 → 304 ms
5 段、每段 0.3 s 的响应,HttpResponse → StreamingHttpResponse 的首字节时间
241 vs 43 MiB
200 MiB 响应时 gunicorn worker 的峰值内存:一次性 vs 生成器流式
chunked
流式响应没有 Content-Length,用 Transfer-Encoding: chunked

首字节时间

实测 · 本机执行输出
  /buffered/  首字节 1507 ms,收完 1532 ms,共 40 字节;Content-Length: 40  Transfer-Encoding: None
  /streaming/  首字节 304 ms,收完 1508 ms,共 40 字节;Content-Length: None  Transfer-Encoding: chunked

gunicorn 同步 worker,一个 worker。客户端用 read1 读取,记录第一个字节到达的时刻(用 read(n) 会因为凑不够 n 字节而被自己拖慢,这是我第一次量时踩的坑)。总耗时都是 ≈1.5 s——流式改变的是「用户什么时候开始看到内容」,不是总时间。

对象的差别

实测 · 本机执行输出
  streaming = True
  r.content → AttributeError: This StreamingHttpResponse instance has no `content` attribute. Use `streaming_content` instead.
  Content-Length 头: False
  streaming_content 类型: map

流式响应没有 .content,只能迭代 streaming_content;中间件里如果读 response.content 就会炸,所以对流式响应要判断 response.streaming。

内存

实测 · 本机执行输出
  /big_buffered/  worker 起始 RSS ≈ 41 MiB;收完 126 ms,首字节 26 ms,200 MiB;峰值 RSS ≈ 241 MiB;Content-Length: 209715200
  /big_streaming/  worker 起始 RSS ≈ 41 MiB;收完 98 ms,首字节 3 ms,200 MiB;峰值 RSS ≈ 43 MiB;Content-Length: None

每种方式都用全新的 worker 进程测(RSS 在释放后不一定回落,共用一个进程会污染下一次测量——我第一次就这样量错了)。流式方式内存基本不涨。

什么时候用

大文件下载、导出、服务端事件推送。注意:压缩(GZipMiddleware)对流式响应的处理、反向代理是否会缓冲(nginx 的 proxy_buffering)都会影响「首字节」效果,这两点本文没有测。同步 worker 下,一个慢的流式响应会占住整个 worker(本文只用了 1 个 worker)。

分步演示:首字节与内存

StreamingHttpResponse
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。gunicorn 26.2 同步 worker ×1;内存每 20 ms 用 ps 采样 worker 进程 RSS
    • 没有验证:多 worker / gthread、uvicorn(ASGI)下的流式行为、反向代理缓冲、GZipMiddleware 与流式的交互、客户端中途断开时服务端生成器是否被及时关闭
    京ICP备2026067502号-1京公网安备11010502063487号