Go 的垃圾回收性能监控和优化
高并发场景下 GC 常见问题
在高并发情况下,经常遇到以下 GC 问题:
-
CPU 争夺
- GC 在标记/清理时需要 CPU,会和业务逻辑共享同一份 CPU 核心资源。
- 导致请求延迟上升,吞吐下降。
-
内存分配过快
- 如果 goroutine 并发过多,每个请求都在分配临时对象,分配速度可能比 GC 回收快。
- 导致堆内存持续膨胀,甚至 OOM。
-
短生命周期对象过多
- 例如 JSON 解析中分配大量临时 slice/map/string,很快就会被丢弃,加剧 GC 频率。
-
Stop The World (STW) 延迟
- Go 1.5+ 已将 STW 大幅缩短,但在大堆/高频率分配场景下仍可能出现卡顿。
常用的优化策略
对象池模式(sync.Pool)
在高并发 web 服务里最常见。用于减少大量短命对象的分配。
package main
import (
"fmt"
"sync"
)
type MyObject struct {
Data []byte
}
func (o *MyObject) Reset() {
for i := range o.Data {
o.Data[i] = 0
}
}
var objPool = sync.Pool{
New: func() interface{} {
return &MyObject{Data: make([]byte, 1024)} // 大对象复用
},
}
func handleRequest(id int, wg *sync.WaitGroup) {
defer wg.Done()
obj := objPool.Get().(*MyObject)
defer objPool.Put(obj)
// 模拟业务逻辑
obj.Data[0] = byte(id % 256)
fmt.Println("req", id, "done")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go handleRequest(i, &wg)
}
wg.Wait()
}
避免短暂对象
避免在循环内频繁创建小对象,可以将对象提升到循环外,减少 GC 压力。
示例:缓存临时字符串构建器
package main
import (
"fmt"
"strings"
)
func main() {
// 避免在循环中频繁 new StringBuilder
var sb strings.Builder
for i := 0; i < 5; i++ {
sb.Reset() // 复用 builder
sb.WriteString("Hello ")
sb.WriteString(fmt.Sprint(i))
fmt.Println(sb.String())
}
}
预分配和复用切片/缓冲
在热点路径避免动态扩容。
package main
import "fmt"
func processBatch(n int) {
// 預分配 slice,避免每次 append 扩容
buf := make([]int, n)
for i := 0; i < n; i++ {
buf[i] = i * i
}
fmt.Println(buf[:10])
}
func main() {
for i := 0; i < 1000; i++ {
processBatch(5000)
}
}
使用高效的数据结构
选择合适的数据结构,避免过度使用 map,必要时用 slice 替代 map 来存储顺序数据。
示例:优先使用切片而不是 map
package main
import "fmt"
func main() {
// 使用 map 存储连续整数,开销较大
m := map[int]int{}
for i := 0; i < 5; i++ {
m[i] = i * i
}
fmt.Println("map:", m)
// 同样的功能使用 slice 更高效
s := make([]int, 5)
for i := 0; i < 5; i++ {
s[i] = i * i
}
fmt.Println("slice:", s)
}