DeepSeek Harness 的流式响应卡在 gzip:只有 2fps 与两秒一跳怎么修

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHgzipZ_SYNC_FLUSH流式响应WebServerH.264
DeepSeek Harness 的 /api 流式响应经 gzip 后卡在 zlib 缓冲区,画面只有约 2fps。根因是压缩中间件未设 flush,给逐块刷新、按类型排除与写入端 flush 三条路径。

手机投屏或模拟器预览只有 2fps、画面每两秒跳一次,但设备本身跑 27fps,宿主也在 90 毫秒内写完了每个数据包——这时该怀疑的不是编码器,而是 gzip。 DeepSeek Harness(DSH)的 Web 服务在开启压缩后,会把 application/octet-stream 的流式响应也纳入 gzip,而压缩中间件没有设置任何 flush 策略;zlib 于是把数据攒在缓冲区里,等填满或响应结束才吐出来(来源)。

先定性:DeepSeek Harness 的 2fps 是编码慢还是被攒

判据是「静态画面最惨、服务端写入很均匀、客户端读取间隔很均匀」——三条同时成立就是攒批,而不是编码性能。

  1. 在服务端记录每次写入的时间戳。预期:间隔与设备帧率一致,约 30ms,而不是两秒。
  2. 在客户端记录每次 reader.read() 的时间与字节数。预期:读取间隔约两秒,单次字节数很大。
  3. 打印响应头,看有没有 content-encoding: gzip。预期:有,说明响应被压缩过。
  4. 临时关掉压缩再测同一路流。预期:帧率恢复到设备水平。
  5. 检查反向代理有没有再压一层。预期:没有额外的缓冲或压缩头。
相似卡顿区分特征
设备采集慢服务端写入间隔本身就长且不均匀
中间层缓冲没有 gzip 头,但读取仍成批
keep-alive 重连几秒后整块停住,日志有连接重建
gzip 攒批写入均匀、响应头带 gzip、读取按两秒一批

