DeepSeek Harness 的流式响应卡在 gzip:只有 2fps 与两秒一跳怎么修
手机投屏或模拟器预览只有 2fps、画面每两秒跳一次,但设备本身跑 27fps,宿主也在 90 毫秒内写完了每个数据包——这时该怀疑的不是编码器,而是 gzip。 DeepSeek Harness(DSH)的 Web 服务在开启压缩后,会把 application/octet-stream 的流式响应也纳入 gzip,而压缩中间件没有设置任何 flush 策略;zlib 于是把数据攒在缓冲区里,等填满或响应结束才吐出来(来源)。
先定性:DeepSeek Harness 的 2fps 是编码慢还是被攒
判据是「静态画面最惨、服务端写入很均匀、客户端读取间隔很均匀」——三条同时成立就是攒批,而不是编码性能。
- 在服务端记录每次写入的时间戳。预期:间隔与设备帧率一致,约 30ms,而不是两秒。
- 在客户端记录每次
reader.read()的时间与字节数。预期:读取间隔约两秒,单次字节数很大。 - 打印响应头,看有没有
content-encoding: gzip。预期:有,说明响应被压缩过。 - 临时关掉压缩再测同一路流。预期:帧率恢复到设备水平。
- 检查反向代理有没有再压一层。预期:没有额外的缓冲或压缩头。
| 相似卡顿 | 区分特征 |
|---|---|
| 设备采集慢 | 服务端写入间隔本身就长且不均匀 |
| 中间层缓冲 | 没有 gzip 头,但读取仍成批 |
| keep-alive 重连 | 几秒后整块停住,日志有连接重建 |
| gzip 攒批 | 写入均匀、响应头带 gzip、读取按两秒一批 |
讨论里的测量正好是这个形状:静态画面时编码器每两秒才发一个大关键帧,缓冲区也每两秒释放一次,于是看起来像编码器只有 2fps(#10024)。
根因:DeepSeek Harness 的压缩中间件为什么攒流
中间件的 filter 只排除 text/event-stream,application/octet-stream 不在排除名单里,而配置里只设了 level 与 threshold,没有 flush。
createGzipMiddleware的过滤函数对content-range响应放行、对text/event-stream跳过压缩。预期:其余交给默认过滤。mime-db把application/octet-stream标为可压缩,未知长度的流也被允许压缩。预期:视频流进入压缩器。- 压缩器没有 flush 选项,zlib 只在缓冲区满或流结束时输出。预期:攒批发生。
- HTTP bridge 写响应时只调用
res.write(chunk),没有调用res.flush()。预期:数据停在 zlib 里。 - 每一帧都等到下一个关键帧把缓冲区撑满才出去。预期:客户端按关键帧节奏更新。
还有一个容易漏掉的前提:compression: 'gzip' 不是 Web 服务的默认值,默认是 none,由 bundle 打开。所以这类问题表现为「某些安装上才有」,也让「换台机器就好了」成为常见误判。
静态画面最惨也有原因:没有小包持续喂给压缩器,只有关键帧足够大时才触发一次输出。画面越动,包越密,攒批越不明显——这和「网络差」的直觉正好相反。
路径一:给 DeepSeek Harness 压缩加 Z_SYNC_FLUSH
这是改动最小、收益面最大的一条:一行配置让每次写入都立刻可达,所有流式路由一起受益。
- 找到 Web 服务里构造压缩中间件的位置,确认只传了
level与threshold。预期:看不到flush字段。 - 在同一个配置对象里加上
flush: zlib.constants.Z_SYNC_FLUSH。预期:中间件创建成功,启动无报错。 - 重启宿主,用原来的流式路由再测。预期:客户端每收到一帧就能立刻读出。
- 观察带宽与 CPU。预期:带宽略增,CPU 基本不变。
- 在单个 profile 上先灰度,保留原配置文件备份。预期:出问题能一条命令还原。
- 回滚时删掉
flush字段并重启。预期:行为回到修改前。 - 再测一次大文件下载。预期:压缩率变化在可接受范围。
块特别碎时,逐块 flush 会降低压缩率。可以改成每个事件循环轮次合并刷新一次,在延迟与吞吐之间取折中。
路径二:按流类型排除 DeepSeek Harness 的压缩
如果不想让压缩器碰流式响应,就在 filter 里明确跳过这类 content-type,或者给 DSH插件 路由一个「不压缩」的标记。
- 给流式路由声明一个可识别的类型,不要和普通二进制下载共用
application/octet-stream。预期:服务端能区分两类响应。 - 在
filter中对这个类型返回 false。预期:响应头不再出现content-encoding: gzip。 - 保留普通 JSON 与静态资源的压缩分支。预期:页面资源仍是 gzip,体积没有回退。
- 回归一次页面加载与一次文件下载。预期:两者都正常。
- 把这个类型写进文档。预期:下一个插件接入时能直接查到规则。
- 用插件的流式路由验证一次。预期:数据按写入节奏到达。
这条路的契约更干净,但需要新增一个让插件表达意图的入口。类型命名一旦定下,就别再改,否则老插件会悄悄回到被压缩的状态。
路径三:在 DeepSeek Harness 写入端逐块 flush
宿主暂时改不了时,插件可以自己刷:compression 中间件会给响应对象挂上 flush,每次写完调用一次即可。
- 在流式响应的写入循环里取到响应对象,确认
flush是否存在。预期:启用压缩时存在,未启用时为 undefined。 - 每次
res.write(chunk)之后调用res.flush?.(),再处理背压。预期:客户端读到的块与写入一一对应。 - 写一个组合测试:handler 的第二块要等客户端解出第一块才产生。预期:不调用 flush 时测试超时。
- 只对流式路由启用,不要给普通大文件响应逐块 flush。预期:下载的压缩率不受影响。
- 在日志里记录 flush 次数。预期:与写入块数一致。
这条路不需要动宿主,代价是每个插件都要自己记得调用,宿主级默认行为仍然没有修。插件越多,漏掉的风险越大。
验证与边界:DeepSeek Harness 流式响应复查
修完后要看的是「首块到达时间」和「块间隔」,不是平均帧率——平均帧率会被关键帧节奏掩盖。
- 分别在压缩开启与关闭下记录首块时间。预期:开启并 flush 后,两者同一量级。
- 确认响应使用 chunked 而不是固定
Content-Length。预期:流式响应没有长度头。 - 只测静态画面。预期:这是最坏输入,能暴露攒批。
- 连续观察五分钟。预期:块间隔稳定,没有周期性卡顿。
| 路线 | 改动位置 | 收益范围 | 主要代价 |
|---|---|---|---|
| 路径一 | 宿主中间件一行配置 | 所有流式路由 | 压缩率略降 |
| 路径二 | 宿主 filter 加路由标记 | 指定流式路由 | 需要新增插件入口 |
| 路径三 | 插件写入循环 | 单个插件 | 每个插件都要自己调用 |
边界有三条:已压缩负载的收益本来就有限,别指望压缩率提升;反向代理可能再压一层,改完宿主仍要检查代理配置;讨论里没有官方修复版本。连接层的同类卡顿见 Web 性能与连接问题。
常见问题
DeepSeek Harness 的响应被 gzip 攒住了,通常不是编码器慢。压缩中间件没有设置 flush,zlib 要等缓冲区填满才吐数据。静态画面下编码器每两秒才发一个大关键帧,客户端就按这个节奏更新。
DeepSeek Harness 的 gzip 不是默认值,只有启用了压缩的 bundle 才会打开。压缩开启时 application/octet-stream 这类未知长度的流也会被压缩;关闭时数据直接透传。先看响应头有没有 content-encoding。
DeepSeek Harness 每个数据块只多几字节 flush 标记,对 16 到 64 KB 的块可以忽略。代价是压缩率略降;对 H.264 这类已压缩负载几乎没有影响。块很碎时可按事件循环轮次合并刷新。
DeepSeek Harness 的流式卡顿可以在写入端解决:压缩中间件会给响应挂上 flush 方法,每次 res.write 后调用 res.flush 就能推出去。这条不用动宿主,适合第三方插件,但每个插件都要自己记得调用。
相关术语
- Z_SYNC_FLUSH
- Z_SYNC_FLUSH 是 zlib 的一种刷新模式,把当前已压缩的数据全部输出,同时保留压缩字典以便后续继续压缩。它适合流式响应,但不结束整个压缩流。— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
- compression 中间件
- compression 中间件是 DeepSeek Harness Web 服务用来对响应做 gzip 的包装层,可配置 level、threshold 与 filter。它默认只按内容类型和长度筛选,不负责替调用方刷新 zlib 缓冲。— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
- chunked 传输
- chunked 是 HTTP/1.1 的分块传输编码,响应不声明 Content-Length,而是边产生边发送。流式接口通常使用它,压缩器也因此可能自行决定何时输出数据。— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
来源
- #10024 — Web 服务 gzip 让流式 /api 响应滞留在 zlib 缓冲区· deepseek-ai(GitHub Discussions)
- DeepSeek Harness 官方仓库· deepseek-ai(GitHub)