V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
matytan
V2EX  ›  Go 编程语言

分享一个自己 golang 的库,用于尽量不 GC 的内存池

  •  
  •   matytan · 2023-07-12 13:48:30 +08:00 · 3022 次点击
    这是一个创建于 505 天前的主题,其中的信息可能已经有所发展或是发生改变。

    实现内存池用于对切片对象进行复用,减少内存分配次数 Mempool 是一个内存池库,用于在处理字节缓冲区时减少分配并提高性能。 https://github.com/matyle/mempool

    28 条回复    2023-07-15 10:59:47 +08:00
    alexsunxl
        1
    alexsunxl  
       2023-07-12 14:10:33 +08:00
    buffer 的内存池。
    没点进去之前我以为是变量的内存池,哈哈哈
    vfs
        2
    vfs  
       2023-07-12 14:17:29 +08:00
    提高性能? 应该数据说一下。
    Mohanson
        3
    Mohanson  
       2023-07-12 14:20:50 +08:00
    为什么不用标准库里的 sync.Pool ... ...
    zdt3476
        4
    zdt3476  
       2023-07-12 14:42:45 +08:00
    看了下,只是 bytes.Buffer 的内存池,用途感觉不是很广; 而且也没有和 sync.Pool 对比的 benchmark
    matytan
        5
    matytan  
    OP
       2023-07-12 14:45:40 +08:00
    @vfs 嗯嗯准备做个 benchmark ,已经在业务中体现了,还没做 benchmark
    matytan
        6
    matytan  
    OP
       2023-07-12 14:46:54 +08:00
    @zdt3476 嗯嗯 用途不是很广,主要是防止多协程频繁 GC ,会做个 benchmark ,README 没有贴上我业务内存的对比图
    matytan
        7
    matytan  
    OP
       2023-07-12 14:49:52 +08:00
    @Mohanson 最开始业务里面就是用的 sync.pool 但是在大内存业务下,gc 会撑爆内存,后面使用了这个限制 gc
    kkhaike
        8
    kkhaike  
       2023-07-12 15:19:35 +08:00
    。。。解释下,为啥用了 sync.pool ,gc 会撑爆内存。。。
    matytan
        9
    matytan  
    OP
       2023-07-12 15:31:52 +08:00
    @kkhaike 本质上是 json 的序列化导致 makeslice 很大,使用 syncpool 并没有限制协程的 buffer 对象,来不及 gc 导致某一时刻峰值很大
    hopingtop
        10
    hopingtop  
       2023-07-12 17:15:42 +08:00   ❤️ 3
    我们也遇到 op 一样的问题,json 序列化会撑爆内存,就是因为 sync.Pool + json.Buffer 导致的。
    当 99%的数据是小包 1%的数据突然来一个几十 MB 的大包,那么有可能后面 sync.Pool 里面的 buffer 都会变成几十 MB ,就会导致内存爆掉。

    这个问题,Golang 有最新的提案和实现, 就是动态优化 buffer 的大小。 但是还没有合并!

    相关 code 链接 https://go-review.googlesource.com/c/go/+/471200
    Trim21
        11
    Trim21  
       2023-07-12 17:22:22 +08:00 via Android   ❤️ 1
    @hopingtop https://github.com/valyala/bytebufferpool 可以用 fasthttp 作者写的这个,如果少数 buffer 特别大会直接丢掉对应的 buffer 。
    hopingtop
        12
    hopingtop  
       2023-07-12 17:23:20 +08:00
    我才看了 op 的代码,如果你真的想实现 mem 的高效利用,可以参考上面的链接实现或许会更好!

    目前 mempool 有点问题就是,限制了上限,但是释放不了下限,没有考虑到 release 机制, 最终还是可能会把所有 buffer 都撑大!
    hopingtop
        13
    hopingtop  
       2023-07-12 17:25:40 +08:00
    @Trim21 #11 场景比较特殊,当前绑定了 json 序列化,后期准备改成 pb 一劳永逸
    Nazz
        14
    Nazz  
       2023-07-12 17:27:30 +08:00
    @hopingtop 包装下 sync.Pool, 把大包扔掉就好了
    hopingtop
        15
    hopingtop  
       2023-07-12 17:32:35 +08:00
    @Nazz #14 唉,encode/json 底层实现用了 全局 sync.Pool ,包括 json-iterator 也是类似实现,所以包不了。 如果 json-iterator 提供设置 自定义的 pool 就好了,可惜也没有
    Nazz
        16
    Nazz  
       2023-07-12 17:51:49 +08:00
    @hopingtop 使用 json.Encoder/Decoder 就可以自己管理 buffer pool 了
    hopingtop
        17
    hopingtop  
       2023-07-12 18:42:43 +08:00   ❤️ 2
    @Nazz #16 我确定是我表示不清楚,还是太久了,你忘记了,所以我们说的不是同一个东西,你可以再去看看代码
    ```go
    // NewEncoder returns a new encoder that writes to w.
    func NewEncoder(w io.Writer) *Encoder {
    return &Encoder{w: w, escapeHTML: true}
    }

    // Encode writes the JSON encoding of v to the stream,
    // followed by a newline character.
    //
    // See the documentation for Marshal for details about the
    // conversion of Go values to JSON.
    func (enc *Encoder) Encode(v any) error {
    if enc.err != nil {
    return enc.err
    }

    e := newEncodeState()
    defer encodeStatePool.Put(e)
    ```
    ```go
    var encodeStatePool sync.Pool

    func newEncodeState() *encodeState {
    if v := encodeStatePool.Get(); v != nil {
    e := v.(*encodeState)
    e.Reset()
    if len(e.ptrSeen) > 0 {
    panic("ptrEncoder.encode should have emptied ptrSeen via defers")
    }
    e.ptrLevel = 0
    return e
    }
    return &encodeState{ptrSeen: make(map[any]struct{})}
    }
    ```
    核心消耗内存的地方是 encodeState
    hopingtop
        18
    hopingtop  
       2023-07-12 18:43:38 +08:00
    @hopingtop #17 '我确定是' -> '我不确定是'
    Nazz
        19
    Nazz  
       2023-07-12 19:48:44 +08:00 via Android
    @hopingtop 是我想得太简单了😂
    matrix1010
        20
    matrix1010  
       2023-07-13 00:33:00 +08:00 via iPhone
    不懂为什么会爆,sync pool 既然是循环使用的那 pool 里面的对象数量应该不会很多才对。考虑到 json 的普遍性如果真有问题应该有很多相关 issues 才对,能不能放几个
    yulon
        21
    yulon  
       2023-07-13 07:56:17 +08:00
    原来 JSON 序列化会爆内存,怪不得,之前有个奇葩甲方一定要我直接拿 JSON 当数据库,然后占内存严重,我就又偷偷改成普通数据库
    hopingtop
        22
    hopingtop  
       2023-07-13 09:41:54 +08:00   ❤️ 1
    @matrix1010 #20 其实网上有比较多的案例,只是大部分情况下,遇不到。大多数场景,我们所产生的数据包 浮动是有限的,所以里面的 buffer 大小就算扩容也有限,本身来说 sync.Pool 是会回收,只是比较慢,通常来说一般要经过 2-3 个 GC 标记确认不用了才回收,但是这里一般流量较大,才会使用到 sync.Pool 的场景,所以可能导致一直回收不了。

    第二点就是 我们是容器部署,容器层限制了 2G 的内存使用量。 但是这个容器限制对于 Go 语言是感知不到的。在 go >1.19 版本,才出现有一个参数配置好像是 GOMEMLIMIT=xx ,告知 Go 我限制了内存。这个时候感到分配压力,GC 才会频繁活动!

    但是我们恰巧是 <1.19 , 我们物理机本身是 32G 内存,所以 Go 感觉内存是杠杠够的,但是 Docker 的 Limit ,导致在 Docker 层直接把进程 Kill 了
    bv
        23
    bv  
       2023-07-13 10:29:31 +08:00
    @yulon 你这应该跟池化关系不大,可能就是 JSON 太多太大,又需要常驻内存,导致内存占用高。
    yulon
        24
    yulon  
       2023-07-13 13:47:29 +08:00
    @bv 是运行好几天之后才爆的,重开程序就低得一批,内存里常驻的数据缓存尺寸固定,怎么可能是太多太大了?
    matytan
        25
    matytan  
    OP
       2023-07-15 10:56:46 +08:00
    @yulon 仔细看我的代码实现,会释放
    matytan
        26
    matytan  
    OP
       2023-07-15 10:57:02 +08:00
    @hopingtop 是的
    matytan
        27
    matytan  
    OP
       2023-07-15 10:58:26 +08:00
    @hopingtop 谢谢,其实我在 put 的时候做了一点 trick 的释放和重建,目前够用,后期我会结合你这个代码优化
    matytan
        28
    matytan  
    OP
       2023-07-15 10:59:47 +08:00
    @matrix1010 如果不重用,来不及 gc 就会爆炸的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3551 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 11:13 · PVG 19:13 · LAX 03:13 · JFK 06:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.