讨论里的测量正好是这个形状:静态画面时编码器每两秒才发一个大关键帧,缓冲区也每两秒释放一次,于是看起来像编码器只有 2fps(#10024)。

根因:DeepSeek Harness 的压缩中间件为什么攒流

中间件的 filter 只排除 text/event-stream,application/octet-stream 不在排除名单里,而配置里只设了 level 与 threshold,没有 flush。

  1. createGzipMiddleware 的过滤函数对 content-range 响应放行、对 text/event-stream 跳过压缩。预期:其余交给默认过滤。
  2. mime-db 把 application/octet-stream 标为可压缩,未知长度的流也被允许压缩。预期:视频流进入压缩器。
  3. 压缩器没有 flush 选项,zlib 只在缓冲区满或流结束时输出。预期:攒批发生。
  4. HTTP bridge 写响应时只调用 res.write(chunk),没有调用 res.flush()。预期:数据停在 zlib 里。
  5. 每一帧都等到下一个关键帧把缓冲区撑满才出去。预期:客户端按关键帧节奏更新。

还有一个容易漏掉的前提:compression: 'gzip' 不是 Web 服务的默认值,默认是 none,由 bundle 打开。所以这类问题表现为「某些安装上才有」,也让「换台机器就好了」成为常见误判。

静态画面最惨也有原因:没有小包持续喂给压缩器,只有关键帧足够大时才触发一次输出。画面越动,包越密,攒批越不明显——这和「网络差」的直觉正好相反。

路径一:给 DeepSeek Harness 压缩加 Z_SYNC_FLUSH

这是改动最小、收益面最大的一条:一行配置让每次写入都立刻可达,所有流式路由一起受益。

  1. 找到 Web 服务里构造压缩中间件的位置,确认只传了 level 与 threshold。预期:看不到 flush 字段。
  2. 在同一个配置对象里加上 flush: zlib.constants.Z_SYNC_FLUSH。预期:中间件创建成功,启动无报错。
  3. 重启宿主,用原来的流式路由再测。预期:客户端每收到一帧就能立刻读出。
  4. 观察带宽与 CPU。预期:带宽略增,CPU 基本不变。
  5. 在单个 profile 上先灰度,保留原配置文件备份。预期:出问题能一条命令还原。
  6. 回滚时删掉 flush 字段并重启。预期:行为回到修改前。
  7. 再测一次大文件下载。预期:压缩率变化在可接受范围。

块特别碎时,逐块 flush 会降低压缩率。可以改成每个事件循环轮次合并刷新一次,在延迟与吞吐之间取折中。

路径二:按流类型排除 DeepSeek Harness 的压缩

如果不想让压缩器碰流式响应,就在 filter 里明确跳过这类 content-type,或者给 DSH插件 路由一个「不压缩」的标记。

  1. 给流式路由声明一个可识别的类型,不要和普通二进制下载共用 application/octet-stream。预期:服务端能区分两类响应。
  2. 在 filter 中对这个类型返回 false。预期:响应头不再出现 content-encoding: gzip。
  3. 保留普通 JSON 与静态资源的压缩分支。预期:页面资源仍是 gzip,体积没有回退。
  4. 回归一次页面加载与一次文件下载。预期:两者都正常。
  5. 把这个类型写进文档。预期:下一个插件接入时能直接查到规则。
  6. 用插件的流式路由验证一次。预期:数据按写入节奏到达。

这条路的契约更干净,但需要新增一个让插件表达意图的入口。类型命名一旦定下,就别再改,否则老插件会悄悄回到被压缩的状态。

路径三:在 DeepSeek Harness 写入端逐块 flush

宿主暂时改不了时,插件可以自己刷:compression 中间件会给响应对象挂上 flush,每次写完调用一次即可。

  1. 在流式响应的写入循环里取到响应对象,确认 flush 是否存在。预期:启用压缩时存在,未启用时为 undefined。
  2. 每次 res.write(chunk) 之后调用 res.flush?.(),再处理背压。预期:客户端读到的块与写入一一对应。
  3. 写一个组合测试:handler 的第二块要等客户端解出第一块才产生。预期:不调用 flush 时测试超时。
  4. 只对流式路由启用,不要给普通大文件响应逐块 flush。预期:下载的压缩率不受影响。
  5. 在日志里记录 flush 次数。预期:与写入块数一致。

这条路不需要动宿主,代价是每个插件都要自己记得调用,宿主级默认行为仍然没有修。插件越多,漏掉的风险越大。

验证与边界:DeepSeek Harness 流式响应复查

修完后要看的是「首块到达时间」和「块间隔」,不是平均帧率——平均帧率会被关键帧节奏掩盖。

  1. 分别在压缩开启与关闭下记录首块时间。预期:开启并 flush 后,两者同一量级。
  2. 确认响应使用 chunked 而不是固定 Content-Length。预期:流式响应没有长度头。
  3. 只测静态画面。预期:这是最坏输入,能暴露攒批。
  4. 连续观察五分钟。预期:块间隔稳定,没有周期性卡顿。
路线改动位置收益范围主要代价
路径一宿主中间件一行配置所有流式路由压缩率略降
路径二宿主 filter 加路由标记指定流式路由需要新增插件入口
路径三插件写入循环单个插件每个插件都要自己调用

边界有三条:已压缩负载的收益本来就有限,别指望压缩率提升;反向代理可能再压一层,改完宿主仍要检查代理配置;讨论里没有官方修复版本。连接层的同类卡顿见 Web 性能与连接问题。

常见问题

DeepSeek Harness 的 /api 推 H.264 只有 2fps 且两秒一跳,是编码器太慢吗?

DeepSeek Harness 的响应被 gzip 攒住了,通常不是编码器慢。压缩中间件没有设置 flush,zlib 要等缓冲区填满才吐数据。静态画面下编码器每两秒才发一个大关键帧,客户端就按这个节奏更新。

为什么同一个插件在有的 DeepSeek Harness 安装上流畅,在另一些安装上只有 2fps?

DeepSeek Harness 的 gzip 不是默认值,只有启用了压缩的 bundle 才会打开。压缩开启时 application/octet-stream 这类未知长度的流也会被压缩;关闭时数据直接透传。先看响应头有没有 content-encoding。

给 DeepSeek Harness 的压缩中间件加 Z_SYNC_FLUSH 会不会明显增加带宽或 CPU?

DeepSeek Harness 每个数据块只多几字节 flush 标记,对 16 到 64 KB 的块可以忽略。代价是压缩率略降;对 H.264 这类已压缩负载几乎没有影响。块很碎时可按事件循环轮次合并刷新。

不想改宿主代码,只改插件或客户端能不能解决 DeepSeek Harness 的流式卡顿?

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

来源