Go-26-Issue

Issue-23199

在阅读 sync.Poolfmt 中的使用时候看到了这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (p *pp) free() {
// Proper usage of a sync.Pool requires each entry to have approximately
// the same memory cost. To obtain this property when the stored type
// contains a variably-sized buffer, we add a hard limit on the maximum
// buffer to place back in the pool. If the buffer is larger than the
// limit, we drop the buffer and recycle just the printer.
//
// See .https://golang.org/issue/23199
if cap(p.buf) > 64*1024 {
p.buf = nil
} else {
p.buf = p.buf[:0]
}
//...
//存入内存池
ppFree.Put(p)
}

上述英文解释的大致意思是

sync.Pool在使用的时候每个对象的内存消耗应该大致相同。当存储的类型包含可变大小的缓冲区时,需要对最大缓冲区设置一个硬限制,以确保如果缓冲区的大小超过限制,将丢弃缓冲区,并仅回收

因为 fmt 使用内存池分配的对象大小不是固定的,如下 buf 其实是一个缓冲区

1
2
3
4
5
6
7
8
9
10
var ppFree = sync.Pool{
New: func() any { return new(pp) },
}

type pp struct {
buf buffer
//...
}

type buffer []byte

这里做一个测试就会明白这样的处理

1
2
3
4
5
6
7
8
9
a := make([]int, 100)
b := make([]int, 100)
fmt.Println(cap(a), len(a)) //100 100
fmt.Println(cap(b), len(b)) //100 100

a = nil
b = b[:0]
fmt.Println(cap(a), len(a)) //0 0
fmt.Println(cap(b), len(b)) ///100 0

使用 b[:0] 之后只是切片长度变化,容量并不会变,也就是内存仍然占用那么多

所以,如果内存池大小不固定的时候注意主动释放,防止额外占用空间而不被释放。再来看看它的测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"bytes"
"fmt"
"runtime"
"sync"
"time"
)

func finalizer(buffer *bytes.Buffer) {
fmt.Printf("GC %d buffer \n", buffer.Cap())
}

func main() {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("start %d B\n", stats.Alloc)

pool := sync.Pool{New: func() interface{} {
buf := new(bytes.Buffer)
runtime.SetFinalizer(buf, finalizer)
return buf
}}

processRequest := func(size int) {
b := pool.Get().(*bytes.Buffer)
time.Sleep(500 * time.Millisecond) // Simulate processing time
b.Grow(size)
pool.Put(b)
time.Sleep(1 * time.Millisecond) // Simulate idle time
}

// 模拟一组大规模写入
for i := 0; i < 10; i++ {
go func() {
processRequest(1 << 20) // 256MiB
}()
}

time.Sleep(time.Second) // Let the initial set finish

// 模拟小规模写入且不会停
for i := 0; i < 10; i++ {
go func() {
for {
processRequest(1 << 10) // 1KiB
}
}()
}

// 每次GC之后查看分配的内存
for i := 0; ; i++ {
runtime.ReadMemStats(&stats)
fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc)
time.Sleep(time.Second)
runtime.GC()
}
}

通过 runtime.SetFinalizer 大概说明对象的回收时间,会发现大对象并不是立即回收的,而是经过了一段时间,内存才趋于稳定。高并发场景下,比如处理网络请求的时候,可能导致大量内存的占用而没有及时释放

解决办法: 就在在判断超过一定大小的时候直接丢弃

参考链接

  1. https://golang.org/issue/23199
  2. https://go-review.googlesource.com/c/go/+/136035/1/src/encoding/json/encode.go