经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
golang channel 未关闭导致的内存泄漏
来源:cnblogs  作者:Paualf  时间:2023/7/24 8:52:48  对本文有异议

现象

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

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

定位

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

举个例子看下代码:

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "golang.org/x/sync/errgroup"
  6. )
  7. func main() {
  8. readGroup, _ := errgroup.WithContext(context.Background())
  9. consumeGroup, _ := errgroup.WithContext(context.Background())
  10. var (
  11. data = make(chan []int, 10)
  12. )
  13. // 3个生产者往里面进行进行生产
  14. readGroup.Go(func() error {
  15. for i := 0; i < 3; i++ {
  16. data <- []int{i}
  17. }
  18. return nil
  19. })
  20. readGroup.Go(func() error {
  21. for i := 3; i < 6; i++ {
  22. data <- []int{i}
  23. }
  24. return nil
  25. })
  26. readGroup.Go(func() (err error) {
  27. for i := 6; i < 9; i++ {
  28. // error
  29. if i == 7 {
  30. err = fmt.Errorf("error le")
  31. return
  32. }
  33. data <- []int{i}
  34. }
  35. return nil
  36. })
  37. // 其中一个生产者遇到error 返回导致 channel 没有关闭,消费者没有退出
  38. // 1个消费者进行消费
  39. consumeGroup.Go(func() error {
  40. for i := range data {
  41. fmt.Println(i)
  42. }
  43. return nil
  44. })
  45. if err := readGroup.Wait(); err != nil {
  46. fmt.Println(err)
  47. return
  48. }
  49. close(data)
  50. if err := consumeGroup.Wait(); err != nil {
  51. fmt.Println(err)
  52. return
  53. }
  54. fmt.Println("end it")
  55. }

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

最简单的关闭修复
将 close 放到最上面的 defer close(data)

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

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "golang.org/x/sync/errgroup"
  6. )
  7. func main() {
  8. var (
  9. data = make(chan []int, 10)
  10. err error
  11. eg, _ = errgroup.WithContext(context.Background())
  12. )
  13. eg.Go(func() (err error) {
  14. defer close(data)
  15. err = readGroup(data)
  16. return
  17. })
  18. eg.Go(func() (err error) {
  19. err = consumeGroup(data)
  20. return
  21. })
  22. err = eg.Wait()
  23. if err != nil {
  24. return
  25. }
  26. fmt.Println("end it")
  27. }
  28. func consumeGroup(data chan []int) (err error) {
  29. consumeGroup, _ := errgroup.WithContext(context.Background())
  30. consumeGroup.Go(func() error {
  31. for i := range data {
  32. fmt.Println(i)
  33. }
  34. return nil
  35. })
  36. if err = consumeGroup.Wait(); err != nil {
  37. fmt.Println(err)
  38. return
  39. }
  40. return
  41. }
  42. func readGroup(data chan []int) (err error) {
  43. readGroup, _ := errgroup.WithContext(context.Background())
  44. // 3个生产者往里面进行进行生产
  45. readGroup.Go(func() error {
  46. for i := 0; i < 3; i++ {
  47. data <- []int{i}
  48. }
  49. return nil
  50. })
  51. readGroup.Go(func() error {
  52. for i := 3; i < 6; i++ {
  53. data <- []int{i}
  54. }
  55. return nil
  56. })
  57. readGroup.Go(func() (err error) {
  58. for i := 6; i < 9; i++ {
  59. // error
  60. if i == 7 {
  61. err = fmt.Errorf("error le")
  62. return
  63. }
  64. data <- []int{i}
  65. }
  66. return nil
  67. })
  68. if err = readGroup.Wait(); err != nil {
  69. fmt.Println(err)
  70. return
  71. }
  72. return
  73. }

修复

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

参考文章:

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

原文链接:https://www.cnblogs.com/zhangpengfei5945/p/17575353.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号