Golang因Channel未关闭导致内存泄漏的解决方案详解

 更新时间:2023年07月24日 09:27:31   作者:Paualf  
这篇文章主要为大家详细介绍了当Golang因Channel未关闭导致内存泄漏时盖如何解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下

现象

某一个周末我们的服务 oom了,一个比较重要的job 没有跑完,需要重跑,以为是偶然,重跑成功,因为是周末没有去定位原因
又一个工作日,它又oom了,重跑成功,持续观察,job 在oom之前竟然占用了30g左右(这里我们的任务比较大的数据量都在内存中计算,所以这里机器内存量大一点)

应用使用30g内存肯定是不正常的,怀疑内存泄漏了,怎么定位内存泄漏呢?

定位

搜了一下网上经常用到的工具是 go 的 pprof 火焰图,自己在本地跑了一下,因为数据量比较少,并没有发现什么,暂时放下了。
后续某个早上在公司工具里面打开了一下,发现有火焰图的工具,打开看了一下一个函数占用了 7224.46mb,占用了 7个g, 而且这个函数是已经跑完了,这个时候定位到那个函数了,和旁边同事说了一下,同事帮忙看了下邮件告警,每个下午都会有任务失败告警(任务失败会进行重试的); 这里怀疑是失败了, channel 没有关闭,导致 消费的go routine 没有回收。

举个例子看下代码:

package main
import (
	"context"
	"fmt"
	"golang.org/x/sync/errgroup"
)
func main() {
	readGroup, _ := errgroup.WithContext(context.Background())
	consumeGroup, _ := errgroup.WithContext(context.Background())
	var (
		data = make(chan []int, 10)
	)
	//  3个生产者往里面进行进行生产
	readGroup.Go(func() error {
		for i := 0; i < 3; i++ {
			data <- []int{i}
		}
		return nil
	})
	readGroup.Go(func() error {
		for i := 3; i < 6; i++ {
			data <- []int{i}
		}
		return nil
	})
	readGroup.Go(func() (err error) {
		for i := 6; i < 9; i++ {
			// error
			if i == 7 {
				err = fmt.Errorf("error le")
				return
			}
			data <- []int{i}
		}
		return nil
	})
	// 其中一个生产者遇到error 返回导致 channel 没有关闭,消费者没有退出
	// 1个消费者进行消费
	consumeGroup.Go(func() error {
		for i := range data {
			fmt.Println(i)
		}
		return nil
	})
	if err := readGroup.Wait(); err != nil {
		fmt.Println(err)
		return
	}
	close(data)
	if err := consumeGroup.Wait(); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("end it")
}

这个case里面,readGroup 遇到error 直接退出了,channel并没有关闭,如果是常驻进程的程序,消费的go routine 并没有回收,就导致了内存泄漏

最简单的关闭修复

将 close 放到最上面的 defer close(data)

不过最好的还是生产者进行关闭,我们可以优化一下代码,把生产者的代码放到一个函数中,这样就可以让生产者去进行关闭的操作了

package main
import (
	"context"
	"fmt"
	"golang.org/x/sync/errgroup"
)
func main() {
	var (
		data = make(chan []int, 10)
		err  error
		eg, _ = errgroup.WithContext(context.Background())
	)
	eg.Go(func() (err error) {
		defer close(data)
		err = readGroup(data)
		return
	})
	eg.Go(func() (err error) {
		err = consumeGroup(data)
		return
	})
	err = eg.Wait()
	if err != nil {
		return
	}
	fmt.Println("end it")
}
func consumeGroup(data chan []int) (err error) {
	consumeGroup, _ := errgroup.WithContext(context.Background())
	consumeGroup.Go(func() error {
		for i := range data {
			fmt.Println(i)
		}
		return nil
	})
	if err = consumeGroup.Wait(); err != nil {
		fmt.Println(err)
		return
	}
	return
}
func readGroup(data chan []int) (err error) {
	readGroup, _ := errgroup.WithContext(context.Background())
	//  3个生产者往里面进行进行生产
	readGroup.Go(func() error {
		for i := 0; i < 3; i++ {
			data <- []int{i}
		}
		return nil
	})
	readGroup.Go(func() error {
		for i := 3; i < 6; i++ {
			data <- []int{i}
		}
		return nil
	})
	readGroup.Go(func() (err error) {
		for i := 6; i < 9; i++ {
			// error
			if i == 7 {
				err = fmt.Errorf("error le")
				return
			}
			data <- []int{i}
		}
		return nil
	})
	if err = readGroup.Wait(); err != nil {
		fmt.Println(err)
		return
	}
	return
}

