不能每个请求都起一个goroutine,因为会导致调度器过载、内存暴涨、gc频繁stw及oom;需用带缓冲channel做背压漏斗,缓冲大小须满足buffer_size×avg_request_mem<可用内存×0.3。

为什么不能每个请求都起一个 goroutine
直接写 go handle(req) 看似简单,但面对每秒几万请求时,会瞬间拉起同等数量 goroutine,调度器扛不住、内存暴涨、GC 频繁 STW,服务直接卡死甚至 OOM。这不是理论风险——线上真实压测中,90% 的“扛不住”问题就出在这一步。
- goroutine 虽轻量(~2KB 栈),但百万级并发下仍需 GB 级内存
- runtime 调度器在高 goroutine 数量下争抢 m/p/g,上下文切换开销剧增
- 没做任何背压控制,上游流量突增时,下游(DB/Redis/第三方 API)来不及消费,任务堆积在内存里变成定时炸弹
用带缓冲 channel 做漏斗,不是队列
make(chan *Request, 10000) 这个数字不是越大越好,它本质是内存缓冲区上限,必须满足:buffer_size × avg_request_mem 。超过这个阈值,OOM 风险陡增;太小又容易丢请求。
- HTTP handler 里只做轻量入队:
select { case taskChan - worker 启动固定数量(比如 8–16 个),从 channel 拉取任务,不阻塞主流程
- 若需按用户/租户保序,worker 内部再按
req.UserID % N分桶,避免全局锁
批量写 + 单 goroutine 消费才是吞吐关键
多个 goroutine 并发 db.Exec 或 csvWriter.Write 不会变快,只会触发连接池争抢、行锁/页锁冲突、磁盘寻道混乱。实测 10 个并发写 CSV,吞吐比 1 个 goroutine 批量写低 40% 以上。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 消费者从 channel 收数据后,攒够 100–1000 条再
tx.Exec("INSERT VALUES (...), (...)", args) - 失败时整批重试,不要单条 fallback —— 否则吞吐断崖下跌
- 用
sync.Pool复用*bytes.Buffer、sql.NamedArg等结构体,避免高频 GC
异步任务状态怎么查?别只靠 Redis key
返回 task_id 很容易,但真正难的是:任务失败了怎么定位?重试几次?超时了谁来熔断?这些不是靠 redis.Set(taskID, "running") 就能解决的。
- 状态存 Redis 时,必须带 TTL(如
redis.SetEx(taskID, "processing", 300)),防 key 泄漏 - 每个任务启动时生成唯一 traceID,日志、监控、链路追踪全部对齐该 ID
- 关键任务失败必须进死信队列(DLQ),并触发告警;非关键任务可设最大重试次数(如 3 次),之后丢弃
最易被忽略的不是怎么发任务,而是 channel 关闭时机和 worker 退出时的 graceful shutdown —— 未处理完的任务是否回滚?缓冲区里的请求要不要落盘?这些细节决定系统是否真的“可靠”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










