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