修复

将生产者放在一个 go routine 里面,最后如果遇到error的话 defer()的时候会把channel给关闭了

The Channel Closing Principle
One general principle of using Go channels is don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. In other words, we should only close a channel in a sender goroutine if the sender is the only sender of the channel.

简单点:就是在生产者中进行channel的关闭

后续讨论和遇到的新问题

拆分代码函数的时候又遇到新的问题了,有一个切片数组我拆分函数的时候,我没有去接受切片函数的返回值,导致了切片发生扩容返回的是一个空切片,并没有修改掉原来的切片。之前以为在golang里面切片是引用类型,会自动改变其中的值最后查了一下,在go 里面都是值传递,可以修改其中的值其实是使用了指针修改了同一块地址中的值所以值发生了变化

总结

使用channel 的时候在生产者中进行关闭,思考一些遇到error的时候channel是否可以正常的关闭

go 中只有值传递,引用传递是修改了同一个指向内存地址中的值

参考文章

Golang优雅关闭channel的方法示例

Go语言参数传递是传值还是传引用

到此这篇关于Golang因Channel未关闭导致内存泄漏的解决方案详解的文章就介绍到这了,更多相关Golang Channel内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang beego框架环境搭建过程

    golang beego框架环境搭建过程

    这篇文章主要为大家介绍了golang beego框架环境搭建的过程脚本,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 基于Golang实现内存数据库的示例详解

    基于Golang实现内存数据库的示例详解

    这篇文章主要为大家详细介绍了如何基于Golang实现内存数据库,文中的示例代码讲解详细,具有一定的借鉴价值,需要的小伙伴可以参考一下
    2023-03-03
  • golang 四则运算计算器yacc归约手写实现

    golang 四则运算计算器yacc归约手写实现

    这篇文章主要为大家介绍了golang 四则运算 计算器 yacc 归约的手写实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Go语言实战学习之流程控制详解

    Go语言实战学习之流程控制详解

    这篇文章主要为大家详细介绍了Go语言中的流程控制,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助 ,需要的朋友可以参考下
    2022-08-08
  • Go语言字符串及strings和strconv包使用实例

    Go语言字符串及strings和strconv包使用实例

    字符串是工作中最常用的,值得我们专门的练习一下,下面这篇文章主要给大家介绍了关于Go语言字符串及strings和strconv包使用的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-06-06
  • Golang使用gob实现结构体的序列化过程详解

    Golang使用gob实现结构体的序列化过程详解

    Golang struct类型数据序列化用于网络传输数据或在磁盘上写入数据。在分布式系统中,一端生成数据、然后序列化、压缩和发送;在另一端,接收数据、然后解压缩、反序列化和处理数据,整个过程必须快速有效
    2023-03-03
  • Go实现socks5服务器的方法

    Go实现socks5服务器的方法

    SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全,这篇文章主要介绍了Go实现socks5服务器的方法,需要的朋友可以参考下
    2023-07-07
  • 详解go 中的 fmt 占位符

    详解go 中的 fmt 占位符

    这篇文章主要介绍了go 中的 fmt 占位符,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • Golang中的闭包(Closures)详解

    Golang中的闭包(Closures)详解

    在 Golang 中,闭包是一个引用了作用域之外的变量的函数,Golang 中的匿名函数也被称为闭包,闭包可以被认为是一种特殊类型的匿名函数,所以本文就给大家详细的介绍一下Golang的闭包到底是什么,感兴趣的小伙伴跟着小编一起来看看吧
    2023-07-07
  • GoFrame框架gset交差并补集使用实例

    GoFrame框架gset交差并补集使用实例

    这篇文章主要为大家介绍了GoFrame框架gset交差并补集使用实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06

最新评论