使用 Linux 开发时最常见的问题是:我的内存呢?怎么只剩这么点了?这是怎么回事了呢?
消失的内存
通常我们会用 free
命令(如下)或 Node Exporter + Prometheus 来监控系统的内存。
$ free -h |
上面的输出中,我们很自然地以为 free
代表可以内存,所以经常会发现这个值特别低,造成“系统的内存用光了”的错觉。在比较新的内核里,会有 available
一项,它才是“可用内存”。
这里有个小知识,free
指的是完全没有被用到的内存,而 Linux 认为内存不用也是浪费,因此会尽量“多”地把内存用来做各种缓存,提高系统的性能。在内存不够用时,它会释放缓存腾出空间给应用程序。因此早期没有 available
这项指标时,一般会认为
free + buff/cache
是系统当前的可用内存。那么现在的 available
是如何计算得到的?
MemAvailable 估算
free
命令只输出简单几项指标,更详细的指标可以用 cat /proc/meminfo
得到:
MemTotal: 32729276 kB |
指标非常多,一般需要对内核有一定了解才能看懂。这些指标的基础上,有 [1]:
MemAvailable <= MemFree + Active(file) + Inactive(file) + SReclaimable |
要理解这个公式,需要了解 Linux 是如何管理内存的。Linux 对内存的管理有多种视角。
- 系统内存 = 空闲内存 + 内核内存 + 用户内存
- 内核内存[2] = Slab + VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X
- Slab = SUnreclaim + SReclaimable,其中
SReclaimable
指可回收部分
- Slab = SUnreclaim + SReclaimable,其中
- 用户内存有两个视角:
- LRU 视角 = Active + Inactive + Unevictable + (HugePages_Total * Hugepagesize)
- Active 与 Inactive 内存指的是活跃程度,如果内存紧张,会优先释放 Inactive 的内存
- Active = Active(File) + Inactive(Anon)
- Inactive = Inactive(File) + Inactive(Anon)
- File-Backend 内存会与磁盘中的文件关联,于是如果内存不足时可以先写回磁盘释放内存;Anonymous 内存不与文件关联,因此除非有 swap 文件,否则无法释放
- 缓存视角 = Cached + AnonPages + Buffers + (HugePages_Total * Hugepagesize)
- LRU 视角 = Active + Inactive + Unevictable + (HugePages_Total * Hugepagesize)
结合上述信息,可以看到可以释放的部分有:
- Slab 的
SReclaimable
,是内核可释放的部分 - 所有的 File-Backend 内存 = Active(File) + Inactive(File)
MemAvailable 公式的由来就很自然而然了。等等!?公式里的符号为什么是小于等于,不是等于?
详细逻辑与样例
上面的公式在详细计算时,并没有考虑 watermark
(虽然代码里有),并且最新的内核已经修改了计算的公式[3],考虑了更多的内容。
-
计算
wmark_low
。low watermark,当系统可用内存小于 low watermark 时,kswapd
进程会开始尝试释放内存页。首先收集需要的信息:# cat /proc/zoneinfo | grep min
min 1
min 184
min 16710每个 ZONE 都有自己的 low watermark(单位为页,页大小为 4K),计算如下
wmark_low = (1 + 230 + 20887) * 4
= 84472 (KB) -
计算空闲页
free_pages
,可以直接由/proc/zoneinfo
中获取:# cat /proc/zoneinfo |grep 'free '
nr_free_pages 3969
nr_free_pages 611300
nr_free_pages 59976587加总即得到
free_pages
:free_pages = (3969 + 611300 + 59976587) * 4
= 242367424 (KB) -
计算保留内存[4]。保留内存需要综合考虑各项指标:
-
lowmem_reserve_ratio
ZONE[5] 是逻辑上的划分,lowmem 是指低位的 ZONE 为高位 ZONE 预留的内存[6]。每个 ZONE 都会为更高位的 ZONE 做预留,因此结果是个矩阵:# cat /proc/zoneinfo | grep 'protection'
protection: (0, 2815, 257771, 257771)
protection: (0, 0, 254955, 254955)
protection: (0, 0, 0, 0) -
high watermark。高水位线,可用内存超出它时,
kswapd
会暂停工作。# cat /proc/zoneinfo | grep 'high '
high 1
high 276
high 25065 -
managed 内存,没查到出处,大概指可被使用的内存。
# cat /proc/zoneinfo | grep 'managed'
managed 3977
managed 720847
managed 65268660 -
计算如下:
total_reserved = Σ(min((max(lowmem) + high_watermark), managed))
total_reserved = Σ(min((max(lowmem) + high_watermark), managed))
= min(max(0, 2815, 257771, 257771) + 1, 3977)
+ min(max(0, 0, 254955, 254955) + 276, 720847)
+ min(max(0, 0, 0, 0) + 25065, 65268660)
= 3977 + 255231 + 25065
= 284273 (page)
= 1137092 (kB)
-
-
计算
pagecache = active file + inactive file
,File Backend 的内存可以被释放。# cat /proc/zoneinfo |grep nr_active_file
nr_active_file 0
nr_active_file 6032
nr_active_file 168031
# cat /proc/zoneinfo | grep nr_inactive_file
nr_inactive_file 0
nr_inactive_file 1833
nr_inactive_file 50064pagecache = active file + inactive file
= (0 + 6032 + 168031) + (0 + 1833 + 50064)
= 225960 (page)
= 903840 (kB) -
pagecache -= min(pagecache / 2, wmark_low)
,并不是所有的 pagecache 都被认为是可用的:pagecache -= min(pagecache / 2, wmark_low)
-= min(903840/2, 84472)
-= 84472
= 903840 - 84472
= 819368 (KB) -
计算
SReclaimable
# cat /proc/zoneinfo | grep nr_slab_reclaimable
nr_slab_reclaimable 0
nr_slab_reclaimable 428
nr_slab_reclaimable 36989SReclaimable = (0 + 428 + 36989) * 4
= 149668 (kB) -
SReclaimable -= min(SReclaimable/2, wmark_low)
,和 pagecache 相似,不能全用。SReclaimable -= min(SReclaimable/2, wmark_low)
-= min(149668 / 2, 84472)
-= 74834
= 149668 - 74834
= 74834 (kB) -
available = free_pages - total_reserved + pagecache + SReclaimable
available = 242367424 - 1137092 + 819368 + 74834
= 242124534 (kB)
最终的结果与 /proc/meminfo
的输出(和上小节的数据不同)只有细微的区别:
# cat /proc/meminfo |
实际上差了约 13MB 左右,不过 zoneinfo 和 meminfo 的输出中间有少许的时间间隔,不确定是不是中间内存有了变化。
补充:进程内存
知道了系统级别的统计方法,自然会想和进程级别的统计做个对应关系。虽然有不少统计进程内存使用的方法,但基本上没办法精确地和系统统计对应。进程的统计指标一般有这几个:
VSZ
:虚拟内存,不直接对应到物理内存RSS
:常驻内存,可以理解成映射的内存的总和。注意进程间有共享的内存页(如 libc 库),不同进程加总时会重复计算这部分PSS
:与RSS
几乎相同,区别在计算时进程共享的内存时,除于了共享的进程数量,因此可以用来加总USS
:该进程独立占用的内存,即扣除了共享的内存页
VSZ
和 RSS
可以直接通过 ps aux
输出:
$ ps aux|head |
PSS
和 USS
可以通过 /proc/<pid>/smaps
中的字段统计得到。也可以用工具
smem 直接输出和统计。
# PSS:通过 Pss 字段相加得到 |
小结
介绍了几个知识点:
free
中的available
才是可用内存/剩余内存MemAvailable <= MemFree + Active(file) + Inactive(file) + SReclaimable
- MemAvailable 具体的计算方式,涉及到 ZONE, lowmem, watermark 等知识
- 补充了进程内存的一些统计方式(RSS、PSS、USS)
好吧,对写业务的我其实也没什么用。
参考
- https://access.redhat.com/solutions/22177 RedHat 对
/proc/meminfo
的解释 - Analyzing Memory Usage in Red Hat Enterprise Linux 对进程内存和物理内存映射关系的讲解
- LINUX MEMORY EXPLAINED 对进程的 VSZ、RSS、PSS、USS 有详细讲解
- /PROC/MEMINFO之谜 讲解了 meminfo 中一些“加起来不刚好”的项的原理
- https://man7.org/linux/man-pages/man5/proc.5.html
/proc/smaps
文件格式 - https://www.kernel.org/doc/Documentation/vm/pagemap.txt
/proc/pagemap
文件格式,内容上可以理解为是 smaps 的数据来源
这也是早期的大致计算逻辑: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea ↩
MemAvailable 计算的源码入口:https://elixir.bootlin.com/linux/v4.6/source/mm/page_alloc.c#L3732 ↩
保留内存的计算源码入口:https://elixir.bootlin.com/linux/v4.6/source/mm/page_alloc.c#L6248 ↩
Linux 会将物理内存切分成几个 ZONE,在 64 位机器上,一般有
ZONE_DMA
,ZONE_DMA32
和ZONE_NORMAL
,是为了兼容早期的硬件设计而划分的。 ↩关于 lowmem 和 ZONE 的细致讲解:https://zhuanlan.zhihu.com/p/68465952 ↩