第 9 / 23 篇

第8章 流式响应、慢客户端与写入截止

输出端也会施加背压

流式响应把结果逐块交付,能够降低首块等待和缓冲内存,却让客户端读取速度直接影响服务端资源。客户端不读时,数据会先进入用户态与内核缓冲;缓冲填满后,Write 才阻塞。服务端看见 Write 暂时成功,并不代表用户已经收到,更不代表已经消费了业务结果。

随书 /stream 专门用于这个实验。它重复写出有限数量字节,并通过 ResponseController 设置写截止。测试使用真实 TCP 连接,发完请求后不读取响应,使输出最终停在网络边界。这里故意使用很大的响应量,是为了越过本机缓冲;它不代表业务应当默认发送如此大的响应。

本机实测中,慢客户端请求不读取二百五十六 MiB 演示响应,八十毫秒写截止使写入返回 i/o timeout,处理函数结束。测试重点是有界结束和资源释放,具体写出了多少字节依赖系统缓冲和调度,不作为跨平台断言。把字节数量写成固定性能结论,会误读实验。

Flush 只推动数据,不确认交付

Flush 尝试把已缓冲输出推进协议层,不等待业务客户端确认处理。它还可能触发真正网络写入并阻塞,因此也要受写截止约束。某些中间件包裹 ResponseWriter 后没有暴露控制接口,会使 Flush 或设置截止失效。服务应针对实际包装链验证能力,并处理不支持的返回值。

分块协议需要自己的结束语义。正常完成帧、应用错误帧和连接中断代表不同结果。对于 SSE,可以使用事件类型与事件 ID;对于自定义 JSON 行协议,应约定每行是否完整、最后是否有终结记录。客户端不能只看到 HTTP 200 就把中途断开的结果当成成功。

代理也可能缓存响应、设置空闲超时或合并小块。随书实验直接连接本地 Go Server,证明的是应用与本地 TCP 的行为;不能据此宣称任意 CDN、Ingress 或浏览器都按同样节奏收到块。部署时应沿完整路径测试首块、间隔和结束帧,并记录代理配置。

绝对截止与空闲截止

一个固定 WriteTimeout 可以约束响应总体写入时间,但长连接流可能合法持续很久。如果希望限制的是客户端长时间不读,就要设计按块或按进度刷新截止的方式。刷新必须发生在合适边界:不断生成但一直堆积在缓冲中的数据,不应被误判为有效交付进度。

教学实现选择简单的有界写窗口,便于证明故障最终释放。对生产流式接口,需要在总时长、块间空闲、最大字节数和每连接资源之间做明确选择。没有一个超时字段同时满足所有流协议;“把超时调大”只是改变故障暴露时间,不会解决无界占用。

写截止到达后,当前连接可能已经不适合复用。随书 HTTP/1.1 流端点明确使用 Connection: close,让最后的协议 flush 也有清楚的连接终点;普通端点仍可复用连接。这是为本实验生命周期选择的策略,不能机械照搬到所有长连接或 HTTP/2 流上。

取消必须穿过生产链

如果流式响应背后还有一个生产 goroutine,处理函数因慢客户端退出时,应取消生产并等待结束。否则只是释放了网络写入者,生成数据的一端还可能继续运行。若生产者向无人读取的 channel 发送,它会永远阻塞;若 channel 无限制缓冲,它会继续消耗内存。

应让生成、传输与关闭共享一个明确取消范围,且只有有权的一方关闭输出 channel。每块数据的缓冲区也要遵守所有权,不要在 Write 或异步编码尚未结束时就归还对象池。所谓低分配优化一旦破坏数据稳定性,可能产生只有压力下才出现的内容损坏。

流式请求计费和完成状态也不能只看处理函数是否开始。可以分别记录连接建立、首块写出、正常终结、客户端取消和写超时。不要为每个小块产生一条同步日志,以免观测成本反过来改变传输节奏。聚合计数与必要的失败摘要通常更合适。

练习:同时观察两端

运行慢读取实验,一次完全不读,一次每隔固定时间读取少量字节。记录 stream finished 日志的错误类别和客户端读到的终结状态,再参考 TestSlowStreamDeadlineReleasesHandler,等待包装 handler 在 ServeHTTP 返回后关闭的 finished 通知,确认服务端已经退出。/stats 的 active 不包含 /stream,不能用它证明流处理完成。把写截止移除后,只在独立子进程中复现阻塞,并由测试总截止清理该进程,防止故障实验拖住整个测试套件。

再给生成端加一个退出通知,要求网络端失败后通知也在有界时间内到达。最后检查 HTTP/1.1 与你计划部署的协议是否具有不同关闭语义。报告应清楚写出本次覆盖哪一层,哪些代理和协议尚未测量。

本章实测证据

真实TCP客户端不读取响应,八十毫秒绝对写截止触发i/o timeout,handler有界返回。实际写出字节数不作跨平台断言。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。

--- PASS: TestSlowStreamDeadlineReleasesHandler (0.08s)