听说过对 Go map 做 GC 吗?
在 Golang 中的 map 结构,在删除键值对的时候,并不会真正的删除,而是标记。那么随着键值对越来越多,会不会造成大量内存浪费?
首先答案是会的,很有可能导致 OOM,而且针对这个还有一个讨论:https://github.com/golang/go/issues/20135。大致的意思就是在很大的 map
中,delete
操作没有真正释放内存而可能导致内存 OOM。
所以一般的做法:就是 重建 map。而 go-zero
中内置了 safemap
的容器组件。safemap
在一定程度上可以避免这种情况发生。
那首先我们看看 go
原生提供的 map
是怎么删除的?
原生 map 删除
测试代码如上,我们可以通过 go tool compile -S -N -l testmap.go | grep "CALL"
:
执行第 12 行的 delete
,实际执行的是 runtime.mapdelete_fast64
。
这些函数的参数类型是具体的 int64
,mapdelete_fast64
跟原始的 delete
操作一样的,所以我们来看看 mapdelete
。
mapdelete
长图预警!!!
大致代码分析如上,具体代码就留给大家去阅读了。其实大致过程:
写保护,防止并发写
查询要删除的
key
是否存在存在则对其标志做删除标记
count--
所以你在大面积删除 key
,实际 map
存储的 key
是不会删除的,只是标记当前的 key 状态为 empty
。
其实出发点,和 mysql
的标记删除类似,防止后续会有相同的 key
插入,省去了扩缩容的操作。
但是这个对有些场景是不妥的,如果开发者在未来时间内都不会再插入相同的 key
,很可能会导致 OOM
。
所以针对以上情况,go-zero
开发了 safemap
。下面我们看看 safemap
是如何避免这个问题的?
safemap
直接从操作 safemap
中分析为什么要这么设计:
预设一个 删除阈值,如果触发会放到一个新预设好的
newmap
中两个
map
是一个整体,所以key
只能留一份
所以为什么要设置两个 map
就很清楚了:
dirtyOld
作为存储主体,如果delete
操作达到阈值,则会触发迁移。dirtyNew
作为暂存体,会在到达阈值时,存放部分key/value
所以在迁移操作时,我们需要做的就是:将原先的 dirtyOld
清空,存储的 key/value 通过 for-range 重新存储到 dirtyNew
,然后将 dirtyNew
指向 dirtyOld
。
可能会有疑问:不是说
key/value
没有删除吗,只是标记了tophash=empty
其实在
for-range
过程中,会过滤掉tophash <= emptyOne
的 key
这样就实现了不需要的 key 不会被加入到 dirtyNew
,进而不会影响 dirtyOld
。
这其实也就是垃圾回收的年老代和新生代的概念。
更多实现细节,可以查看源码!
项目地址
https://github.com/tal-tech/go-zero
欢迎使用 go-zero 并 star 支持我们!
微信交流群
关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
版权声明: 本文为 InfoQ 作者【万俊峰Kevin】的原创文章。
原文链接:【http://xie.infoq.cn/article/594485a27e842c1aaab8d3132】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论