一 OOM案例演示
强调:演示之前,先把swap关掉,防止内存不够用之后使用swap影响你看到OOM的效果,至于swap的影响我们后续会详细介绍。
C语言内存空间分配函malloc()数简介:
mem_alloc.c
编译生成可执行文件
运行测试,该进程启动后30s后,会开始循环申请内容,每次申请1M,当内存不足时,就会被系统杀死,你可以调整下面1000这个值来控制总共申请内存的大小
接下来我们把该程序打包到容器里,是一样的效果,因为容器本身就是进程嘛
然后紧接着我们为该该容器设置mem cgroup上限为515MB
容器启动后运行mem_alloc,不断申请内存,直到512MB,会触发OOM被干掉
二 OOM介绍
2.1 什么是OOM?
OOM 全称 Out of Memory(内存不足/内存溢出),是linux系统的一种保护机制,当物理内存不够用时,linux系统的killer会杀死某个正在运行的进程来释放内存,当然不会随机杀进程,具体杀哪个进程是会对进程进行评分的,后续将详细介绍
2.2 何时会发生OOM?
OOM分为整个系统级别,以及cgroup控制组级别
1、无论如何,只要内存不足时肯定会触发OOM,是站在整个系统的角度去杀进程,所有进程都是目标
2、内存充足,但是所有进程对内存的占用超过了规定的限制(使用mem cgroup限制),也会触发OOM killer,只能杀死本控制组内进程
2.3 为何一定要发生OOM?
在linux中,内存申请是通过调用malloc()完成的,一旦内存不足,该方法调用失败就可以了,不是吗?为何要杀死进程呢?
这个理解是错误的,因为linux系统的内存都是超配的overcommit。即程序申请的内存其实是虚拟内存,操作系统给程序的只是一个地址范围,该地址范围对应到物理内存上,而且只有在程序往这个地址对应的空间里写入数据时才会真正占用物理内存,所以程序申请的内存是可以超过物理内存大小的。例如物理内存有1G,但是malloc()可以申请2G的内存。
示例1:申请内存,获得虚拟地址,过30s后,根据虚拟地址应用物理内存
打开另外一个终端查看,第一次查看VIRT为106624KB(~100M),RSS只有616KB
过大概30s后查看。VIRT不变,RSS物理内存被占用到了53960KB(~50M),RSS才是真正占用物理内存的大小,RRS内存包括了代码段内存、栈内存、堆内存、共享库的内存,这些内存是进程运行必须要有的,我们代码里malloc/memset得到的内存就是堆内存,具体RSS大小可以查看/proc/[pid]/smaps文件
这种overcommit的内存申请模式的好处是,可以有效提高系统内存的利用率,但坏处就是在所有进程都试图真的用完申请的内存空间时,物理内存可能会发生不够用,系统为了保命,只能杀掉一些进程,这就是OOM。这有点像航空公司多卖几张票,一百个座位卖105张票,如果105个人真的都去坐飞机了,那就告诉多出来的5个人,今天由于天气原因航班取消了,感谢您的体谅
当然oom并不会随便拉出一个容器干掉
而是有规则的,
系统可用的页面数乘以oom_score_adj,然后再加上已用的页面数
得到的结果越大,那oom时被杀掉的几率就越大
详见2.5小节
2.4 储备知识:了解MMU、TLB与大页内存HugePages
我们知道,CPU是通过寻址来访问内存的。32位CPU的寻址宽度是 0~0xFFFFFFFF ,16^8 计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。
但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。
为了解决此类问题,现代CPU引入了 MMU(Memory Management Unit 内存管理单元)。
MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。
MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。
内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。
这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。
在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
在上文中提到,虚拟地址与物理地址需要通过映射,才能使CPU正常工作。
而映射就需要存储映射表。在现代CPU架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)的地方。
如下图:
从这张图中,可以清晰地看到CPU与页表,物理内存之间的交互关系。
进一步优化,引入TLB(Translation lookaside buffer,页表寄存器缓冲)
由上一节可知,页表是被存储在内存中的。我们知道CPU通过总线访问内存,肯定慢于直接访问寄存器的。
为了进一步优化性能,现代CPU架构引入了TLB,用来缓存一部分经常访问的页表内容。
如下图:
对比前面那张图,在中间加入了TLB。
为什么要支持大内存分页?
TLB是有限的,这点毫无疑问。当超出TLB的存储极限时,就会发生 TLB miss,之后,OS就会命令CPU去访问内存上的页表。如果频繁的出现TLB miss,程序的性能会下降地很快。
为了让TLB可以存储更多的页地址映射关系,我们的做法是调大内存分页大小。
如果一个页4M,对比一个页4K,前者可以让TLB多存储1000个页地址映射关系,性能的提升是比较可观的。
调整OS内存分页
在[Linux]()和windows下要启用大内存页,有一些限制和设置步骤。
Linux:
限制:需要2.6内核以上或2.4内核已打大内存页补丁。
确认是否支持,请在终端敲如下命令:
# cat /proc/meminfo | grep Huge
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB # hugepagesize设置见文档:https://www.jianshu.com/p/b9470fc331dd
如果有HugePage字样的输出内容,说明你的OS是支持大内存分页的。Hugepagesize就是默认的大内存页size。
接下来,为了让JVM可以调整大内存页size,需要设置下OS 共享内存段最大值 和 大内存页数量。
共享内存段最大值
建议这个值大于Java Heap size,这个例子里设置了4G内存。
# echo 4294967295 > /proc/sys/kernel/shmmax
大内存页数量
# echo 154 > /proc/sys/vm/nr_hugepages
这个值一般是 Java进程占用最大内存/单个页的大小 ,比如java设置 1.5G,单个页 10M,那么数量为 1536/10 = 154。
注意:因为proc是内存FS,为了不让你的设置在重启后被冲掉,建议写个脚本放到 init 阶段(rc.local)。
numa
numa是一种关于多个cpu如何访问内存的架构模型,现在的cpu基本都是numa架构,linux内核2.5开始支持numa。
numa架构简单点儿说就是,一个物理cpu(一般包含多个逻辑cpu或者说多个核心)构成一个node,这个node不仅包括cpu,还包括一组内存插槽,也就是说一个物理cpu以及一块内存构成了一个node。每个cpu可以访问自己node下的内存,也可以访问其他node的内存,但是访问速度是不一样的,自己node下的更快。numactl –hardware命令可以查看node状况。
通过numactl启动程序,可以指定node绑定规则和内存使用规则。可以通过cpunodebind参数使进程使用固定node上的cpu,使用localalloc参数指定进程只使用cpu所在node上分配的内存。如果分配的node上的内存足够用,这样可以减少抖动,提供性能。如果内存紧张,则应该使用interleave参数,否则进程会因为只能使用部分内存而out of memory或者使用swap区造成性能下降。
NUMA的内存分配策略有localalloc、preferred、membind、interleave。
- localalloc规定进程从当前node上请求分配内存;
- preferred比较宽松地指定了一个推荐的node来获取内存,如果被推荐的node上没有足够内存,进程可以尝试别的node。
- membind可以指定若干个node,进程只能从这些指定的node上请求分配内存。
- interleave规定进程从指定的若干个node上以RR(Round Robin 轮询调度)算法交织地请求分配内存。
2.5 OOM发生时什么进程会被杀掉
当OOM发生时,并不会随机杀进程,具体杀哪个进程是会调用内核了一个oom_badness()函数来评定,主要就是两个指标
1、进程已经使用的物理内存页面数
2、每个进程的OOM校准值oom_score_adj
oom_badness()函数会计算一个评分
评分=系统总的可用页面数 * oom_score_adj值 + 进程已经使用的物理页面数
该评分的值越大,被OOM kill掉的几率就越大。
三 memory cgroup
每个Cgroups的子系统都是通过一个虚拟文件系统挂载点的方式,挂到一个缺省的目录下。在linux发行版里,memory cgroup一般是挂载到/sys/fs/cgroup/memory目录下,我们创建一个子目录作为控制组。
控制组参数有很多,我们主要关注3个
参数1:memory.limit_in_bytes:
参数2、memory.oom_control
参数3、memory.usage_in_bytes
四 如何定位OOM问题与解决
启动容器,占用1000M内存