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内存泄漏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言中常用的基础方法总结

    Go语言中常用的基础方法总结

    这篇文章主要为大家详细介绍了Go语言中常用的一些基础方法,例如:使用正则表达式验证字符串、格式化字符串、时间的比较等等,需要的可以参考一下
    2022-09-09
  • Go语言map字典用法实例分析

    Go语言map字典用法实例分析

    这篇文章主要介绍了Go语言map字典用法,实例分析了map字典的使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • Go语言实现Sm2加解密的示例代码

    Go语言实现Sm2加解密的示例代码

    本文主要介绍了Go语言实现Sm2加解密的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • golang中json反序列化可能遇到的问题

    golang中json反序列化可能遇到的问题

    这篇文章主要给大家介绍了关于golang中json反序列化可能遇到的问题的解决方法,文中通过示例代码介绍的非常详细,对大家学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-01-01
  • golang解析html网页的方法

    golang解析html网页的方法

    今天小编就为大家分享一篇golang解析html网页的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-08-08
  • 一文带你搞懂Golang依赖注入的设计与实现

    一文带你搞懂Golang依赖注入的设计与实现

    在现代的 web 框架里面,基本都有实现了依赖注入的功能,可以让我们很方便地对应用的依赖进行管理。今天我们来看看 go 里面实现依赖注入的一种方式,感兴趣的可以了解一下
    2023-01-01
  • go语言算法题解二叉树的拷贝、镜像和对称

    go语言算法题解二叉树的拷贝、镜像和对称

    这篇文章主要为大家详细介绍了go语言算法题解二叉树的拷贝、镜像和对称,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-01-01
  • Go语言为什么很少使用数组原理解析

    Go语言为什么很少使用数组原理解析

    这篇文章主要为大家介绍了Go语言为什么很少使用数组原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Go语言排序算法之插入排序与生成随机数详解

    Go语言排序算法之插入排序与生成随机数详解

    从这篇文章开始将带领大家学习Go语言的经典排序算法,比如插入排序、选择排序、冒泡排序、希尔排序、归并排序、堆排序和快排,二分搜索,外部排序和MapReduce等,本文将先详细介绍插入排序,并给大家分享了go语言生成随机数的方法,下面来一起看看吧。
    2017-11-11
  • Go单例模式与Once源码实现

    Go单例模式与Once源码实现

    这篇文章主要介绍了Go单例模式与Once源码实现,本文结合示例代码给大家讲解的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12

最新评论