使用 Go `runtime` 包监控和优化容器内存使用
Talk is cheap, show me the code. 本篇目标是通过 mini-task ,掌握如何使用 Go 语言的 runtime
包来监控和优化容器内存使用。Let’s go!
一、背景介绍
runtime
包在 Go 语言中提供了与运行时系统交互的功能,允许开发者访问和控制 Go 程序的运行时环境。它主要的作用包括:
- 并发控制:管理和控制 goroutine 的并发执行,例如通过 GOMAXPROCS 函数设置或查询逻辑处理器的数量。
- 内存管理:提供垃圾回收(Garbage Collection, GC)相关的函数和调优参数,帮助开发者优化内存使用。
- 调度器控制:允许开发者干预调度器的行为,例如设置或查询线程的抢占策略。
- 堆栈跟踪:提供获取当前 goroutine 的堆栈跟踪信息的函数,有助于调试和错误处理。
二、任务目标:
通过这个任务,实践使用 Go 语言的 runtime
包来监控和调整应用程序的内存使用情况。最终目标是编写一个简单的 Go 程序,能够在容器化环境中运行,定期打印当前的内存使用情况,并根据使用量进行优化(e.g proactive garbage collection)。mini-task 计划如下:
- 了解 Go
runtime
包的基本功能:通过官方文档查询与内存管理相关的函数。 - 编写监控内存使用的 Go 程序:使用
runtime
包中的MemStats
结构体和ReadMemStats
方法,实时监控内存使用情况。 - 在容器中运行该程序:通过
Docker
将程序容器化,并观察在容器中运行时的内存使用情况。
四、操作记录:
- 初步学习
runtime
包内存管理功能:- 阅读并理解 Go 官方文档中关于
runtime
包的介绍,本次 mini-task 主要是与内存管理相关的部分:runtime.MemStats
结构体:包含了 Go 程序的各种内存统计数据。runtime.ReadMemStats(&memStats)
:获取当前的内存统计数据。runtime.GC()
:手动触发垃圾回收。
- 阅读并理解 Go 官方文档中关于
想要深入了解 runtime 可以阅读官方文档 https://pkg.go.dev/runtime
编写 Go 程序:
- 编写一个简单的程序,定期(如每秒)调用
runtime.ReadMemStats
获取内存使用情况,并打印输出到标准输出。还可以在某个条件下调用runtime.GC()
主动进行垃圾回收。 - 示例代码:
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
31package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var memStats runtime.MemStats
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
runtime.ReadMemStats(&memStats)
fmt.Printf("Alloc = %v MiB", bToMb(memStats.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(memStats.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(memStats.Sys))
fmt.Printf("\tNumGC = %v\n", memStats.NumGC)
// 可选:如果某个条件成立,主动触发垃圾回收
if memStats.Alloc > 100*1024*1024 {
// 这里设置当内存分配超过 100 MiB 时,主动触发垃圾回收
fmt.Println("Triggering GC...")
runtime.GC()
}
}
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
- 编写一个简单的程序,定期(如每秒)调用
容器化并运行程序:
- 编写一个简单的 Dockerfile 将该程序容器化:
1
2
3
4
5FROM golang:latest
WORKDIR /app
COPY . .
RUN go build -o mem-monitor .
CMD ["./mem-monitor"] - 构建和运行容器:
1
2docker build -t mem-monitor .
docker run --rm mem-monitor - 观察容器内的内存使用情况,并调整容器的内存限制、CPU 配额等,查看程序的运行表现。
(1)构建镜像
(2)观察容器的内存使用情况
可以上图发现 alloc、TotalAlloc、Sys、NumGC 等内存使用情况,我们可以对代码做一些小修改,增加波动。(3)修改代码
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
43package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var memStats runtime.MemStats
ticker := time.NewTicker(1 * time.Second)
// 模拟的内存分配
var allocations [][]byte
for range ticker.C {
// 分配 10 MiB 内存
allocations = append(allocations, make([]byte, 10*1024*1024))
// 每隔 5 次释放一部分内存
if len(allocations) > 5 {
allocations = allocations[2:] // 保留最近三次的分配
}
// 读取当前内存状态
runtime.ReadMemStats(&memStats)
fmt.Printf("Alloc = %v MiB", bToMb(memStats.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(memStats.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(memStats.Sys))
fmt.Printf("\tNumGC = %v\n", memStats.NumGC)
// 可选:如果某个条件成立,主动触发垃圾回收
if memStats.Alloc > 100*1024*1024 {
// 这里设置当内存分配超过 100 MiB 时,主动触发垃圾回收
fmt.Println("Triggering GC...")
runtime.GC()
}
}
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}(4)更新镜像、重新运行容器
通过这个修改,你应该能看到内存使用情况在每次分配和释放操作时产生波动,Alloc、TotalAlloc、Sys 等字段的值会有明显变化,并且 NumGC 的值会随着垃圾回收的触发而增加。这样可以更直观地观察 Go 程序的内存管理行为。
- 编写一个简单的 Dockerfile 将该程序容器化:
五、任务总结和思考
通过这个任务,我们了解到 Go 语言的 runtime
包的基本功能,以及如何使用它来编写和运行 Go 程序。同时,我们还可以通过容器化和运行程序来观察容器内的内存使用情况,并根据需要进行优化。
附录1 基础环境工具配置
docker desktop(macos) 4.31.0 (153195)
go version go1.23.0 darwin/arm64