golang系统内存溢出
发现问题
在一次压测中,服务会在短时间内崩溃,由于时间短,一时不能通过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报告:
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,而服务端没有验证,因此&^^&
解决问题
服务端调整参数处理
总结
- 服务端零信任客户端参数
- 在服务中,慎重对待递归处理,不然有时后果非常严重
- pprof分析报告显示服务分配内存不大,但系统占用内存非常大,原因可能是方法无限递归了,这部分栈占用算系统的
其他
附其它内存溢出原因
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/ByteDanceTech/article/details/124113705
goroutine 导致内存泄露
- goroutine 申请过多
- goroutine 阻塞
- I/O 问题
- 互斥锁未释放
- waitgroup 使用不当
select 阻塞
channel 阻塞
- 写阻塞
- 无缓冲 channel 的阻塞通常是写操作因为没有读而阻塞
- 有缓冲的 channel 因为缓冲区满了,写操作阻塞
- 读阻塞
- 期待从 channel 读数据,结果没有 goroutine 往进写
定时器使用不当
- time.after()使用不当
默认的 time.After()是会有内存泄漏问题的,因为每次 time.After(duratiuon x)会产生 NewTimer(),在 duration x 到期之前,新创建的 timer 不会被 GC,到期之后才会 GC。
那么随着时间推移,尤其是 duration x 很大的话,会产生内存泄漏的问题。 - time.ticker 未 stop
slice 引起内存泄露
- 两个 slice 共享地址,其中一个为全局变量,另一个也无法被 gc;
- append slice 后一直使用,未进行清理
排查思路总结
今后遇到 golang 内存泄漏问题可以按照以下几步进行排查解决:
-
观察服务器实例,查看内存使用情况,确定内存泄漏问题;
-
判断 goroutine 问题;
- 这里可以使用 1 中提到的监控来观察 goroutine 数量,也可以使用 pprof 进行采样判断
- 判断 goroutine 数量是否出现了异常增长。
-
利用 pprof,通过函数名称定位具体代码行数,可以通过 pprof 的 graph、source 等手段去定位;
- 排查整个调用链是否出现了上述场景中的问题,如 select 阻塞、channel 阻塞、slice 使用不当等问题,
- 优先考虑自身代码逻辑问题,其次考虑框架是否存在不合理地方;
-
解决对应问题并在测试环境中观察,通过后上线进行观察