bigywater.tech
Django · 117

长连接与 Streaming Response

想用 StreamingHttpResponse 做 SSE 或长连接,要先弄清两件事:生成器该写成 sync 还是 async?一个长连接会占用什么?这篇在 uvicorn 与 gunicorn 上把四种组合、客户端断开、以及并发长连接都量出来。

生成器要与服务器匹配
ASGI 用 async 生成器,WSGI 用 sync 生成器;不匹配会被缓冲并警告
1520 ms
uvicorn 单进程同时 30 个 1.5 s 的长流的总耗时
9045 ms
gunicorn 一个同步 worker 同时 6 个 1.5 s 的长流(排队)

四种组合的首批到达时间

实测 · 本机执行输出
  ASGI uvicorn  + async 生成器  首批    307 ms  最后一批   1511 ms  收到 5 批
  ASGI uvicorn  + sync 生成器   首批   1526 ms  最后一批   1526 ms  收到 5 批
    服务端警告: StreamingHttpResponse must consume synchronous iterators in order to serve them asynchronously. Use an asynchronous iterator instead.
  WSGI gunicorn + async 生成器  首批   1513 ms  最后一批   1513 ms  收到 5 批
  WSGI gunicorn + sync 生成器   首批    303 ms  最后一批   1508 ms  收到 5 批
    服务端警告: StreamingHttpResponse must consume asynchronous iterators in order to serve them synchronously. Use a synchronous iterator instead.

缓冲的意思是:服务端把整个生成器读完才开始发,首批到达时间等于总时间。Django 在这两种不匹配的情况下都会打出警告,可以从日志里发现。

客户端断开

实测 · 本机执行输出
  客户端在第 1 批之后断开;1.5 s 后 events.log 内容: 'async generator finally 执行了\n'

生成器里的 try/finally 在客户端断开后执行了,适合放释放资源的代码。这是在 uvicorn + async 生成器下测的,别的服务器没有测。

并发长连接

实测 · 本机执行输出
  uvicorn  30 个并发 async 流总耗时 1520 ms  全部 200: True
  gunicorn 6 个并发 sync 流总耗时 9045 ms  全部 200: True

ASGI 下一个进程能同时挂很多长连接(这里 30 个);WSGI 同步 worker 一次只处理一个请求,一个长流会占满一个 worker。这就是长连接场景要用 ASGI(或者多线程 worker)的原因。

分步演示

流式响应与长连接
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。uvicorn 与 gunicorn 同步 worker 各 1 个进程;事件 5 个、间隔 0.3 s;30 / 6 个并发流由本机线程发出
    • 没有验证:数千以上的并发连接、代理(nginx)对 SSE 的缓冲、gunicorn gthread / gevent worker、心跳与超时设置、浏览器 EventSource 的重连行为
    京ICP备2026067502号-1京公网安备11010502063487号