golang系统内存溢出

文章讲述了在Golang服务中遇到内存溢出的问题,通过dmesg和pprof分析确定了服务card_game占用内存过大,随后通过添加内存检查函数和日志追踪,发现是由于参数错误导致的无限递归。文章提供了排查和解决内存泄露问题的步骤和常见原因总结。
摘要由CSDN通过智能技术生成

发现问题

在一次压测中,服务会在短时间内崩溃,由于时间短,一时不能通过pprof分析问题找出原因

分析问题

dmesg分析

sudo dmesg -H -T |tailn 200
[Wed Mar  6 09:38:26 2024] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[Wed Mar  6 09:38:26 2024] [4185335]  1000 4185335  2527059  1793783 15605760        0             0 card_game
[Wed Mar  6 09:38:26 2024] [4185336]  1000 4185336   313483     1611   212992        0             0 card_gm
[Wed Mar  6 09:38:26 2024] [4187804]     0 4187804     1486       10    49152        0             0 head
[Wed Mar  6 09:38:26 2024] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=user.slice,mems_allowed=0,global_oom,task_memcg=/user.slice/user-1000.slice/session-237.scope,task=card_game,pid=4185335,uid=1000
[Wed Mar  6 09:38:26 2024] Out of memory: Killed process 4185335 (card_game) total-vm:10108236kB, anon-rss:7175132kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:15240kB oom_score_adj:0

dmesg分析结果显示:服务card_game分配内存已经达到7G多,而机器内存总共才8G,因此内存溢出了

pprof分析

内存达到临界时,主动拒绝服务请求

由于服务崩溃太快,根本抓不到pprof分析报告,想到内存达到临界时,拒绝服务请求,从而有时间抓到pprof报告
在服务请求入口处添加:

func (session *ClientSession) OnReceive(data []byte) {
	defer func() {
		if err := recover(); err != nil {
			logger.ERROR("OnReceive: session panic:%v, %s", err, debug.Stack())
		}
	}()
	// 达到临界时,拒绝服务请求
	if checkMem() == false {
		return
	}
	......
}

func checkMem() bool {
	var mem runtime.MemStats
	runtime.ReadMemStats(&mem)
	total := mem.HeapSys + mem.StackSys + mem.MSpanSys + mem.MCacheSys + mem.BuckHashSys + mem.GCSys + mem.OtherSys
	if mem.Sys >= 6000000000 || mem.Alloc >= 6000000000 || mem.TotalAlloc >= 8000000000 {
		// 打印内存统计信息
		logger.INFO("checkMem: Memory total size=%dbytes, mem.HeapSys=%d, mem.StackSys=%d, mem.MSpanSys=%d, mem.MCacheSys=%d, mem.BuckHashSys=%d, mem.GCSys=%d, mem.OtherSys=%d\n",
			total, mem.HeapSys, mem.StackSys, mem.MSpanSys, mem.MCacheSys, mem.BuckHashSys, mem.GCSys, mem.OtherSys)
		logger.INFO("checkMem: Sys=%d, Alloc=%d, TotalAlloc=%d, NumGC=%d, NumForcedGC=%d", mem.Sys, mem.Alloc, mem.TotalAlloc, mem.NumGC, mem.NumForcedGC) // 已分配的对象大小(字节)
		logger.INFO("checkMem: .........................................\n")
		return false
	}
	return true
}

网上说机器总内存是:total := mem.HeapSys + mem.StackSys + mem.MSpanSys + mem.MCacheSys + mem.BuckHashSys + mem.GCSys + mem.OtherSys	
其实 total  = mem.Sys, 只是系统分配的内存,不是总内存 ,看下文档:
type MemStats struct {
    // 一般统计
    Alloc      uint64 // 已申请且仍在使用的字节数
    TotalAlloc uint64 // 已申请的总字节数(已释放的部分也算在内)
    Sys        uint64 // 从系统中获取的字节数(下面XxxSys之和)
    Lookups    uint64 // 指针查找的次数
    Mallocs    uint64 // 申请内存的次数
    Frees      uint64 // 释放内存的次数
    // 主分配堆统计
    HeapAlloc    uint64 // 已申请且仍在使用的字节数
    HeapSys      uint64 // 从系统中获取的字节数
    HeapIdle     uint64 // 闲置span中的字节数
    HeapInuse    uint64 // 非闲置span中的字节数
    HeapReleased uint64 // 释放到系统的字节数
    HeapObjects  uint64 // 已分配对象的总个数
    // L低层次、大小固定的结构体分配器统计,Inuse为正在使用的字节数,Sys为从系统获取的字节数
    StackInuse  uint64 // 引导程序的堆栈
    StackSys    uint64
    MSpanInuse  uint64 // mspan结构体
    MSpanSys    uint64
    MCacheInuse uint64 // mcache结构体
    MCacheSys   uint64
    BuckHashSys uint64 // profile桶散列表
    GCSys       uint64 // GC元数据
    OtherSys    uint64 // 其他系统申请
    // 垃圾收集器统计
    NextGC       uint64 // 会在HeapAlloc字段到达该值(字节数)时运行下次GC
    LastGC       uint64 // 上次运行的绝对时间(纳秒)
    PauseTotalNs uint64
    PauseNs      [256]uint64 // 近期GC暂停时间的循环缓冲,最近一次在[(NumGC+255)%256]
    NumGC        uint32
    EnableGC     bool
    DebugGC      bool
    // 每次申请的字节数的统计,61是C代码中的尺寸分级数
    BySize [61]struct {
        Size    uint32
        Mallocs uint64
        Frees   uint64
    }
}
MemStats记录内存申请和分配的统计信息,是记录申请和分配信息的。

