Go map发生内存泄漏解决方法
作者:Stefno 发布时间:2024-05-05 09:29:52
Go 程序运行时,有些场景下会导致进程进入某个“高点”,然后就再也下不来了。
比如,多年前曹大写过的一篇文章讲过,在做活动时线上涌入的大流量把 goroutine 数抬升了不少,流量恢复之后 goroutine 数也没降下来,导致 GC 的压力升高,总体的 CPU 消耗也较平时上升了 2 个点左右。
有一个 issue 讨论为什么 allgs(runtime 中存储所有 goroutine 的一个全局 slice) 不收缩,一个好处是:goroutine 复用,让 goroutine 的创建更加得便利,而这也正是 Go 语言的一大优势。
最近在看《100 mistakes》,书里专门有一节讲 map 的内存泄漏。其实这也是另一个在经历大流量后,无法“恢复”的例子:map 占用的内存“只增不减”。
之前写过的一篇《深度解密 Go 语言之 map》里讲到过 map 的内部数据结构,并且分析过创建、遍历、删除的过程。
在 Go runtime 层,map 是一个指向 hmap 结构体的指针,hmap 里有一个字段 B,它决定了 map 能存放的元素个数。
hamp 结构体代码
type hmap struct {
count int
flags uint8
B uint8
// ...
}
若我们想初始化一个长度为 100w 元素的 map,B 是多少呢?
用 B 可以计算 map 的元素个数:loadfactor * 2^B,loadfactor 目前是 6.5,当 B=17 时,可放 851,968 个元素;当 B=18,可放 1,703,936 个元素。因此当我们将 map 的长度初始化为 100w 时,B 的值应是 18。
loadfactor 是装载因子,用来衡量平均一个 bucket 里有多少个 key。
查看占用的内存数量
如何查看占用的内存数量呢?用 runtime.MemStats:
package main
import (
"fmt"
"runtime"
)
const N = 128
func randBytes() [N]byte {
return [N]byte{}
}
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}
func main() {
n := 1_000_000
m := make(map[int][N]byte, 0)
printAlloc()
for i := 0; i < n; i++ {
m[i] = randBytes()
}
printAlloc()
for i := 0; i < n; i++ {
delete(m, i)
}
runtime.GC()
printAlloc()
runtime.KeepAlive(m)
}
如果不加最后的 KeepAlive,m 会被回收掉。
当 N = 128 时,运行程序:
$ go run main2.go
0 MB
461 MB
293 MB
可以看到,当删除了所有 kv 后,内存占用依然有 293 MB,这实际上是创建长度为 100w 的 map 所消耗的内存大小。当我们创建一个初始长度为 100w 的 map:
package main
import (
"fmt"
"runtime"
)
const N = 128
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}
func main() {
n := 1_000_000
m := make(map[int][N]byte, n)
printAlloc()
runtime.KeepAlive(m)
}
运行程序,得到 100w 长度的 map 的消耗的内存为:
$ go run main3.go
293 MB
这时有一个疑惑,为什么在向 map 写入了 100w 个 kv 之后,占用内存变成了 461MB?
我们知道,当 val 大小 <= 128B 时,val 其实是直接放在 bucket 里的,按理说,写入 kv 与否,这些 bucket 占用的内存都在那里。换句话说,写入 kv 之后,占用的内存应该还是 293MB,实际上却是 461MB。
这里的原因其实是在写入 100w kv 期间 map 发生了扩容,buckets 进行了搬迁。我们可以用 hack 的方式打印出 B 值:
func main() {
//...
var B uint8
for i := 0; i < n; i++ {
curB := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(*(**int)(unsafe.Pointer(&m)))) + 9))
if B != curB {
fmt.Println(curB)
B = curB
}
m[i] = randBytes()
}
//...
runtime.KeepAlive(m)
}
运行程序,B 值从 1 一直变到 18。搬迁的过程可以参考前面提到的那篇 map 文章,这里不再赘述。
而如果我们初始化的时候直接将 map 的长度指定为 100w,那内存变化情况为:
293 MB
293 MB
293 MB
当 val 小于 128B 时,初始化 map 后内存占用量一直不变。原因是 put 操作只是在 bucket 里原地写入 val,而 delete 操作则是将 val 清零,bucket 本身还在。因此,内存占用大小不变。
而当 val 大小超过 128B 后,bucket 不会直接放 val,转而变成一个指针。我们将 N 设为 129,运行程序:
0 MB
197 MB
38 MB
虽然 map 的 bucket 占用内存量依然存在,但 val 改成指针存储后内存占用量大大降低。且 val 被删掉后,内存占用量确实降低了。
总之,map 的 buckets 数只会增,不会降。所以在流量冲击后,map 的 buckets 数增长到一定值,之后即使把元素都删了也无济于事。内存占用还是在,因为 buckets 占用的内存不会少。
对于 map 内存泄漏的解法
重启;
将 val 类型改成指针;
定期地将 map 里的元素全量拷贝到另一个 map 里。
好在一般有大流量冲击的互联网业务大都是 toC 场景,上线频率非常高。有的公司能一天上线好几次,在问题暴露之前就已经重启恢复了,问题不大。
来源:https://www.cnblogs.com/qcrao-2018/p/16885760.html
猜你喜欢
- 想要利用Python来操作word文档可以使用docx模块.安装: pip install python-docxfrom docx imp
- 测试方法首先使用implode, serialize, json_encode, msgpack_pack创建四个文本文件,用于测试。创建代
- 定义:select语句中嵌套select语句,被嵌套的select语句是子查询。子查询可以出现在:select ....(select)..
- 以前装过sql server,后来删掉。现在重装,却出现“以前的某个程序安装已在安装计算机上创建挂起的文件操作。运行安装程序之前必须重新启动
- 将近两个多月没有敲linux命令,手指头不听话了。该再次温习卖油翁的故事了。曾为oracle如何启动多个实例运行纠结过,不服输心理。当时在一
- 有的时候,我们为了保持网页的美观,需要将较长的文字在一定长度时截断。比如我们希望在列表中显示文章标题的前15个字,那么一个这样的标题:“rs
- JDBC,Java Database Connectivity,即 Java 数据库连接。实际上 JDBC 是 Java 中的一套和数据库进
- 一、为表创建自增长自段有两种,一种是不同的表使用各自的Sequence,方法如下: 1、在Oracle sequence首先创建sequen
- 1.Training: 如何训练模型一句话理解机器学习一般训练过程 :通过有标签样本来调整(学习)并确定所有权重Weights和偏差Bias
- 在编写程序时,数据库结构会经常变化,所以经常需要编写一些数据库脚本,编写完成后需发往现场执行,如果已经存在或者重复执行,有些脚本会报错,所以
- 前言在制作论文插图时,有时要求将图片的局部放大来展示细节内容,同时将放大图拼接在原图上以方便观察对比。当然直接利用电脑自带的画图软件或者别的
- 用于存储数据的csv文件有时候数据量是十分庞大的,然而我们有时候并不需要全部的数据,我们需要的可能仅仅是前面的几行。这样就可以通过panda
- Python 循环Python 有两个原始的循环命令:while 循环for 循环while 循环如果使用 while 循环,只要条件为真,
- 由于客户提供的是excel文件,在使用时期望使用csv文件格式,且对某些字段内容需要做一些处理,如从某个字段中固定的几位抽取出来,独立作为一
- 无论是 DAO 还是 ADO 都有两种从 Recordset 对象中查询记录的方法: Find 方法和 Seek 方法。在这两种方法中可以让
- python使用ctypes模块调用windows api GetVersionEx获取当前系统版本,没有使用python32#!c:/py
- 首先必须将权重也转为Tensor的cuda格式;然后将该class_weight作为交叉熵函数对应参数的输入值。class_weight =
- python版本:3.8class object: """ The most base type "
- 开发微信小程序过程中,有个需求需要用到日期时间筛选器,查看微信官方文档后,发现官方文档的picker筛选器只能单独支持日期或者是时间,所以为
- 目录主页分析代码:修改视图函数返回值修改 URL 规则修改视图函数名总结追溯到最初,Flask 诞生于 Armin Ronacher 在 2