bigywater.tech
Flask · 118

Streaming JSON

返回几十万行数据,是一次性 jsonify 还是流式?流式能省多少内存、慢多少?流到一半出错客户端怎么知道?客户端断开会怎样?这篇用真实的 gunicorn 与 PostgreSQL 服务端游标,把答案逐项量出来。

+352 → +1 MB
返回 50 万行时 worker 峰值内存的增量:一次性与流式
1698 → 104 ms
响应头到达时间:一次性与流式 NDJSON
IncompleteRead
流到一半出错时客户端读取到的异常

一次性与流式

实测 · 本机执行输出
  (每种方式各用一个全新的 worker 进程,避免上一次请求留下的内存影响峰值)
  一次性 jsonify      状态 200 响应头到达   1698 ms | 首块数据   1698 ms | 全部读完   1718 ms | 41.6 MB | Content-Length: 41592617 | Transfer-Encoding: None | worker 空闲 72 MB,峰值 RSS 424 MB(多 352 MB)
  流式 NDJSON        状态 200 响应头到达    104 ms | 首块数据    114 ms | 全部读完   5314 ms | 41.6 MB | Content-Length: None | Transfer-Encoding: chunked | worker 空闲 72 MB,峰值 RSS 73 MB(多 1 MB)
  流式 JSON 数组       状态 200 响应头到达      1 ms | 首块数据    100 ms | 全部读完   5258 ms | 41.6 MB | Content-Length: None | Transfer-Encoding: chunked | worker 空闲 72 MB,峰值 RSS 73 MB(多 1 MB)

上一篇:大批量 JSON 响应;下一篇:SECRET_KEY 的用途。

每种方式单独起一个 worker:第一次我在同一个 worker 里连续测,上一次请求留下的内存把后面的峰值抬高了,结果不可信;而且我第一次忘了给生成器加 stream_with_context,流式路径返回了一个 141 字节的错误页,我是看到这个异常小的响应才发现的。

JSON 数组的合法性

实测 · 本机执行输出
  json.loads 解析成功,500000 个元素,第一个 {'id': 1, 'name': 'user1', 'email': 'user1@example.com', 'score': 1.5},最后一个 {'id': 500000, 'name': 'user500000', 'email': 'user500000@example.com', 'score': 750000.0}

出错与不完整

实测 · 本机执行输出
  状态码: 200 | Transfer-Encoding: chunked
  客户端读取结果: 读取时抛出 IncompleteRead

客户端断开

实测 · 本机执行输出
  服务端日志: ['generator closed (client went away)', 'generator finally']

分步演示

流式 JSON
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)、gunicorn gthread worker,内存取 ps 的 RSS 每 50 ms 采样的最大值
    • 没有验证:经过 nginx 反向代理时的缓冲(proxy_buffering 会让流式失效,见部署部分)、浏览器端边收边解析、gzip 与分块传输一起使用、HTTP/2 下的行为、超长流的超时设置(--timeout 对流式响应的影响)
    京ICP备2026067502号-1京公网安备11010502063487号