彻底理解虚拟内存:从一个demo看free/ps/top内存含义

2020-06-01 10:48:30 浏览数 (1)

1,背景

笔者团队发现现网服务负载即将达到瓶颈,但cpu利用率并未达到瓶颈,基于充分利用机器资源的考量,研发同学提出:“降低nginx worker数,腾出一部分内存,随后提高业务程序worker数,从而提升业务处理能力”的解决方案。

为了确保方案的可靠实施,我们需要在充分理解free/ps/top等命令有关内存信息准确含义的前提下,分析机器当前的内存情况、以及各worker的内存占用情况,明确nginx和业务程序的worker数分别调低和调高多少,既不会出现内存紧张、又能充分利用机器资源,于是有了本文的几个demo实验。

2,概念

在展示代码之前,我们先一起回忆一下操作系统课学过的几个概念。

虚拟内存——虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。[1]

Memory Overcommit——Memory overcommitment is a concept in computing that covers the assignment of more memory to virtual computing devices (or processes) than the physical machine they are hosted, or running on, actually has.[2]

缺页中断——页缺失(英语:Page fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。[3]

3,demo

上面的概念大家或多或少都了解一些,下面我们从代码及进程启动后的内存占用情况来一一说明。

3.1,申请内存但不使用

代码语言:javascript复制
//mem_never_use.cpp 
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    sleep(1024000);
    return 0;
}

编译执行结果为:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1408952     3381420      288216    11375684    14362492
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 1000000000 &
[1] 32233
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 1000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     32233  0.0  0.0 994340  1120 pts/4    S    20:29   0:00 ./mem_never_use 1000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409060     3381308      288228    11375688    14362388
Swap:             0           0           0

这里先以manpage中的说明来解释下ps输出的含义(man ps)[4]

VSZ——virtual memory size of the process in KiB (1024-byte units).

RSS——resident set size, the non-swapped physical memory that a task has used (in kiloBytes).

结论

可以看出,进程的虚拟内存占用为994340KB,与入参申请分配的大小基本一致;而实际物理内存占用仅1MB,说明执行new分配内存只是操作系统内部做了一个标记,不会立刻实际分配。

3.2,多个进程申请超出总内存,但均不使用

既然new完不会立刻实际分配,那我们会想,最大能new多大的内存?现在我们继续用上节的代码实验一下:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 20000000000
new mem: 20000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
已放弃
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409300     3366564      288228    11390192    14362008
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[2] 7313
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[3] 7316
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[4] 7318
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      7313  0.0  0.0 9783404 1124 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
root      7316  0.0  0.0 9783404 1124 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
root      7318  0.0  0.0 9783404 1120 pts/4    S    11:19   0:00 ./mem_never_use 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409980     3365896      288228    11390180    14361352
Swap:             0           0           0

结论

1,机器总内存16GB,一次性申请new 20GB内存,会申请失败。

2,三个进程,分别new 10GB内存,没问题。

3,free命令的输出无明显变化。“虚拟内存”的占用,在free命令无法展示出来。(这个结论很重要)

3.3,单个进程分次申请超出总内存,但均不使用

接上节,我们再来验证一下,如果分多次申请10GB的行为在同一个进程内,会出现什么结果?

代码语言:javascript复制
//mem_never_use_twice.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes first time" << std::endl;
    char* s0 = new char[i];
    std::cout << "new mem: " << i << " bytes second time" << std::endl;
    char* s1 = new char[i];
    sleep(1024000);
    return 0;
}

编译执行结果如下:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413396     9884072      288228     4868588    14360040
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use_twice 10000000000 &
[1] 6939
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes first time
new mem: 10000000000 bytes second time

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      6939  0.0  0.0 19549032 1124 pts/4   S    11:30   0:00 ./mem_never_use_twice 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413620     9883808      288228     4868628    14359820
Swap:             0           0           0

说明上节的结论依然有效,单个进程new过的内存20GB,已经超出机器总内存。

3.4,overcommit概述及演示

目前我们已经涉及到overcommit和OOM killer的概念,操作系统允许特定条件下申请内存数大于系统内存,本质上它是系统为了充分利用资源而提供的一种特性。

commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。[5]关于更详细的解释,大家可以去看这篇参考文献,这里举一个关闭overcommit的例子:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# cat /proc/sys/vm/overcommit_memory #笔者注:本行到此才结束
0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# echo 2 > /proc/sys/vm/overcommit_memory #笔者注:本行到此才结束
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 10000000000 &
[1] 10692
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 10000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc

[1]   已放弃               ./mem_never_use 10000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409804     3365220      288228    11391032    14361488
Swap:             0           0           0

overcommit_memory这个内核参数用于控制内核对overcommit的处理行为,最常用的是默认值0,上面我改成2(代表禁止overcommit)再去申请分配10GB内存会立刻报失败。这个值在多数场景下取默认值比较好。现在改回默认值继续下文实验。

3.5,申请内存并全部使用

代码语言:javascript复制
//mem_use_all.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    std::cout << "now memset the memory" << std::endl;
    memset(s, 0, i);
    sleep(1024000);
    return 0;
}

编译执行如下:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1409512     3362872      288228    11393672    14361724
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_all 1000000000 &
[1] 3233
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 1000000000 bytes
now memset the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      3233  2.5  6.0 994340 977656 pts/4   S    19:28   0:00 ./mem_use_all 1000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     2389516     2382876      288228    11393664    13381716
Swap:             0           0           0

结论

1,ps的VSZ和RSS都是900多MB,与入参1GB相符,说明memset的内存写操作触发了真实物理内存的占用。

2,free命令展示的used、free、available值在程序执行前后分别变动了900多MB,与结论1相印证。

细说free

老规矩,先上manpage[6]:(中文为笔者注)(单位都是KB)

used—— Used memory (calculated as total - free - buffers - cache)(用户已使用的物理内存,可以看到其中排除了buff/cache)

free—— Unused memory (MemFree and SwapFree in /proc/meminfo)(整机未使用的物理内存)

buffers—— Memory used by kernel buffers (Buffers in /proc/meminfo)(内核用的)

cache—— Memory used by the page cache and slabs (Cached and Slab in /proc/meminfo)(内核用的)

buff/cache—— Sum of buffers and cache(可以理解为内核用来作缓存的一部分内存,可以随时释放出来给用户用)

available—— Estimation of how much memory is available for starting new applications, without swapping.(一个估算值,真正可供用户进程使用的内存值,比free那列更有意义)

总结:used free buff cache = total

现在我们再回头看上面free执行的结果:

1,used变多900多MB,用户自己进程用掉的。

2,free变少900多MB,说明机器直接从free里面取的,而没有释放buff/cache,这可以理解,因为free还够用。

3,available变少900多MB,buff/cache基本没动,那自然总的可用值要减少900多MB。

3.6,申请大量内存并全部使用

现在继续用上节的代码,我们来看一下用户进程占走buff/cache的情况,我new 5GB内存:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1412020     3359124      288228    11394912    14359260
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_all 5000000000 &
[1] 7644
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 5000000000 bytes
now memset the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      7644 26.4 30.2 4900592 4884064 pts/4 S    19:50   0:02 ./mem_use_all 5000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     6299272      103932      288228     9762852     9472172
Swap:             0           0           0

used和available分别变多和变少约5GB,不用多说。我们重点看free和buff/cache:

free变少3GB,buff/cache变少2GB。

说明进程新占用的5GB内存,有3GB来自原来的空闲物理内存,有2GB来自内核从缓存中释放出来的内存。

内核的策略是优先用free里面的,用到剩差不多100MB的时候,开始从缓存中释放内存给用户进程使用。

3.7,申请但只使用部分内存

本文讲到这里,相信读者可以自行判断下面这个demo的执行结果了

代码语言:javascript复制
//mem_use_one_fourth.cpp
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[]) {
    long long i = atoll(argv[1]);
    std::cout << "new mem: " << i << " bytes" << std::endl;
    char* s = new char[i];
    std::cout << "now memset 1/4 of the memory" << std::endl;
    memset(s, 0, i/4);
    sleep(1024000);
    return 0;
}

