内存管理¶
学习目标¶
学完本章后,学习者应该能够:
- 理解虚拟内存、页表、缺页中断、堆和栈的基本概念。
- 理解 page cache、mmap 和 OOM 对后端服务的影响。
- 能解释 Go 服务内存上涨的常见原因。
- 能初步设计内存问题排查路径。
虚拟内存¶
现代操作系统通常让每个进程看到独立的虚拟地址空间。进程以为自己拥有连续内存,操作系统通过页表把虚拟地址映射到物理内存。
虚拟内存带来的好处:
- 进程隔离。
- 简化程序内存模型。
- 支持按需分配。
- 支持内存映射文件。
- 支持换页和 page cache。
对后端工程师来说,虚拟内存解释了为什么进程看到的地址不是物理地址,也解释了为什么内存指标有 RSS、VIRT、heap、page cache 等多个维度。
分页与缺页中断¶
操作系统通常以页为单位管理内存。程序访问某个虚拟页时,如果页还没有映射到物理内存,可能触发缺页中断,由内核负责建立映射或从磁盘加载数据。
缺页不一定是错误,但频繁缺页会影响性能。
堆与栈¶
简化理解:
- 栈:函数调用、局部变量、执行上下文,分配和释放快。
- 堆:生命周期更复杂的数据,由运行时或程序管理。
Go 中 goroutine 初始栈很小,会按需增长。对象是否分配到堆上,和逃逸分析有关。
这里返回局部变量地址,User 通常会逃逸到堆上。
page cache¶
Linux 会用空闲内存缓存文件内容,这就是 page cache。它能加速文件读写,但也会让新手误以为“内存被吃光了”。
数据库、日志、文件上传、容器镜像层都可能和 page cache 相关。
关键点:
- page cache 是性能优化,不一定是内存泄漏。
- 系统内存紧张时,部分 page cache 可以回收。
- 容器场景下,page cache 和 cgroups 统计可能影响内存判断。
mmap¶
mmap 可以把文件映射到进程虚拟地址空间,让程序像访问内存一样访问文件内容。
常见场景:
- 数据库存储引擎。
- 搜索引擎索引文件。
- 高性能文件读取。
- 进程间共享内存。
它不是“免费读文件”,仍然受缺页、磁盘 IO 和内存压力影响。
Go 后端实际应用例子¶
例子一:限制请求体大小¶
不限制请求体大小,攻击者或误用方可能上传超大请求导致内存压力。
func uploadHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
defer r.Body.Close()
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
_ = data
w.WriteHeader(http.StatusNoContent)
}
更好的大文件处理方式通常是流式读写,而不是一次性 io.ReadAll。
例子二:避免全量查询进入内存¶
导出数据时不要一次性查出所有记录。更稳妥的方式是分页或游标流式处理:
func ExportUsers(ctx context.Context, repo UserRepo, w io.Writer) error {
const pageSize = 1000
for offset := 0; ; offset += pageSize {
users, err := repo.List(ctx, offset, pageSize)
if err != nil {
return err
}
if len(users) == 0 {
return nil
}
for _, user := range users {
if _, err := fmt.Fprintf(w, "%d,%s\n", user.ID, user.Name); err != nil {
return err
}
}
}
}
这类代码虽然多几行,但能避免一次性把大结果集压进内存。
常见误区¶
- 误区一:内存上涨一定是泄漏。
可能是正常缓存、page cache、流量上涨、GC 尚未回收,也可能是真泄漏。要结合指标判断。
- 误区二:OOM 只看 Go heap。
进程总内存还包括 goroutine 栈、运行时结构、mmap、cgo、page cache 等。
- 误区三:缓存越多越好。
缓存必须有容量上限、过期策略和命中率指标,否则很容易变成内存事故。
线上问题案例¶
某导出接口为了方便,一次性查询所有订单并生成 Excel。数据量从几千增长到几十万后,容器内存迅速超过 limit,被 Kubernetes OOMKilled。
修复方式是改成分页查询、流式写出、限制导出时间范围、异步生成文件,并为导出任务设置并发上限。
实战任务¶
设计一个“用户数据导出”接口的内存安全方案:
- 不能一次性加载全部数据。
- 支持大数据量。
- 能限制单个请求的内存占用。
- 能说明 OOM 后如何排查。
参考答案
可以把导出设计成异步任务:用户提交导出请求后进入任务队列,worker 按分页或游标读取数据,并流式写入 CSV 或对象存储。接口只返回任务 ID,用户稍后下载文件。
内存控制手段包括分页大小限制、导出时间范围限制、worker 并发限制、请求体大小限制、缓存容量限制。OOM 排查时要看容器事件、内存指标、Go heap profile、goroutine 数量、最近流量和导出任务数量。
面试题¶
1. 什么是虚拟内存?¶
参考答案
虚拟内存是操作系统给每个进程提供的独立地址空间。进程看到的是虚拟地址,操作系统通过页表把虚拟地址映射到物理内存。
它带来进程隔离、按需分配、内存映射和更简单的编程模型。后端排查内存时,需要区分虚拟地址空间、实际物理内存占用和 Go heap。
2. page cache 是什么?为什么会影响内存判断?¶
参考答案
page cache 是 Linux 用空闲内存缓存文件内容的机制,用于提升文件读写性能。它会占用内存指标,但在系统需要内存时部分可以回收。
新手看到内存使用很高时,不能立刻判断为泄漏,要区分进程 heap、RSS、page cache、容器限制和可回收内存。
3. Go 服务 OOM 常见原因有哪些?¶
参考答案
常见原因包括一次性读取大文件、大查询不分页、缓存无上限、请求体无限制、goroutine 堆积、对象被全局引用无法释放、cgo 或 mmap 占用、容器 memory limit 设置过小。
排查时要结合 Kubernetes 事件、内存曲线、heap profile、goroutine profile、日志和最近发布变更。