跳转至

内存管理

学习目标

学完本章后,学习者应该能够:

  1. 理解虚拟内存、页表、缺页中断、堆和栈的基本概念。
  2. 理解 page cache、mmap 和 OOM 对后端服务的影响。
  3. 能解释 Go 服务内存上涨的常见原因。
  4. 能初步设计内存问题排查路径。

虚拟内存

现代操作系统通常让每个进程看到独立的虚拟地址空间。进程以为自己拥有连续内存,操作系统通过页表把虚拟地址映射到物理内存。

虚拟内存带来的好处:

  • 进程隔离。
  • 简化程序内存模型。
  • 支持按需分配。
  • 支持内存映射文件。
  • 支持换页和 page cache。

对后端工程师来说,虚拟内存解释了为什么进程看到的地址不是物理地址,也解释了为什么内存指标有 RSS、VIRT、heap、page cache 等多个维度。

分页与缺页中断

操作系统通常以页为单位管理内存。程序访问某个虚拟页时,如果页还没有映射到物理内存,可能触发缺页中断,由内核负责建立映射或从磁盘加载数据。

缺页不一定是错误,但频繁缺页会影响性能。

堆与栈

简化理解:

  • 栈:函数调用、局部变量、执行上下文,分配和释放快。
  • 堆:生命周期更复杂的数据,由运行时或程序管理。

Go 中 goroutine 初始栈很小,会按需增长。对象是否分配到堆上,和逃逸分析有关。

func NewUser(name string) *User {
    return &User{Name: name}
}

这里返回局部变量地址,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。

修复方式是改成分页查询、流式写出、限制导出时间范围、异步生成文件,并为导出任务设置并发上限。

实战任务

设计一个“用户数据导出”接口的内存安全方案:

  1. 不能一次性加载全部数据。
  2. 支持大数据量。
  3. 能限制单个请求的内存占用。
  4. 能说明 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、日志和最近发布变更。