不能在http handler中直接运行pg_dump,因为其阻塞式执行会卡住goroutine导致超时、oom和服务假死;必须用stdoutpipe流式传输,配合goroutine分离读写、显式flush、context超时控制及cmd.wait()确保子进程清理。

为什么 pg_dump 不能直接在 HTTP handler 里跑
因为 pg_dump 是阻塞式外部命令,直接 exec.Command 启动并 cmd.Run() 会卡住整个 goroutine,HTTP 连接超时、内存暴涨、服务假死——这不是并发问题,是 I/O 绑定 + 进程生命周期失控。
真正要做的不是“调用 pg_dump”,而是把它当作一个流式数据源,把 stdout 当成管道,边读边写回 ResponseWriter,同时不阻塞主线程。
- 必须用
cmd.StdoutPipe()获取只读 pipe,不能用cmd.Output()或cmd.CombinedOutput() - HTTP handler 内不能调用
cmd.Wait(),否则等它结束才返回,备份几 GB 就挂几小时 - 要显式设置
Content-Type: application/octet-stream和Content-Disposition: attachment; filename=backup.sql,否则浏览器不会触发下载 - 记得调用
http.Error()捕获cmd.Start()失败(比如 pg_dump 不在 PATH)
如何安全地把 pg_dump stdout 流进 http.ResponseWriter
核心是用 goroutine 分离执行和写入:一个 goroutine 启动 pg_dump 并读 pipe,另一个 goroutine 把数据 chunk 写到 http.ResponseWriter。中间用 io.Copy 最简,但要注意 response flush 和 context 取消。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 用
bufio.NewReader包一层 stdout pipe,避免小包频繁 write 导致 TCP 粘包或延迟 - 在 handler 开头调用
w.(http.Flusher).Flush(),确保 header 发出去,不然客户端卡在等待响应头 - 用
context.WithTimeout(r.Context(), 2*time.Hour)控制最大执行时间,避免备份卡死进程 - 别忽略
cmd.Wait()的 error:即使流写完了,pg_dump可能因权限/锁表失败,得记录日志而不是静默
cmd := exec.Command("pg_dump", "-U", "user", "-d", "db", "--no-owner", "--no-privileges")
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
http.Error(w, "backup start failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="backup_`+time.Now().Format("20060102_150405")+".sql"`)
io.Copy(w, stdout) // 这里会阻塞,但由 HTTP server 管理超时
cmd.Wait() // 必须等,否则子进程残留
大体积备份下如何防止 OOM 和连接中断
10GB 备份文件如果全读进内存再 write,Go runtime 直接 panic;而 HTTP 默认 30s 超时,数据库锁表太久也会出问题。关键不是压缩,是控制流量节奏和释放资源。
- 禁用
pg_dump --inserts(改用--column-inserts或默认格式),减少文本膨胀 - 加
--jobs=2(仅限 directory 格式),但流式导出只能用 plain 格式,所以别加 - 用
ResponseWriter的底层Flush()手动分段刷新,例如每 1MB 写一次:if n%1024*1024 == 0 { w.(http.Flusher).Flush() } - PostgreSQL 连接串里加
connect_timeout=10和statement_timeout=3600000(1h),防 dump 卡在慢查询
异步任务怎么通知用户完成状态
流式导出本质是“边生成边传输”,没法像普通 API 返回 JSON 说“已开始”。真异步就得拆成两阶段:先返回 task ID,再提供 /backup/status?id=xxx 接口轮询。
- 启动
pg_dump前生成唯一taskID := uuid.New().String(),存到内存 map 或 Redis,value 包含startTime、pid、error字段 - 导出完成后(
cmd.Wait()返回),更新状态为 done,并写入临时文件路径(如/tmp/backup_abc123.sql),供后续下载或清理 - 不要在 /backup/start 接口里启动 goroutine 后立即返回 success —— 得确保
cmd.Start()成功,否则用户以为成功了实际根本没跑 - 轮询接口返回字段必须包含
progress(估算值,比如按已写入字节数 / 预估总大小),但 PostgreSQL 没法实时获取 dump 总大小,建议固定返回"running"或"done"两种状态
最常被忽略的是子进程僵尸化:cmd.Wait() 必须执行,且不能被 context cancel 中断,否则 pg_dump 进程一直挂着,磁盘和连接数悄悄耗尽。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










