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使用grpc+go-kit模拟oauth认证的操作

    golang使用grpc+go-kit模拟oauth认证的操作

    这篇文章主要介绍了golang使用grpc+go-kit模拟oauth认证的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go语言使用protojson库实现Protocol Buffers与JSON转换

    Go语言使用protojson库实现Protocol Buffers与JSON转换

    本文主要介绍Google开源的工具库Protojson库如何Protocol Buffers与JSON进行转换,以及和标准库encoding/json的性能对比,需要的朋友可以参考下
    2023-09-09
  • Golang实现gRPC的Proxy的原理解析

    Golang实现gRPC的Proxy的原理解析

    gRPC是Google开始的一个RPC服务框架, 是英文全名为Google Remote Procedure Call的简称,广泛的应用在有RPC场景的业务系统中,这篇文章主要介绍了Golang实现gRPC的Proxy的原理,需要的朋友可以参考下
    2021-09-09
  • gORM操作MySQL的实现

    gORM操作MySQL的实现

    本文主要介绍了gORM操作MySQL的实现,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2021-07-07
  • Goland IDEA项目多开设置方式

    Goland IDEA项目多开设置方式

    这篇文章主要介绍了Goland IDEA项目多开设置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • go xorm存库处理null值问题

    go xorm存库处理null值问题

    这篇文章主要介绍了go xorm存库处理null值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • 浅谈Go切片的值修改是否会覆盖数组的值 

    浅谈Go切片的值修改是否会覆盖数组的值 

    本文主要介绍了浅谈Go切片的值修改是否会覆盖数组的值,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 
    2022-02-02
  • 浅谈go语言中别名类型的使用

    浅谈go语言中别名类型的使用

    类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题,本文主要介绍了go语言中别名类型的使用,感兴趣的可以了解一下
    2024-01-01
  • go使用net/url包来解析URL提取主机部分

    go使用net/url包来解析URL提取主机部分

    这篇文章主要为大家介绍了go使用net/url包来解析URL提取主机部分实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Golang并发编程之Channel详解

    Golang并发编程之Channel详解

    传统的并发编程模型是基于线程和共享内存的同步访问控制的,共享数据受锁的保护,使用线程安全的数据结构会使得这更加容易。本文将详细介绍Golang并发编程中的Channel,,需要的朋友可以参考下
    2023-05-05

最新评论