近日接了一些oom案子,此类问题通常是客户自身业务导致的问题。但现在客户的提问越来越复杂,通常情况下我们需要站在客户一侧提供“协助”的技术服务。oom类案例通过一年多的学习和探讨,我将其分为3类:
1、内存真的不足
2、文件数到达上限
3、lowmem内存不足
内存真的不足的情况针对于64位系统,这时可能导致的原因有:进程hung住占用大量内存、进程申请连续大页导致溢出、本身内存负载接近临界值。这时可尝试升级内存配置、优化业务进程来解决。
单个进程最多允许打开的文件句柄数(包括socket连接数)是有限制的,当大于这个系统限制时,程序会抛出大量的无法打开文件的报错。这种情况设涉及到几个内核参数:
/proc/sys/fs/nr_open——系统文件系统支持文件句柄总数上限,默认值1048576(1M),Linux2.6.25开始增加该内核参数,用于替换内核宏NR_OPEN(1048576),该值上限受限于系统内存。
/proc/sys/fs/file-max——系统文件系统支持文件句柄总数最大值,必须小于/proc/sys/fs/nr_open或NR_OPEN,增加该值时,必须同步修改/proc/sys/fs/inode-max = 4*/proc/sys/fs/file-max。
因此当出现messages中存在相关句柄数达上限之类的提示时就可以适当调高上述内核参数
对于32位机器会出现一些内存使用率不高却仍然发生oom的情况,这是因为对于32位机器内核采用lowmem来管理highmem。LowMem 区 (也叫 NORMAL ZONE ) 一共 880 MB,而且不能改变(除非用 hugemem 内核)。对于高负载的系统,就可能因为 LowMem 利用不好而引发 OOM Killer 。一个可能原因是 LowFree 太少了,另外一个原因是 LowMem 里都是碎片,请求不到连续的内存区域。
检查当前 LowFree 的值:
# cat /proc/meminfo |grep LowFree
检查LowMem内存碎片:
# cat /proc/buddyinfo
这时可以通过升级到64位系统、使用hugemem内核(安装hugemem kernel RPM)、适当调高/proc/sys/vm/lower_zone_protection(lower_zone_protection越高,系统越倾向于保护lowmem)来解决。
oom的kill机制
如果oom_kill_allcating_task设置为非零值,则oom根据score_adj来选择杀掉的进程。 /proc/[pid]/oom_adj ,该pid进程被oom killer杀掉的权重,介于 [-17,15]之间,越高的权重,意味着更可能被oom killer选中,-17表示禁止被kill掉。关于kill机制,涉及到2个内核参数:
1、oom_kill_allocating_task
控制在OOM时是否杀死触发OOM的进程。
如果设置为0,OOM killer会扫描进程列表,选择一个进程来杀死。通常都会选择消耗内存内存最多的进程,杀死这样的进程后可以释放大量的内存。
如果设置为非零值,OOM killer只会简单地将触发OOM的进程杀死,避免遍历进程列表(代价比较大)。如果panic_on_oom被设置,则会忽略oom_kill_allocating_task的值。
默认值是0。
2、vm.panic_on_oom
控制内核在OOM发生时时是否panic。
如果设置为0,内核会杀死内存占用过多的进程。通常杀死内存占用最多的进程,系统就会恢复。
如果设置为1,在发生OOM时,内核会panic。然而,如果一个进程通过内存策略或进程绑定限制了可以使用的节点,并且这些节点的内存已经耗尽,oom-killer可能会杀死一个进程来释放内存。在这种情况下,内核不会panic,因为其他节点的内存可能还有空闲,这意味着整个系统的内存状况还没有处于崩溃状态。
如果设置为2,在发生OOM时总是会强制panic,即使在上面讨论的情况下也一样。即使在memory cgroup限制下发生的OOM,整个系统也会panic。
默认值是0。
将该参数设置为1或2,通常用于集群的故障切换。选择何种方式,取决于你的故障切换策略。
代码判断逻辑如下:
void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask,
int order, nodemask_t *nodemask, bool force_kill)
{
// 等待notifier调用链返回,如果有内存了则返回
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
if (freed > 0)
return;
// 如果进程即将退出,则表明可能会有内存可以使用了,返回
if (fatal_signal_pending(current) || current->flags & PF_EXITING) {
set_thread_flag(TIF_MEMDIE);
return;
}
// 如果设置了sysctl的panic_on_oom,则内核直接panic
check_panic_on_oom(constraint, gfp_mask, order, mpol_mask);
// 如果设置了oom_kill_allocating_task
// 则杀死正在申请内存的process
if (sysctl_oom_kill_allocating_task && current->mm &&
!oom_unkillable_task(current, NULL, nodemask) &&
current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
get_task_struct(current);
oom_kill_process(current, gfp_mask, order, 0, totalpages, NULL,
nodemask,
"Out of memory (oom_kill_allocating_task)");
goto out;
}
// 用select_bad_process()选择badness指
// 数(oom_score)最高的进程
p = select_bad_process(&points, totalpages, mpol_mask, force_kill);
if (!p) {
dump_header(NULL, gfp_mask, order, NULL, mpol_mask);
panic("Out of memory and no killable processes...\n");
}
if (p != (void *)-1UL) {
// 查看child process, 是否是要被killed,则直接影响当前这个parent进程
oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,
nodemask, "Out of memory");
killed = 1;
}
out:
if (killed)
schedule_timeout_killable(1);
}
计算权值时会将是否为系统进程、进程RSS和swap内存占用考虑进去。当然也可自行设置:echo 17 > /proc/[pid]/oom_adj(不允许oom杀掉这个进程)
计算逻辑代码如下:
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
// 内部判断是否是pid为1的initd进程,是否是kthread内核进程,是否是其他cgroup,如果是则跳过
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p = find_lock_task_mm(p);
if (!p)
return 0;
// 获得/proc/[pid]/oom_adj权值,如果是OOM_SCORE_ADJ_MIN则返回
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN) {
task_unlock(p);
return 0;
}
// 获得进程RSS和swap内存占用
points = get_mm_rss(p->mm) + p->mm->nr_ptes +
get_mm_counter(p->mm, MM_SWAPENTS);
task_unlock(p);
// 计算步骤如下,【计算逻辑比较简单,不赘述了】
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
adj -= 30;
adj *= totalpages / 1000;
points += adj;
return points > 0 ? points : 1;
}
相关内核参数解析
dirty_background_bytes
当脏页所占的内存数量超过dirty_background_bytes时,内核的pdflush线程开始回写脏页。
dirty_background_ratio
默认值 :10
参数意义:当脏页所占的百分比(相对于所有可用内存,即空闲内存页+可回收内存页)达到dirty_background_ratio时内核的pdflush线程开始回写脏页。增大会使用更多内存用于缓冲,可以提高系统的读写性能。当需要持续、恒定的写入场合时,应该降低该数值。
注意:dirty_background_bytes参数和dirty_background_ratio参数是相对的,只能指定其中一个。当其中一个参数文件被写入时,会立即开始计算脏页限制,并且会将另一个参数的值清零。
dirty_bytes
当脏页所占的内存数量达到dirty_bytes时,执行磁盘写操作的进程自己开始回写脏数据。
注意:dirty_bytes参数和dirty_ratio参数是相对的,只能指定其中一个。当其中一个参数文件被写入时,会立即开始计算脏页限制,并且会将另一个参数的值清零
dirty_ratio
默认值:40
参数意义:当脏页所占的百分比(相对于所有可用内存,即空闲内存页+可回收内存页)达到dirty_ratio时,进程pdflush会自己开始回写脏数据。增大会使用更多系统内存用于缓冲,可以提高系统的读写性能。当需要持续、恒定的写入场合时,应该降低该数值。
dirty_expire_centisecs
默认值:2999参数意义:用来指定内存中数据是多长时间才算脏(dirty)数据。指定的值是按100算做一秒计算。只有当超过这个值后,才会触发内核进程pdflush将dirty数据写到磁盘。
dirty_writeback_centisecs
默认值:499这个参数会触发pdflush回写进程定期唤醒并将old数据写到磁盘。每次的唤醒的间隔,是以数字100算做1秒。如果将这项值设为500就相当5秒唤醒pdflush进程。如果将这项值设为0就表示完全禁止定期回写数据。
drop_caches
向/proc/sys/vm/drop_caches文件中写入数值可以使内核释放page cache,dentries和inodes缓存所占的内存。
只释放pagecache:
echo 1 > /proc/sys/vm/drop_caches
只释放dentries和inodes缓存:
echo 2 > /proc/sys/vm/drop_caches
释放pagecache、dentries和inodes缓存:
echo 3 > /proc/sys/vm/drop_caches
这个操作不是破坏性操作,脏的对象(比如脏页)不会被释放,因此要首先运行sync命令。
注:这个只能是手动释放
legacy_va_layout
该文件表示是否使用最新的32位共享内存mmap()系统调用,Linux支持的共享内存分配方式包括mmap(),Posix,System VIPC。
0,使用最新32位mmap()系统调用。
1,使用2.4内核提供的系统调用。
lowmem_reserve_ratio
保留的lowmem,3列分别为DMA/normal/HighMem
在有高端内存的机器上,从低端内存域给应用层进程分配内存是很危险的,因为这些内存可以通过mlock()系统锁定,或者变成不可用的swap空间。在有大量高端内存的机器上,缺少可以回收的低端内存是致命的。因此如果可以使用高端内存,Linux页面分配器不会使用低端内存。这意味着,内核会保护一定数量的低端内存,避免被用户空间锁定。
这个参数同样可以适用于16M的ISA DMA区域,如果可以使用低端或高端内存,则不会使用该区域。
lowmem_reserve_ratio参数决定了内核保护这些低端内存域的强度。预留的内存值和lowmem_reserve_ratio数组中的值是倒数关系,如果值是256,则代表1/256,即为0.39%的zone内存大小。如果想要预留更多页,应该设更小一点的值。
min_free_kbytes
这个参数用来指定强制Linux VM保留的内存区域的最小值,单位是kb。VM会使用这个参数的值来计算系统中每个低端内存域的watermark[WMARK_MIN]值。每个低端内存域都会根据这个参数保留一定数量的空闲内存页。
一部分少量的内存用来满足PF_MEMALLOC类型的内存分配请求。如果进程设置了PF_MEMALLOC标志,表示不能让这个进程分配内存失败,可以分配保留的内存。并不是所有进程都有的。kswapd、direct reclaim的process等在回收的时候会设置这个标志,因为回收的时候它们还要为自己分配一些内存。有了PF_MEMALLOC标志,它们就可以获得保留的低端内存。
如果设置的值小于1024KB,系统很容易崩溃,在负载较高时很容易死锁。如果设置的值太大,系统会经常OOM。
max_map_count
进程中内存映射区域的最大数量。在调用malloc,直接调用mmap和mprotect和加载共享库时会产生内存映射区域。虽然大多数程序需要的内存映射区域不超过1000个,但是特定的程序,特别是malloc调试器,可能需要很多,例如每次分配都会产生一到两个内存映射区域。默认值是65536。
mmap_min_addr
指定用户进程通过mmap可使用的最小虚拟内存地址,以避免其在低地址空间产生映射导致安全问题;如果非0,则不允许mmap到NULL页,而此功能可在出现NULL指针时调试Kernel;mmap用于将文件映射至内存;该设置意味着禁止用户进程访问low 4k地址空间
nr_pdflush_threads
当前pdfflush线程数量,为read-only。
oom_dump_tasks
如果启用,在内核执行OOM-killing时会打印系统内进程的信息(不包括内核线程),信息包括pid、uid、tgid、vm size、rss、nr_ptes,swapents,oom_score_adj和进程名称。这些信息可以帮助找出为什么OOM killer被执行,找到导致OOM的进程,以及了解为什么进程会被选中。
如果将参数置为0,不会打印系统内进程的信息。对于有数千个进程的大型系统来说,打印每个进程的内存状态信息并不可行。这些信息可能并不需要,因此不应该在OOM的情况下牺牲性能来打印这些信息。
如果设置为非零值,任何时候只要发生OOM killer,都会打印系统内进程的信息。
默认值是1(启用)。
OOM killer(Out-Of-Memory killer):监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉
oom_kill_allocating_task
控制在OOM时是否杀死触发OOM的进程。
如果设置为0,OOM killer会扫描进程列表,选择一个进程来杀死。通常都会选择消耗内存内存最多的进程,杀死这样的进程后可以释放大量的内存。
如果设置为非零值,OOM killer只会简单地将触发OOM的进程杀死,避免遍历进程列表(代价比较大)。如果panic_on_oom被设置,则会忽略oom_kill_allocating_task的值。
默认值是0。
panic_on_oom
控制内核在OOM发生时时是否panic。
如果设置为0,内核会杀死内存占用过多的进程。通常杀死内存占用最多的进程,系统就会恢复。
如果设置为1,在发生OOM时,内核会panic。然而,如果一个进程通过内存策略或进程绑定限制了可以使用的节点,并且这些节点的内存已经耗尽,oom-killer可能会杀死一个进程来释放内存。在这种情况下,内核不会panic,因为其他节点的内存可能还有空闲,这意味着整个系统的内存状况还没有处于崩溃状态。
如果设置为2,在发生OOM时总是会强制panic,即使在上面讨论的情况下也一样。即使在memory cgroup限制下发生的OOM,整个系统也会panic。
默认值是0。
将该参数设置为1或2,通常用于集群的故障切换。选择何种方式,取决于你的故障切换策略。
overcommit_memory
默认值为:0从内核文档里得知,该参数有三个值,分别是:0:当用户空间请求更多的的内存时,内核尝试估算出剩余可用的内存。
1:当设这个参数值为1时,内核允许超量使用内存直到用完为止,主要用于科学计算
2:当设这个参数值为2时,内核会使用一个决不过量使用内存的算法,即系统整个内存地址空间不能超过swap+50%的RAM值,50%参数的设定是在overcommit_ratio中设定。
overcommit_ratio
默认值为:50这个参数值只有在vm.overcommit_memory=2的情况下,这个参数才会生效。该值为物理内存比率,当overcommit_memory=2时,进程可使用的swap空间不可超过PM * overcommit_ratio/100
Swappiness
该参数控制是否使用swap分区,以及使用的比例。设置的值越大,内核会越倾向于使用swap。如果设置为0,内核只有在看空闲的和基于文件的内存页数量小于内存域的高水位线(应该指的是watermark[high])时才开始swap。
默认值是60。
vfs_cache_pressure
控制内核回收dentry和inode cache内存的倾向。
默认值是100,内核会根据pagecache和swapcache的回收情况,让dentry和inode cache的内存占用量保持在一个相对公平的百分比上。
减小vfs_cache_pressure会让内核更倾向于保留dentry和inode cache。当vfs_cache_pressure等于0,在内存紧张时,内核也不会回收dentry和inode cache,这容易导致OOM。如果vfs_cache_pressure的值超过100,内核会更倾向于回收dentry和inode cache。
优化的建议
echo 【调高】(针对32位) > /proc/sys/vm/lower_zone_protection
echo 【调低】 > /proc/sys/vm/min_free_kbytes
echo 【调高】 > /proc/sys/vm/dirty_ratio
echo 【调低】 > /proc/sys/vm/dirty_background_ratio
echo 【调低】> /proc/sys/vm/lowmem_reserve_ratio
echo 【调高】> /proc/sys/vm/swappiness
echo 【调高】> /proc/sys/fs/file-max
echo 2 > /proc/sys/vm/overcommit_memory
调高/etc/security/limits.conf连接数目
当然也可通过直接关闭oom规避,但是这样很危险,可能会导致系统发生crash
关闭/打开oom-killer:
# echo "0" > /proc/sys/vm/oom-kill
# echo "1" > /proc/sys/vm/oom-kill
转载于:https://blog.51cto.com/12814931/2343623