首页 > 系统相关 >golang channel 未关闭导致的内存泄漏

golang channel 未关闭导致的内存泄漏

时间:2023-07-23 18:11:39浏览次数:59  
标签:err golang 内存 func error Go data 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
}

修复

将生产者放在一个 goroutint 里面,最后如果遇到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 中只有值传递,引用传递是修改了同一个指向内存地址中的值

参考文章:
如何优雅地关闭Go channel
Go语言参数传递是传值还是传引用

标签:err,golang,内存,func,error,Go,data,channel
From: https://www.cnblogs.com/zhangpengfei5945/p/17575353.html

相关文章

  • Java虚拟机(JVM):第六幕:自动内存管理 - 选择合适的垃圾收集器
    前言:在虚拟机的世界里面,内置了很多的垃圾收集器,但并不是说最先进的就是最好的。有一句话说的好“因地制宜”;一、Epsilon收集器是一个无操作的收集器,但是贴切的来说是“自动内存管理子系统”。但是一个垃圾收集器的工作不仅仅只有垃圾收集,还负责堆的管理与布局、对象的分配、......
  • java 内存数据压缩
    Java内存数据压缩简介Java是一种高级编程语言,它在运行时需要使用内存来存储数据。然而,随着应用程序和数据的大小越来越大,内存的使用量也在增加。为了节省内存并提高性能,Java提供了内存数据压缩的功能。内存数据压缩是一种将数据在内存中进行压缩以减少其占用空间的技术。在Java......
  • java 内存升高不降
    Java内存升高不降Java是一种高级编程语言,它的内存管理是由Java虚拟机(JVM)来负责的。在Java中,内存由Java堆和方法区组成。Java堆用于存储对象实例,方法区用于存储类信息和静态变量。然而,有时候我们会遇到Java内存占用升高但不降的情况。这可能会导致程序的性能下降,并最终......
  • java 内存配置
    Java内存配置Java是一种面向对象的编程语言,它在内存管理方面与其他编程语言有所不同。Java使用一种称为Java虚拟机(JVM)的技术来管理内存。在编写Java程序时,了解和配置Java内存是非常重要的,可以提高程序的性能和可靠性。Java内存模型Java内存模型(JVM)由以下几个主要组件组成:堆(He......
  • 根据内存占据的大小排列,看到C盘里面哪个文件占据流量最大?
    大家好,我是皮皮。一、前言这篇文章还得从前几天在Python最强王者群【......
  • 使用golang灵活处理动态文案
    代码packagescripts_stroageimport("fmt""github.com/duke-git/lancet/v2/slice""github.com/gogf/gf/util/gconv""github.com/gookit/goutil/dump""regexp""strings"&q......
  • python 实现共享内存
    Python实现共享内存在Python中,共享内存是一种进程间通信的方式,用于在多个进程之间共享数据。使用共享内存可以提高程序的性能和效率。本文将向刚入行的小白开发者介绍如何在Python中实现共享内存,以及每个步骤需要做的事情和相应的代码。1.确定共享内存的数据类型和大小在开始之......
  • python如何获取本程序内存基址
    Python如何获取本程序内存基址在Python中,我们可以使用ctypes模块来获取本程序的内存基址。内存基址是进程中所有数据的起始地址,通常在程序运行时是不变的。下面将介绍如何使用ctypes模块来获取本程序的内存基址,并且给出一个具体的问题来解决。使用ctypes模块获取内存基址ctypes......
  • golang 实现 RPC(远程过程调⽤实例)
    golang实现RPC 前提概要,先了解几个概念,websocket简介什么是websocketWebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议webs......
  • java内存分析工具使用
    目录参考链接1、名词解释2、常见异常java堆内存异常java栈内存异常方法区内存异常3、解决思路4、生成dump文件的三种方式jvisualvmjmap命令应用启动配置5、MemoryAnalyzer分析工具参考链接Java内存溢出OOM之dump分析1、名词解释内存泄露:代码中的某个对象本应该被虚拟机回收,......