那些年我们一起踩过的Golang坑(一)

可能引起内存泄漏的time.After()方法

接触golang已经有半年的时间了, 这段时间在写一个MQTT的gateway, 架构很简单用golang实现MQTT协议, 用kafka作为消息中间件服务.

今天要记录的是在使用过程中遇到的一个坑:

场景: 用测试用例往gateway中发数据, 500个线程, 每个线程发20k条, 不间断, 所以发了10M条数据, 每条的内容是18-20个字节不等的MQTT消息. 省去调试的过程, 10M条消息在测试端都接收到了, 消息峰值大概是10k条/s(qos=0) 但是打开任务管理器(我用的windows开发环境)发现server服务的占用内存始终在1G+下不来.

于是准备用pprof作内存分析, pprof的使用, 移步 pprof使用描述

隐式引入pprof

_ "net/http/pprof"

在主进程中加入监听

	go func() {
		http.ListenAndServe("0.0.0.0:9090", nil)
	}()

然后重新跑一遍测试用例. 结束之后, server的内存在1G+.

命令行调用

go tool pprof http=localhost:9090/debug/pprof/heap

在命令中输入web, 可以打开图形界面观察

Fetching profile over HTTP from http://localhost:9090/debug/pprof/heap
Saved profile in C:\Users\Elvis\pprof\pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.005.pb.gz
Type: inuse_space
Time: Aug 21, 2019 at 10:10am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web

在这里插入图片描述
从图片中可以看到, time.NewTimer 占用了83.44%的堆内存, 并且随着时间的推移, 这部分占用的内存并没有得到释放.

在箭头上方的提示中找到这段代码的位置是在mdmp_handler中, time.After()中的调用.

找到出错处的源码:

if connMsg.FixedHeader.PackageType == constant.MQTT_MSG_TYPE_CONNECT && h.dealConnect(connMsg, client) {
		x := make(chan bool)
		for {
			//监听超时, 异步读取数据
			go func() {
				n, err = reader.Read(buff)
				logger.Debug("received bytes of ", n, " ", err)
				if n > 0 {
					client.DealByteArray(buff[:n])
					x <- true
					return
				}
				if err != nil {
					if err == io.EOF {
						logger.Info("connection closed by client")
					} else {
						client.Will(connMsg)
						logger.Error(err.Error())
					}
					client.Closing <- true
					return
				}
			}()
			select {
			case <-x: //正常收取消息
				continue
			case <-time.After(time.Duration(3*connMsg.VariableHeader.KeepAliveTimer/2) * time.Second): //超时
				logger.Warn("connection time out")
				client.Will(connMsg)
				h.activeConn.Delete(client)
				client.Close()
				return
			case <-client.Closing: //客户端关闭
				h.activeConn.Delete(client)
				client.Close()
				return
			}
		}
	}

为了确保是time.After()导致的内存占用, 注释掉了

//			case <-time.After(time.Duration(3*connMsg.VariableHeader.KeepAliveTimer/2) * time.Second): //超时
//				logger.Warn("connection time out")
//				client.Will(connMsg)
//				h.activeConn.Delete(client)
//				client.Close()
//				return

再测, 10M条数据过后, server的内存占用稳定在60M上下, 在测试的过程中也一直稳定在这个数值内

居然就是这里!

同时, 我猜测是golang在for循环结束之前资源没有自动gc, 于是编写了测试用例, 将timer放在gorouting中(gorouting结束了肯定要GC吧), 进行测试:

package timer

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

func TestName(t *testing.T) {
	wg := sync.WaitGroup{}
	ch := make(chan bool)
	//timeout := make(chan bool)
	for i := 0; i < 10000000; i++ {
		wg.Add(1)
		go func() {
			ch <- true
		}()
		select {
		case <-ch:
			//fmt.Println(i, " finished")
		case <-time.After(10 * time.Second):
			fmt.Println(i, " timed out")
		}
		wg.Done()
	}
	wg.Wait()
	fmt.Println("all finished")
	time.Sleep(1000 * time.Second)
}

结果是打印了 "all finished"之后, 内存持续占用2G以上!

以上, time.After()不释放内存和for循环无关.

再观察一下time.After()的源码

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
	return NewTimer(d).C
}
The underlying Timer is not recovered by the garbage collector until the timer fires. If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.
垃圾收集器无法恢复基础Timer, 直到计时器开火。 如果要考虑效率,请使用NewTimer, 相反,如果不再需要定时器,则调用Timer.Stop。

GC不会自动回收time.After()的timer, 需要调用, time.Timer.Stop()释放掉内存

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang每日一库是一个系列的文章,旨在介绍Golang中的各种优秀的第三方库。其中一篇文章介绍了zap库和logrus库。\[1\]zap库是Golang中性能最高的日志库,而logrus库虽然已经不再维护和更新,但在使用的简易程度方面更好用一些。\[1\]除了这两个库,还有很多其他的日志库可以选择。另外,flag库也是Golang中常用的库之一,用于处理命令行参数。\[2\]\[3\]flag库提供了多种选项类型,包括自定义选项类型和时间间隔类型。自定义选项类型可以根据需求进行定义,而时间间隔类型支持多种格式,如"300ms"、"-1.5h"、"2h45m"等。\[2\]\[3\]这些库都是Golang开发中常用的工具,可以帮助开发者更高效地进行日志记录和命令行参数处理。 #### 引用[.reference_title] - *1* [Golang一日一库之logrus](https://blog.csdn.net/Zuko_chen/article/details/130212672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Go每日一库之Flag](https://blog.csdn.net/weixin_43999327/article/details/130701461)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值