不能直接用os.ReadFile后Write,因会一次性加载整个视频到内存导致OOM;应采用流式透传、内存缓冲中转与连接级状态管理实现低延迟分发。

Go 语言常驻内存的视频分发系统,本质不是“把整个视频文件 load 到内存里”,而是用内存作中转缓冲 + 连接级状态管理,让流持续、低延迟、不阻塞地透传或复用。强行全量载入大视频(如 1GB MP4)只会触发 OOM,这是最常踩的第一个坑。
为什么不能直接 os.ReadFile 后 http.ResponseWriter.Write
这种写法在小文件(
- 内存暴涨:每个并发请求都复制一份完整视频数据,100 个连接 ≈ 占用 100GB 内存
- 启动延迟高:读完才开始响应,首帧时间不可控
- 无法处理慢客户端:TCP 缓冲区满后
Write阻塞,整个 goroutine 卡死,后续请求排队 - 不支持 range 请求:浏览器拖拽、暂停续播全部失效
正确做法是流式打开 + 分块读取 + 带超时控制的 io.Copy,并显式设置 Content-Range 和 Accept-Ranges: bytes 响应头。
io.Copy 配合 http.NewResponseController 控制写入节奏
Go 1.22+ 提供了 http.NewResponseController,可主动中断写入、获取写入字节数、设置写超时——这对视频分发至关重要:
立即学习“go语言免费学习笔记(深入)”;
- 用
rc.SetWriteDeadline防止单个卡顿连接拖垮整个连接池 - 用
rc.Conn().SetReadDeadline防止客户端长时间不读导致服务端缓冲堆积 - 避免手动实现
bufio.Writer或自定义 chunked 封装,原生支持更稳
示例关键片段:
func streamHandler(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("video.mp4")
if err != nil { ... }
defer f.Close()
<pre class='brush:php;toolbar:false;'>stat, _ := f.Stat()
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
rc := http.NewResponseController(w)
rc.SetWriteDeadline(time.Now().Add(30 * time.Second))
io.Copy(w, f) // 底层自动按 32KB buffer 分块,且受 deadline 约束}
goroutine 泄漏与连接生命周期管理
视频分发服务常因忽略连接关闭信号而累积大量僵尸 goroutine:
- 没监听
r.Context().Done(),客户端关页面或网络中断后,io.Copy仍继续运行 - 没用
http.TimeoutHandler包裹 handler,导致长连接无超时兜底 - 使用
sync.Pool复用缓冲区时,忘了在defer中归还,造成内存缓慢泄漏
必须在 handler 开头注册取消逻辑:
go func() {
<-r.Context().Done()
// 主动关闭底层 net.Conn(需类型断言),或通知 writer goroutine 退出
}()内存复用与背压控制的实际阈值
缓存代理场景中,bufChan 容量不是越大越好:
-
make(chan []byte, 8):适合低延迟直播,但慢客户端 2 秒不读就会丢帧 -
make(chan []byte, 64):平衡点,覆盖多数 5–10 秒弱网抖动 - 超过 128:内存占用翻倍,GC 压力显著上升,且无法解决根本的下游消费能力不足问题
真正可靠的背压,靠的是 select + default 非阻塞发送 + 计数丢弃策略,而不是堆缓冲区大小。
常驻内存的关键不在“存多少”,而在“怎么管住每条连接的生命周期、缓冲区和错误传播路径”。一个没做连接中断清理的流分发服务,跑三天后 goroutine 数可能从几百涨到上万——这点比任何算法优化都优先。


















