我总是对我的 Go 应用程序不断增长的驻留内存感到困惑,最后我不得不学习 Go 生态系统中存在的分析工具。运行时在runtime.Memstats 结构中提供了许多指标,但可能很难理解其中哪些有助于找出内存增长的原因,因此需要一些额外的工具。
分析环境
在您的应用程序中使用https://github.com/tevjef/go-runtime-metrics。例如,你可以把它放在你的main:
import(
metrics "github.com/tevjef/go-runtime-metrics"
)
func main() {
//...
metrics.DefaultConfig.CollectionInterval = time.Second
if err := metrics.RunCollector(metrics.DefaultConfig); err != nil {
// handle error
}
}
在 Docker 容器中运行 InfluxDB 和 Grafana:
docker run --name influxdb -d -p 8086:8086 influxdb
docker run -d -p 9090:3000/tcp --link influxdb --name=grafana grafana/grafana:4.1.0
设置Grafana 和InfluxDB Grafana 之间的交互(Grafana 主页 -> 左上角 -> 数据源 -> 添加新数据源):
从https://grafana.com 导入仪表板#3242(Grafana 主页 -> 左上角 -> 仪表板 -> 导入):
最后,启动您的应用程序:它将运行时指标传输到竞争化的Influxdb。将您的应用程序置于合理的负载下(在我的情况下它非常小 - 5 RPS 几个小时)。
内存消耗分析
-
Sys(RSS的同义词)曲线与HeapSys曲线非常相似。事实证明,动态内存分配是整体内存增长的主要因素,因此堆栈变量消耗的少量内存似乎是恒定的,可以忽略不计;
- 恒定数量的 goroutine 保证不存在 goroutine 泄漏/堆栈变量泄漏;
- 在进程的生命周期内,分配的对象总量保持不变(没有必要考虑波动)。
- 最令人惊讶的事实:
HeapIdle 的增长速度与Sys 相同,而HeapReleased 始终为零。显然,运行时根本不会将内存返回给操作系统,至少在这个测试的条件下:
HeapIdle minus HeapReleased estimates the amount of memory
that could be returned to the OS, but is being retained by
the runtime so it can grow the heap without requesting more
memory from the OS.
对于那些试图调查内存消耗问题的人,我建议按照描述的步骤来排除一些琐碎的错误(如 goroutine 泄漏)。
显式释放内存
有趣的是,通过显式调用debug.FreeOSMemory() 可以显着减少内存消耗:
// in the top-level package
func init() {
go func() {
t := time.Tick(time.Second)
for {
<-t
debug.FreeOSMemory()
}
}()
}
事实上,与默认情况相比,这种方法节省了大约 35% 的内存。