• 听说过对 Go map 做 GC 吗?


    在 Golang 中的 map 结构,在删除键值对的时候,并不会真正的删除,而是标记。那么随着键值对越来越多,会不会造成大量内存浪费?

    首先答案是会的,很有可能导致 OOM,而且针对这个还有一个讨论:https://github.com/golang/go/issues/20135。大致的意思就是在很大的 map 中,delete 操作没有真正释放内存而可能导致内存 OOM。

    所以一般的做法:就是 重建map。而 go-zero 中内置了 safemap 的容器组件。safemap 在一定程度上可以避免这种情况发生。

    那首先我们看看 go 原生提供的 map 是怎么删除的?

    原生map删除

    1  package main
    2
    3  func main() {
    4      m := make(map[int]string, 9)
    5      m[1] = "hello"
    6      m[2] = "world"
    7      m[3] = "go"
    8
    9      v, ok := m[1]
    10     _, _ = fn(v, ok)
    11
    12     delete(m, 1)
    13  }
    14
    15 func fn(v string, ok bool) (string, bool) {
    16     return v, ok
    17 }
    

    测试代码如上,我们可以通过 go tool compile -S -N -l testmap.go | grep "CALL"

    0x0071 00113 (test/testmap.go:4)        CALL    runtime.makemap(SB)
    0x0099 00153 (test/testmap.go:5)        CALL    runtime.mapassign_fast64(SB)
    0x00ea 00234 (test/testmap.go:6)        CALL    runtime.mapassign_fast64(SB)
    0x013b 00315 (test/testmap.go:7)        CALL    runtime.mapassign_fast64(SB)
    0x0194 00404 (test/testmap.go:9)        CALL    runtime.mapaccess2_fast64(SB)
    0x01f1 00497 (test/testmap.go:10)       CALL    "".fn(SB)
    0x0214 00532 (test/testmap.go:12)       CALL    runtime.mapdelete_fast64(SB)
    0x0230 00560 (test/testmap.go:7)        CALL    runtime.gcWriteBarrier(SB)
    0x0241 00577 (test/testmap.go:6)        CALL    runtime.gcWriteBarrier(SB)
    0x0252 00594 (test/testmap.go:5)        CALL    runtime.gcWriteBarrier(SB)
    0x025c 00604 (test/testmap.go:3)        CALL    runtime.morestack_noctxt(SB)
    

    执行第12行的 delete,实际执行的是 runtime.mapdelete_fast64

    这些函数的参数类型是具体的 int64mapdelete_fast64 跟原始的 delete 操作一样的,所以我们来看看 mapdelete

    mapdelete

    长图预警!!!

    大致代码分析如上,具体代码就留给大家去阅读了。其实大致过程:

    1. 写保护,防止并发写
    2. 查询要删除的 key 是否存在
    3. 存在则对其标志做删除标记
    4. count--

    所以你在大面积删除 key ,实际 map 存储的 key 是不会删除的,只是标记当前的key状态为 empty

    其实出发点,和 mysql 的标记删除类似,防止后续会有相同的 key 插入,省去了扩缩容的操作。

    但是这个对有些场景是不妥的,如果开发者在未来时间内都不会再插入相同的 key ,很可能会导致 OOM

    所以针对以上情况,go-zero 开发了 safemap 。下面我们看看 safemap 是如何避免这个问题的?

    safemap

    直接从操作 safemap 中分析为什么要这么设计:

    1. 预设一个 删除阈值,如果触发会放到一个新预设好的 newmap
    2. 两个 map 是一个整体,所以 key 只能留一份

    所以为什么要设置两个 map 就很清楚了:

    1. dirtyOld 作为存储主体,如果 delete 操作达到阈值,则会触发迁移。
    2. 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 支持我们!

    微信交流群

    关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

  • 相关阅读:
    ObjectiveC字符串处理
    分享 10 个 jQuery 的语言翻译插件
    30 个实用的 jQuery 选项卡/导航教程推荐
    iphoneCocos2D游戏开发
    cocos2d和unity3d的比较
    将NSString转换编码集变为GBK或GB2312
    超过 40 款很有用而且很新的 jQuery 插件
    表格单元的表现形式
    ShareKit
    UI Prototype Design IDE( 界面原型设计工具 )
  • 原文地址:https://www.cnblogs.com/kevinwan/p/15009965.html
Copyright © 2020-2023  润新知