当前位置: 首页 > news >正文

GO练习题-Goroutinue泄漏

某电商公司的订单服务,上线后运行了大约 2 小时,OOM(Out of Memory)被 K8s 杀死,进程反复重启。

运维同事看到的日志只有一行:fatal error: runtime: out of memory

开发同事重启了几次,问题依旧。直到有经验的架构师用pprof看了一眼 goroutine 分布,才发现:每处理一个请求,就 leak 一个 goroutine,而且这些 goroutine 全部卡在 channel 的 send 操作上,永远无法退出。

1000 个 QPS × 2 小时 = 720 万次泄露的 goroutine,每个 goroutine 默认 2KB 栈空间,再加上分配的堆对象,最终把内存吃光了。

教训:goroutine 泄露是 Go 生产环境中最隐蔽、危害最大的并发问题之一。没有之一。


学习目标

  • 理解 goroutine 的生命周期:谁启动、谁负责停止
  • 理解 channel 阻塞如何导致 goroutine 泄露
  • 掌握context.WithCancel的正确用法和取消传播链
  • 学会用net/http/pprof检测 goroutine 泄露

问题描述

下面的代码是一个模拟的请求处理函数。每个请求启动一个 worker goroutine 来处理任务,并将结果发送到 channel。

但这段代码有一个 goroutine 泄露——worker goroutine 在完成工作后,试图向resultCh发送结果时,如果没有人读取,就会永远阻塞。

请找出泄露点并修复它。

原始代码(有 BUG)

package main import ( "fmt" "math/rand" "time" ) func simulateRequest() { resultCh := make(chan string) // 无缓冲! // 启动 worker goroutine 处理"请求" go func() { // 模拟耗时处理(300~800ms) time.Sleep(time.Duration(rand.Intn(500)+300) * time.Millisecond) resultCh <- fmt.Sprintf("处理完成,耗时 %dms", 300+rand.Intn(500)) // TODO: 这里会泄露——如果 resultCh 没人读,goroutine 永远卡住 }() // TODO: 模拟请求取消或超时的情况(比如 500ms 后调用方不再关心结果) // 如果 worker 还没完成,它就 leak 了 time.Sleep(500 * time.Millisecond) // 这里不会收到结果,因为 goroutine 已经被阻塞在 send 上了 fmt.Println("请求方放弃等待") } func main() { for i := 0; i < 5; i++ { simulateRequest() fmt.Printf("第 %d 个请求处理完毕\n\n", i+1) time.Sleep(200 * time.Millisecond) } fmt.Println("所有请求处理完毕") // 运行后观察:goroutine 数量只增不减 time.Sleep(2 * time.Second) }

预期现象:运行后 goroutine 数量持续增长,因为这些 goroutine 卡在resultCh <- ...上无法退出。


修复要求

使用context.WithCancel修复泄露问题,确保:

  1. worker goroutine 能感知到取消信号,及时退出
  2. 使用defer cancel()确保清理
  3. 修复后运行,goroutine 数量保持稳定

面试追问

Q1: 如何用 pprof 检测 goroutine 泄露?

// 在 HTTP 服务中启用 pprof import _ "net/http/pprof"

然后通过以下命令检查:

# 查看 goroutine 数量 curl http://localhost:6060/debug/pprof/goroutine?debug=1 # 导出 goroutine profile 到文件 curl http://localhost:6060/debug/pprof/goroutine -o goroutine.prof # 用 pprof 工具分析 go tool pprof goroutine.prof

生产环境中,定期采集 goroutine 数量并设置告警是必要的。


Q2: channel 阻塞的 4 种情况?

#场景谁阻塞是否泄露
1sendCh <- val,无人接收发送方是,发送方 goroutine 永久阻塞
2<-recvCh,无人发送接收方是,接收方 goroutine 永久阻塞
3双向 channel,两端都关闭后继续读写读写方是,panic 或永久阻塞
4select 中没有 default 分支,所有 case 都阻塞select 所在 goroutine是,goroutine 永久阻塞

Q3: 如何用 context 预防 goroutine 泄露?

:核心原则——所有从请求衍生出的 goroutine,都应该传入同一个 context,并在 context 取消时优雅退出

func handler(ctx context.Context) { ctx, cancel := context.WithCancel(ctx) defer cancel() // 关键:无论什么路径退出,都调用 cancel go func() { defer fmt.Println("goroutine 已退出") select { case <-ctx.Done(): return // context 取消,优雅退出 case resultCh <- doWork(): // 发送成功,正常退出 return } }() // ... 处理逻辑 }

context 取消传播链

用户请求取消 / 超时 → handler 的 context 被 cancel → 所有通过 context.WithCancel/WithTimeout 衍生出的子 context 被 cancel → 所有 select 中 case <-ctx.Done() 分支触发 → 所有衍生 goroutine 退出
http://www.jsqmd.com/news/1095016/

相关文章:

  • TSW14J50评估板:JESD204B接口高速ADC/DAC数据采集与验证实战指南
  • 从SDH到OTN:一张图看懂光传送网的演进与核心架构
  • 高速ADC设计实战:ADC07D1520关键配置与优化要点解析
  • DeepSeek狂招36人!80%岗位都在抢Agent工程师
  • ChatGPT o1推理模型:为什么你的vLLM集群吞吐暴跌?揭秘o1专属tokenization预处理冲突及4步热修复方案
  • MSP430中断控制器与FRAM控制器深度解析:从寄存器配置到实战优化
  • 重新定义桌面伴侣:Mate Engine如何让虚拟角色成为你的数字伙伴
  • 语音唤醒失效、语义错乱、上下文丢失——ChatGPT语音对话三大致命缺陷,工程师必须在Q3前修复
  • 解码半导体四大顶会:IEDM、ISPSD、VLSI、ISSCC的技术风向标
  • SpiderFoot开源情报工具:自动化信息收集与攻击面管理实战指南
  • CC1101寄存器深度解析:从射频核心到RF1A接口的嵌入式无线通信实战
  • DRV10970评估板实战指南:无刷电机驱动快速验证与配置详解
  • eBPF简介
  • 如何实现课堂自主权:JiYuTrainer在极域电子教室环境中的教学优化解决方案
  • MSP430数字I/O与电容触摸寄存器配置实战指南
  • CSDN涨粉秘籍:快速提升经验值的终极指南
  • 【Claude】Request too large / Image too large / Unable to resize / PDF 报错已解决(4合1)
  • 【独家首发】OpenAI未公开的视频token压缩算法:实测降低87%显存占用,让消费级显卡跑通长视频推理
  • TMP814单相全波风扇电机预驱动器:从原理到PCB布局的完整设计指南
  • 鸿蒙原生 ArkTS 布局方式之 TextAlign:文字在 Text 组件中的对齐策略深度解析
  • AO3镜像站完全指南:解锁全球同人创作宝库的终极解决方案
  • 高速ADC评估实战:从TSW54J60 EVM性能验证到系统设计优化
  • 【TEE从入门到精通及实战】76 段页式内存隔离:让Wasm沙箱在TEE里真正“物理隔离”
  • SAGAN实战:从Self-Attention原理到PyTorch代码精讲
  • 数据安全与合规:IM选型中不可逾越的“一票否决项”
  • 3步掌握哔哩下载姬:提升视频下载效率的完整方案
  • TI MSP-DRV-ADAPT-EVM适配板解析:快速构建电机控制评估平台
  • 游戏App安全实战:从代码混淆到服务器验证的立体防御体系
  • 高速DAC设计实战:从电流舵架构到PCB布局的完整指南
  • MSP430从F1xx到F2xx迁移实战:硬件兼容、软件重构与避坑指南