编译执行如下:

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1407576     4994720      296420     9763760    14355616
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_use_one_fourth 4000000000 &
[1] 9364
[root@VM_144_234_centos ~/demo/virt_mem_demo]# new mem: 4000000000 bytes
now memset 1/4 of the memory

[root@VM_144_234_centos ~/demo/virt_mem_demo]# ps axu| head -n1 && ps axu|grep -v grep |grep mem_                                         
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      9364 10.2  6.0 3924028 977656 pts/4  S    19:59   0:00 ./mem_use_one_fourth 4000000000
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     2385940     4016344      296420     9763772    13377248
Swap:             0           0           0

VSZ是4GB,RSS是1GB;used增加1GB,free减少1GB,buff/cache不变,available减少1GB。

3.8,单次可申请的最大内存

我们看到前文的内存申请有时失败,有时成功。那么成功与否的条件到底是什么,是内核Heuristic overcommit算法在每次申请时计算出来的一个值[5],不考虑swap的话这个值大致就是available列的值。

代码语言:javascript复制
[root@VM_144_234_centos ~/demo/virt_mem_demo]# free
              total        used        free      shared  buff/cache   available
Mem:       16166056     1413328     9882304      288228     4870424    14359988
Swap:             0           0           0
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 14000000000 
new mem: 14000000000 bytes
^C
[root@VM_144_234_centos ~/demo/virt_mem_demo]# ./mem_never_use 15000000000 
new mem: 15000000000 bytes
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
已放弃 

4,环境说明

本文演示运行环境为CentOS 7, Linux 3.10.107-1,8C16G机器。

top命令内存信息跟ps基本一样,具体可自行查阅manpage。

5,不同系统输出差异

部分老版本系统的命令输出可能会有一些差异,这里再以CentOS 6, Linux 2.6.32.43上的输出为例进行一点补充说明。

代码语言:javascript复制
[root@VM_1_1_centos ~/demo/cpp]# free
             total       used       free     shared    buffers     cached
Mem:       8191216    3226828    4964388          0     320988    1691800
-/  buffers/cache:    1214040    6977176
Swap:      2097144     590204    1506940

这个展示就没有新版内核那么好理解了,而且含义有差异。

Mem那行的公式是:total = used free,这个used代表整机已使用的内存,基本等于新内核里used buff cache;free含义没变化。

buff/cache那行的公式也是:total = used free,这个used代表:刨去buff/cache后用户使用的内存,即新内核里used的含义;free代表:Mem行的free buff/cache,有点类似新内核的available(注意只是类似)。

现在你再看buff/cache那行开头的“-/ ”符号,明白它的含义了吗?[7]分别代表Mem行的used减去、free加上(buff cache)后的值。即公式:

3226828 - 320988 - 1691800 = 1214040

4964388 320988 1691800 = 6977176

6,总结

看完demo,该回正题了,调整worker数时候到底该关注哪个内存指标?

我的观点是,不能只看available,它只代表当前瞬时的可用内存;还要关注你的代码行为预期。

如果你的程序是python等GC型编程语言,那你不能只关注瞬时情况,还需要对程序的内存占用情况进行一段时间观察,尤其是GC期间的内存波动情况,可能出现短时大量虚拟内存发生缺页占用物理内存的情况;

如果是c 等自主管理内存的程序,那你应该对自己程序的内存占用有一个清晰的预判:多个进程情况下总overcommit多少内存是ok的,超出多少会有多大概率发生OOM。

想清楚上面的事情,相信你对机器上的内存申请量会有自己的一个合理规划了。

7,关键词

缺页、Minor Fault、Major Fault、sar、overcommit、OOM、brk、mmap、SWAP

8,参考

[1], https://zh.wikipedia.org/wiki/虚拟内存

[2], https://en.wikipedia.org/wiki/Memory_overcommitment

[3], https://zh.wikipedia.org/wiki/页缺失

[4], http://man7.org/linux/man-pages/man1/ps.1.html

[5], 《理解LINUX的MEMORY OVERCOMMIT》,http://linuxperf.com/?p=102

[6], https://www.man7.org/linux/man-pages/man1/free.1.html

[7], https://serverfault.com/questions/85470/meaning-of-the-buffers-cache-line-in-the-output-of-free

0 人点赞