记一次内存问题排查
背景
最近线上有一台机器上的服务经常 OOM ,奇怪的是只在一台机器上存在,其他机器都不存在。所以引起了好奇心,探索一下其根因。
系统内存的使用情况,常见的方式使用 free 命令,其输出界面如下
$ free -g
total used free shared buff/cache available
内存: 100G 30G 4G 60G 72G 2.0G
交换: 0 0 0
在 free -g
, -g
表示以 GB 为单位 展示内存数据,更直观易读(尤其是在大内存机器上)。整个表格主要由两部分组成:
- Mem : 物理内存使用的情况
- Swap : 交换分区的使用情况
这里的 total、used、free 都很好理解
字段 | 说明 |
---|---|
total | 总内存 |
used | 已使用的内存(包括缓存 和 buff) |
free | 完全未使用的内存(几乎总是很小) |
shared | 多个进程共享的内存(主要和 tmpfs、共享库) |
buff/cache | 真正可用的内存(考虑回收缓存后的可用) |
✅ 按公式的关系总结:
used = total - free - buff/cache
这个 used
是 减去 cache 后的实际占用, 是 free
命令的一个人为定义,和 top
、ps
等工具可能略有不同。
available ≈ free + 可回收的 cache + 可收回的 slab
available
是内核评估释放缓存,还能用多少内存 , 这个值才是系统是否内存吃紧的判断依据。
如何正确判断内存是否紧张?
不要盯着 used/free , 应该看
available
是否太低?✅ 这里只有 2.0G,已接近危险值swap
是否启用?❌ 没有 swap,一旦不够会触发 OOMbuff/cache
是否过高?✅ 有 72G,在内存压力下能释放,但释放有代价shared
是否有异常?✅ 有 60G,这个非常高,应该重点排查
问题分析
在GPT加持下,最终定位到其实是 Nginx 的缓存能力 proxy_cache_path
代理缓存所需要的目录被反复挂载导致的问题。
proxy_cache_path
是 Nginx 用来定义缓存目录及缓存元数据存储区域的指令。它告诉Nginx:
- 缓存文件放在哪里(磁盘路径)
- 管理缓存键(key)和状态的共享内存区域的大小
- 缓存区分规则(缓存文件如何组织)
一个正常的配置如下:
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:50m
inactive=60m max_size=10g loader_files=1000 loader_sleep=10m;
说明:
- 缓存文件存储在
/data/nginx/cache
- 目录结构是两级:先一级目录一个字符,再二级目录两个字符
- 共享内存大小为 50MB,存储缓存元信息
- 60 分钟未访问缓存文件会被删除
- 最大缓存占用 10GB
- 启动时最多并发扫描 1000 个缓存文件
- 扫描时每 10 分钟休眠一次
缓存的工作流程
- 请求到来,Nginx 根据请求计算缓存Key
- 在共享内存(keys_zone)里查找是否存在有效缓存条目
- 如果命中缓存,直接读取磁盘上的缓存文件返回客户端
- 如果缓存未命中,向后端请求数据,写入缓存文件(磁盘),并更新共享内存中的元信息。
- 共享内存保证所有的 worker 进程对缓存状态的同步
为了提高 文件的访问速度,我们可以进一步的将 /data/nginx/cache
这个目录放到一个 tmpfs (内存文件系统) 挂载点下,然后让 Nginx 的 proxy_cache_path
指向这个 tmpfs 目录。
tmpfs 是一种基于内存的文件系统,挂载后的目录的所有数据都存在于内存中(或 swap 中)
访问速度非常快,类似内存操作,快于磁盘,但是数据是 易失的 ,重启后目录中的内容会全部丢失。
所以会有脚本将 cache 目录以 tmpfs 挂载用于访问。
sudo mount -t tmpfs -o size=5G tmpfs /data/nginx/cache
执行一次还好,但是如果这个命令反复执行,那就有问题了。每次执行都会导致 重新申请一片共享内存指向该块区域。
概念加深
其实在排查这个问题的时候,操作系统内存的很多概念都忘记了。所以特地再来加深一下。
共享内存(Shared Memory):进程通信的加速器
在操作系统的世界里,进程是彼此隔离的沙箱。每个进程都有自己的独立的地址空间,互不干扰,这种设计提高了安全性和稳定性,但也带来了一个问题:进程间如何通信?
虽然我们有很多的方式 – 如管道(pipe)、消息队列(message queue)、socket 通信 – 但如果你追求性能 和 低延迟 , 那么有一个更强大的选择:共享内存(Shared Memory)。
什么是共享内存
共享内存是一种将同一块物理内存区域映射到多个进程的虚拟地址中的技术。
通俗来说,就是两个或者多个进程 “共享” 同一个内存区域 – 一个进程写入的数据,另一个进程可以直接读取,而 无需通过内核中转或者拷贝内存内容, 这极大地提升了通信效率。
为什么选择共享内存?
字段 | 说明 |
---|---|
高性能 | 数据不需要再内核和用户态之间拷贝、零拷贝(zero-copy)通信 |
低延迟 | 写入和读取几乎都是瞬时完成 |
适合大数据量通信 | 比如视频帧、日志缓冲、大量结构体数组等 |
操作灵活 | 可以使用指针、结构体操作共享数据 |
但共享内存也带来挑战:需要自行管理并发和同步问题,比如加锁或者使用信号量。
在Linux 中使用共享内存的方式
Linux 提供了多种机制来实现共享内存,主要包括:
1. mmap
+ 文件映射
- 可以是普通文件或
/dev/shm
下的临时文件 - 多个进程
mmap
同一个文件,即可实现共享
匿名共享内存 → 纯 mmap
,也不依赖文件系统,
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
这种创建的共享内存段既没有文件路径,也没有 key,只能在 /proc/<pid>/maps
里看到,没有 /dev/shm
文件。
有时会结合 memfd_create()
给它取个名字,但仍不走 tmpfs
。
// Go 中创建匿名内存映射
fd, _ := os.Create("/dev/shm/shared_mem")
ftruncate(fd.Fd(), 4096)
data, _ := syscall.Mmap(int(fd.Fd()), 0, 4096, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
2. System V 共享内存(传统方式)
- 使用
shmget
,shmat
,shmdt
,shmctl
等系统调用 - 可以通过
ipcs -m
命令查看使用情况
3. POSIX 共享内存(更现代)
- 使用
shm_open
,mmap
,shm_unlink
等函数 - 通常配合
/dev/shm
临时挂载目录使用
什么是 /dev/shm?
在现代 Linux 系统中,/dev/shm
是一个基于内存的虚拟文件系统,全称叫 tmpfs 文件系统,挂载在 /dev/shm
下。这个目录允许用户或程序以文件形式访问和操作共享内存区域。
- 它使用的是系统的RAM(物理内存),所以读写速度远远高于普通的硬盘文件。
- 它的作用之一是实现 System V 或 POSIX 标准下的匿名共享内存或具名共享内存机制。
- 它的大小通常默认为总内存的一半,可以通过
/etc/fstab
或挂载参数调整。
查看应用程序的共享内存分布
查看共享内存的分布(包括 /dev/shm
、System V、POSIX 共享内存、匿名映射等),可以从以下几个层面入手:
1. /dev/shm
下的文件(POSIX 共享内存)
ls -lh /dev/shm/
total 0
drwxr-xr-x 2 root root 80 Jul 31 20:45 yunjing
这些是基于 POSIX shm_open()
或 mmap
创建的共享内存对象,基于 tmpfs /dev/shm
本质上是一个 tmpfs
(临时文件系统)挂载点。
2. 查看 System V 类型共享内存(老式 IPC)
ipcs -m
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x00000000 5 root 600 524288 2 目标
0x00000000 8 root 600 524288 2 目标
3. 查看 /proc/<pid>/maps
中的共享内存映射情况
ls -l /proc/<PID>/maps | grep shm
Buffer & Cache 详解:Linux 内存性能的加速器
在现代操作系统中,磁盘 I/O 是性能的最大瓶颈之一。相比 CPU 和内存,机械硬盘和即使是 SSD 的访问速度依然慢得多。 为了减少等待时间、提高系统吞吐量,Linux 内核引入了两位幕后英雄:Buffer 和 Cache。它们并不是同一个东西,但目标一致——减少磁盘访问次数、提高数据访问速度。
1️⃣ Buffer(缓冲区):块设备的临时调度员
在磁盘这种块设备中,数据的基本传输单位是块(block)。如果应用频繁读写少量数据,直接访问磁盘会非常低效。
于是 Linux 内核在内存中维护了 Buffer:
- 当你写入数据时,内核先放进 Buffer,等凑够合适大小或空闲时再一次性写入磁盘(写聚合)。
- 当你读取数据时,如果数据已经在 Buffer 中,就可以直接返回,无需访问磁盘。
作用:
- 主要缓存块设备的数据块和相关元信息(如 buffer_head)。
- 降低磁盘 I/O 次数。
- 提升原始块读写性能。
2️⃣ Cache(页缓存):文件系统的速读机
Cache 专门服务于文件系统,它缓存的是文件内容本身,而不是底层磁盘块。
- 当你打开一个文件并读取内容时,Linux 会把这些数据放进 Page Cache(页缓存)。
- 下次再访问同一个文件,直接从 Cache 返回,速度接近内存访问。
作用:
- 提高文件读取速度(尤其是重复读取)。
- 减少磁盘读取次数。
- 也缓存目录项(dentry)和 inode 信息,加快路径解析。
观察 buffer & Cache
- 用
vmstat 1
观察
vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 215172 10240 153600 0 0 0 1 100 200 5 1 94 0 0
- 观察 Page Cache 明细
vmtouch
如果想具体看到哪些文件在占用 cache, 这可以直接看到某个文件的页缓存占用情况。
sudo vmtouch /opt/work
- 观察 Slab Cache(属于 Cache 范畴)
slabtop
✅ 总结:
- 快速总览:
free -h
- 精确分布:
cat /proc/meminfo
- 实时变化:
vmstat 1
- 文件级缓存:
vmtouch
/fincore
- 内核对象缓存:
slabtop