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

Golang http请求忘记调用resp.Body.Close()而导致的协程泄漏问题(含面试常见协程泄漏相关测试题)

参考:

  • 知乎:别因为忘记close你的httpclient,造成goroutine泄漏
  • CSDN:resp.Body.Close() 引发的内存泄漏goroutine个数

先来看几道题,想一想最终的输出结果是多少呢?

package mainimport ("fmt""io/ioutil""net/http""runtime"
)// 测试1:不执行resp.Body.Close(),也不执行ioutil.ReadAll(resp.Body)
func testFun1() {for i := 0; i < 5; i++ {_, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun1 http.Get err: ", err)return}}time.Sleep(time.Second * 1)fmt.Println("testFun1() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试2:执行resp.Body.Close(),不执行ioutil.ReadAll(resp.Body)
func testFun2() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun2 http.Get err: ", err)return}resp.Body.Close() // 执行resp.Body.Close()}// Close()过程需要一定时间,如果直接输出goroutine数量,可能出现连接还未完全回收的情况,结果有时为1有时为3// 因此,为了结果的准确性,我们这里休眠等待1秒,使得连接完全被回收。time.Sleep(time.Second * 1)fmt.Println("testFun2() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试3:不执行resp.Body.Close(),执行ioutil.ReadAll(resp.Body)
func testFun3() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun3 http.Get err: ", err)return}_, _ = ioutil.ReadAll(resp.Body) // 执行ioutil.ReadAll(resp.Body)}time.Sleep(time.Second * 1)fmt.Println("testFun3() 当前goroutine数量=", runtime.NumGoroutine())
}// 测试4:执行resp.Body.Close(),也执行ioutil.ReadAll(resp.Body)
func testFun4() {for i := 0; i < 5; i++ {resp, err := http.Get("https://www.baidu.com")if err != nil {fmt.Println("testFun4 http.Get err: ", err)return}_, _ = ioutil.ReadAll(resp.Body) // 执行ioutil.ReadAll(resp.Body)resp.Body.Close()                // 执行resp.Body.Close()}time.Sleep(time.Second * 1)fmt.Println("testFun4() 当前goroutine数量=", runtime.NumGoroutine())
}func main() {testFun1()testFun2()testFun3()testFun4()
}

答案:

  • testFun1() 当前goroutine数量= 11
  • testFun2() 当前goroutine数量= 1
  • testFun3() 当前goroutine数量= 3
  • testFun4() 当前goroutine数量= 3

注意:

针对以上testFun2(),如果仅仅执行了 resp.Body.Close(),那么为了结果的准确性,我们在打印结果之前通过time.Sleep(time.Second * 1)先休眠等待1秒,使得连接完全被回收后,然后再打印输出结果
因为,Close()过程可能需要一定时间,如果直接输出goroutine数量,那么可能出现连接还未完全回收的情况,导致结果随机,有时为1有时为3 …

而针对以上testFun4(),虽然执行了 resp.Body.Close(),但是同时也执行了 ioutil.ReadAll(resp.Body),这里会优先把当前的连接放入空闲列表中,供下次复用,所以不存在输出结果随机的情况。

解析:

首先要清楚,如果没有调用 resp.Body.Close(),也就是没有 回收/释放 连接,那一定会存在协程的泄漏问题(进而导致内存泄漏)。

另外,稍微了解 go net/http 包的同学,都知道 每次执行http的 Get/Post 请求时,底层都会创建两个协程,分别处理 写请求/读响应 这两个事件。具体底层逻辑后面会提到 …
所以你可以简单的理解为:执行一条http请求时,go内部会创建两个协程。

接下来,针对每一个示例做分析:

  • testFun1() 这里执行了 5 次http请求,且不执行 resp.Body.Close(),也不执行 ioutil.ReadAll(resp.Body)
    因为 每次执行http的 Get/Post 请求时,底层都会创建两个协程,加上主协程本身,所以一共有 5*2 + 1 = 11 个协程。
  • testFun2() 这里执行了 5 次http请求,执行 resp.Body.Close(),不执行 ioutil.ReadAll(resp.Body)
    说明在执行 resp.Body.Close()后,回收了底层都会创建两个协程,只剩下主协程本身,所以一共就 1 个协程。
  • testFun3() 这里执行了 5 次http请求,不执行 resp.Body.Close(),执行 ioutil.ReadAll(resp.Body)
    当执行 ioutil.ReadAll(resp.Body),将 resp.body 读取完之后,会把当前的连接放入空闲列表中,供下次复用
  • testFun4() 这里执行了 5 次http请求,执行 resp.Body.Close(),也执行 ioutil.ReadAll(resp.Body)
    这里的结果和 testFun3() 一致,关键在于 执行了 ioutil.ReadAll(resp.Body),将 resp.body 读取完之后,会优先把当前的连接放入空闲列表中,供下次复用,即使你执行了 resp.Body.Close()

源码解析:

通过跟踪 go net/http 包的源码,得到其调用链路的流程图:

可以发现每次新建立一个http请求,最终底层实际上都会创建两个新的协程(写请求/读响应)

  • go pconn.readLoop():启动一个 读响应 相关的goroutine
  • go pconn.writeLoop():启动一个 写请求 相关的goroutine
    在这里插入图片描述

readLoop()writeLoop()本身都是for循环:

  • 只要【控制是否退出循环的变量 alive】为true,循环就会一直进行下去,且会把当前的连接放入空闲列表中,供下次复用
    • 示例:Fn正常的读body,当body读完之后,会向 waitForBodyRead推入一个true:waitForBodyRead <- isEOF
  • 而只有从 bodyEOF := <-waitForBodyRead中读出的值为false,循环才会退出。
    示例:earlyCloseFn 未读取body就close的会执行此方法,可以发现向 waitForBodyRead推入一个false:waitForBodyRead <- false
    • 若退出循环,最后会执行 readLoop 中的defer()函数。defer函数中的 pc.close(closeErr)不仅会关闭本身的通道closech,也会进而控制 writeLoop的退出。
      在这里插入图片描述

因此,waitForBodyRead这个chan对接下来goroutine的生死起着关键作用。

在这里插入图片描述

总结:

  • 日常开发中,在写代码时基本都不会遗漏 ioutil.ReadAll(resp.Body),但可能会存在忘记写 resp.Body.Close()的情况,这里可能会导致协程泄漏。
    但如果你请求的域名 url 不变的话,那么顶多只会泄漏一个负责 读响应 的goroutine 和一个负责 写请求 的goroutine,不会导致协程数飙升的情况,所以程序运行一般也不会出现什么太明显的问题。
  • 不过还是建议:在执行任何http 请求时,一定要记得加 resp.Body.Close(),避免异常情况下的goroutine泄漏,进而导致内存泄漏(每个goroutine初始时只占几kb,但量多了也扛不住),引起服务异常。
http://www.lryc.cn/news/27945.html

相关文章:

  • 进程信号生命周期详解
  • 2023-03-03干活小计
  • 操作系统结构
  • [SSD科普] 固态硬盘物理接口SATA、M.2、PCIe常见疑问,如何选择?
  • 【Java学习笔记】3.Java 基础语法
  • Python基础学习6——if语句
  • 有免费的PDF转Word吗?值得收藏的7个免费 PDF转Word工具请收好
  • Thinkphp6使用RabbitMQ消息队列
  • 小成本互联网创业怎么做?低成本创业的方法分享
  • 六、栈、栈的相关问题
  • Java安全停止线程
  • 12 readdir 函数
  • Windows环境搭建Android开发环境-Android Studio/Git/JDK
  • 全国爱耳日丨听力受损严重有哪些解决办法
  • 【抽水蓄能电站】基于粒子群优化算法的抽水蓄能电站的最佳调度方案研究(Matlab代码实现)
  • 【异常】因多租户字段缺少导致Error updating database. Column ‘tenant_id‘ cannot be null
  • 类和对象(上)
  • Java经典面试题——谈谈 Java 反射机制,动态代理是基于什么原理?
  • 19 客户端服务订阅机制的核心流程
  • 教师论文|科技专著管理系统
  • 骨传导耳机是什么意思,骨传导耳机的好处具体有哪些
  • elasticsearch—使用汇总
  • 聊一聊代码重构——我们为什么要代码重构
  • 【Python学习笔记】第二十九节 Python2 和Python3发生了哪些变化
  • [oeasy]python0099_雅达利大崩溃_IBM的开放架构_兼容机_oem
  • 学术论文投稿之同行评审过程中可能会遭遇哪些偏见?
  • Python写一个自动发送直播弹幕的工具,非常简单
  • 学生档案管理系统的设计与实现
  • JavaEE学习笔记-SpringBoot快速上手、部分注解解释
  • 【Python学习笔记】第二十六节 Python PyMySQL