分析pprof报告

heap报告:
heap统计allocs报告:
allocs统计其中有个模块方法idlePool()突出, 基本断定是这里有异常了

加日志

func (idlemgr *xx) idlePool(idle *IdleData) error {
	if idle.StartTime <= 0 {
		idle.Times += 1
		logger.INFO("idlePool: idle.StartTime:%d <= 0, PlayerId=%d, Times=%d", idle.StartTime, idle.Role.PlayerId, idle.Times)
		...
		 idlemgr.idlePool(idle)
		...
	}
}

再次压测后输出:

2024/03/06 09:37:14.891989 [INFO]idlePool: idle.DurationTime:-875532446 <= 0, PlayerId=1000009, Times=1
2024/03/06 09:37:14.892001 [INFO]idlePool: idle.DurationTime:-875528846 <= 0, PlayerId=1000009, Times=2
2024/03/06 09:37:14.892006 [INFO]idlePool: idle.DurationTime:-875525246 <= 0, PlayerId=1000009, Times=3
......
2024/03/06 09:37:16.504243 [INFO]idlePool: idle.DurationTime:-8846 <= 0, PlayerId=1000009, Times=243202
2024/03/06 09:37:16.504248 [INFO]idlePool: idle.DurationTime:-5246 <= 0, PlayerId=1000009, Times=243203
2024/03/06 09:37:16.504253 [INFO]idlePool: idle.DurationTime:-1646 <= 0, PlayerId=1000009, Times=243204

通过日志分析,有个变量超过int32最大值,数值溢出了,导致无限递归循环
追溯原因,发现客户端有个参数无意中设置为0,而服务端没有验证,因此&^^&

解决问题

服务端调整参数处理

总结

  1. 服务端零信任客户端参数
  2. 在服务中,慎重对待递归处理,不然有时后果非常严重
  3. pprof分析报告显示服务分配内存不大,但系统占用内存非常大,原因可能是方法无限递归了,这部分栈占用算系统的

其他

附其它内存溢出原因

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/ByteDanceTech/article/details/124113705

goroutine 导致内存泄露

  1. goroutine 申请过多
  2. goroutine 阻塞
    • I/O 问题
    • 互斥锁未释放
    • waitgroup 使用不当

select 阻塞

channel 阻塞

  • 写阻塞
    • 无缓冲 channel 的阻塞通常是写操作因为没有读而阻塞
    • 有缓冲的 channel 因为缓冲区满了,写操作阻塞
  • 读阻塞
    • 期待从 channel 读数据,结果没有 goroutine 往进写

定时器使用不当

  1. time.after()使用不当
    默认的 time.After()是会有内存泄漏问题的,因为每次 time.After(duratiuon x)会产生 NewTimer(),在 duration x 到期之前,新创建的 timer 不会被 GC,到期之后才会 GC。
    那么随着时间推移,尤其是 duration x 很大的话,会产生内存泄漏的问题。
  2. time.ticker 未 stop

slice 引起内存泄露

  1. 两个 slice 共享地址,其中一个为全局变量,另一个也无法被 gc;
  2. append slice 后一直使用,未进行清理

排查思路总结

今后遇到 golang 内存泄漏问题可以按照以下几步进行排查解决:

  1. 观察服务器实例,查看内存使用情况,确定内存泄漏问题;

  2. 判断 goroutine 问题;

    • 这里可以使用 1 中提到的监控来观察 goroutine 数量,也可以使用 pprof 进行采样判断
    • 判断 goroutine 数量是否出现了异常增长。
  3. 利用 pprof,通过函数名称定位具体代码行数,可以通过 pprof 的 graph、source 等手段去定位;

    • 排查整个调用链是否出现了上述场景中的问题,如 select 阻塞、channel 阻塞、slice 使用不当等问题,
    • 优先考虑自身代码逻辑问题,其次考虑框架是否存在不合理地方;
  4. 解决对应问题并在测试环境中观察,通过后上线进行观察

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值