OOM问题排查及原因解析

一、前言

最近公司线上出了故障,有业务反馈说线上某台机器发出的请求status都是101,代表是超时。于是顺着调用栈和监控去查,最后发现这台机器上的网关挂掉了,所以导致请求发不出去,导致业务超时。那为啥无缘无故的网关挂掉了呢,顺着各种系统日志去查,结合机器是mq消费脚本专用机器,最后从系统日志/var/log/messages中查到如下日志

...
Out of memory: Kill process 9682 (mysqld) score 9 or sacrifice child
Killed process 9682, UID 27, (mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kB
httpd invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
httpd cpuset=/ mems_allowed=0
Pid: 8911, comm: httpd Not tainted 2.6.32-279.1.1.el6.i686 #1
...

现在才意识到,该机器是跑mq脚本的固定机器,由于进程开的太多导致内存占用太大,导致内存不够从而发生OOM kill问题。

二、深入理解OOM

2.1 Linux OverCommit

Linux下允许程序申请比系统可用内存更多的内存(如malloc函数),这个特性叫Overcommit。这么做是出于优化系统的考虑,因为并不是所有的程序申请了内存就立刻使用,当使用的时候说不定系统已经回收了一些内存资源了。不过当需要真正使用内存资源而系统已经没有多余的内存资源可用时,OOM机制就被触发了。

Linux下有3种Overcommit策略,可以通过/proc/sys/vm/overcommit_memory配置,取0、1和2三个值,默认是0:

>取值0:启发式策略,比较多的内存申请可能会被拒绝,如当前内存2G,突然申请1T的内存(一般当系统启动selinux模块时有效,其他情况等同取值1);

>取值1:允许分配比当前内存资源多的内存;

 >取值2:系统所能分配的内存资源不能超过swap+内存资源*系数(/proc/sys/vm/overcommit_ratio,默认50%,可调整)。如果资源已经用光,再有内存申请请求时,都会返回错误。

2.2  oom_killer

oom_killer(out of memory killer)是Linux内核的一种内存管理机制,在系统可用内存较少的情况下,内核为保证系统还能够继续运行下去,会选择杀掉一些进程释放掉一些内存。通常oom_killer的触发流程是:进程A想要分配物理内存(通常是当进程真正去读写一块内核已经“分配”给它的内存)->触发缺页异常->内核去分配物理内存->物理内存不够了,触发OOM

一句话说明oom_killer的功能:当系统物理内存不足时,oom_killer遍历当前所有进程,根据进程的内存使用情况进行打分,然后从中选择一个分数最高的进程,杀之取内存。

2.3 OOM kill策略

Linux下每个进程都有一个OOM权重,在/proc/<pid>/oom_adj里面,取值是-17到+15(为-17此进程不会被杀掉),取值越高,越容易被杀掉。

最终OOM-Killer是通过/proc/<pid>/oom_score这个值来决定哪个进程被杀死。这个值是系统综合进程的内存消耗量、CPU时间(utime+stime)、存活时间(utime - start_time)和oom_adj计算出的,消耗内存越多oom_score值越高,存活时间越长值越低。

另外,Linux在计算进程的内存消耗的时候,会将子进程所耗内存的一半算到父进程中.

总之,OOM-Killer策略是:损失最少的工作,释放最大的内存;同时不伤及无辜的用了很大内存的进程,并且杀掉的进程数尽量少。

2.4 OOM Kill 实现机制

(1) 查找/proc/sys/vm/panic_on_oom设置,如果值为2,引起Kernel Panic内核恐慌则会停止掉所有进程10s自动重启系统;一般默认为0。

(2) 判断/proc/sys/vm/oom_kill_allocating_task设置:为1,直接将当前进程杀死,默认为0。

(3) 获取/proc/sys/vm/overcommit_memory中的配置的值: 如果值为2,直接将当前进程杀死;为0:判断panic_on_oom是否有值,有值直接panic,否则进入下一步。

(4) 判断/proc/sys/vm/would_have_oomkilled设置:值为1,也不会真正的去杀死进程。

(5) 判断接口/proc/<pid>/oom_adj设置值为-17,表示该进程不可被杀死。

(6) 接下来调用 select_bad_process() 选择一个“bad”进程杀掉,判断和选择一个“bad”进程的过程由 oom_badness()决定,最 bad 的那个进程就是那个最占用内存的进程。

(7) root 权限的进程通常被认为很重要,不应该被轻易杀掉,所以打分的时候可以得到 3% 的优惠(分数越低越不容易被杀掉);可以在用户空间通过操作每个进程的 oom_adj 内核参数来决定哪些进程不这么容易被 OOM killer 选中杀掉;如果计算分数为0也就是告知OOM killer,该进程是“good process”,不要干掉它。

(8) 先杀死当前进程的子进程,在关闭父进程。

(9) 当内存资源紧张,同时也没有可以杀死的进程时,系统会panic;

2.5 选择bad进程函数实现



    static void __out_of_memory(gfp_t gfp_mask, int order)
    {
        struct task_struct *p;
        unsigned long points;
      //如果sysctl_oom_kill_allocating_task值设置了,就会直接杀掉申请内存的进程。
        if (sysctl_oom_kill_allocating_task)
            if (!oom_kill_process(current, gfp_mask, order, 0, NULL,
                    "Out of memory (oom_kill_allocating_task)"))
                return;
    retry:
        /*
         * Rambo mode: Shoot down a process and hope it solves whatever
         * issues we may have.
         */
        p = select_bad_process(&points, NULL);

        if (PTR_ERR(p) == -1UL)
            return;

        /* Found nothing?!?! Either we hang forever, or we panic. */
        if (!p) {
            read_unlock(&tasklist_lock);
            panic("Out of memory and no killable processes...\n");
        }

        if (oom_kill_process(p, gfp_mask, order, points, NULL,
                 "Out of memory"))
            goto retry;

static struct task_struct *select_bad_process(unsigned long *ppoints,struct mem_cgroup *mem)
{
    struct task_struct *p;
    struct task_struct *chosen = NULL;
    struct timespec uptime;
    *ppoints = 0;

    do_posix_clock_monotonic_gettime(&uptime);
    for_each_process(p) {//遍历所有的进程包括用户进程和内核进程
        unsigned long points;

        /*
         * skip kernel threads and tasks which have already released
         * their mm. 跳过内核进程
         */
        if (!p->mm)
            continue;
        /* skip the init task 跳过Init进程*/
        if (is_global_init(p))
            continue;
        if (mem && !task_in_mem_cgroup(p, mem))
            continue;

        
        if (test_tsk_thread_flag(p, TIF_MEMDIE))
            return ERR_PTR(-1UL);

        if (p->flags & PF_EXITING) {
            if (p != current)
                return ERR_PTR(-1UL);

            chosen = p;
            *ppoints = ULONG_MAX;
        }
//这里就是 #define OOM_DISABLE (-17) 也就是/proc/<pid>/oom_adj这个值
        if (p->signal->oom_adj == OOM_DISABLE)
            continue;
//对其它的进程调用badness()函数来计算相应的score,score最高的将被选中
        points = badness(p, uptime.tv_sec);
        if (points > *ppoints || !chosen) {
            chosen = p;
            *ppoints = points;
        }
    }

    return chosen;
}



    unsigned long badness(struct task_struct *p, unsigned long uptime)
    {
        unsigned long points, cpu_time, run_time;
        struct mm_struct *mm;
        struct task_struct *child;
        int oom_adj = p->signal->oom_adj;
        struct task_cputime task_time;
        unsigned long utime;
        unsigned long stime;
       //如果OOM是被禁止的,则直接返回。
        if (oom_adj == OOM_DISABLE)
            return 0;

        task_lock(p);
        mm = p->mm;
        if (!mm) {
            task_unlock(p);
            return 0;
        }

        /*
         * The memory size of the process is the basis for the badness.
          该进程占用的内存大小
         */
        points = mm->total_vm;

        /*
         * After this unlock we can no longer dereference local variable `mm'
         */
        task_unlock(p);

        /*
         * swapoff can easily use up all memory, so kill those first.
         */
        if (p->flags & PF_OOM_ORIGIN)
            return ULONG_MAX;

        list_for_each_entry(child, &p->children, sibling) {
            task_lock(child);
     //如果该进程含有子进程,该进程子进程total_vm的一半加入到points中
            if (child->mm != mm && child->mm)
                points += child->mm->total_vm/2 + 1;
            task_unlock(child);
        }

        /*
         * CPU time is in tens of seconds and run time is in thousands
             * of seconds. There is no particular reason for this other than
             * that it turned out to work very well in practice.
         */
        thread_group_cputime(p, &task_time);
        utime = cputime_to_jiffies(task_time.utime);
        stime = cputime_to_jiffies(task_time.stime);
        cpu_time = (utime + stime) >> (SHIFT_HZ + 3);


        if (uptime >= p->start_time.tv_sec)
            run_time = (uptime - p->start_time.tv_sec) >> 10;
        else
            run_time = 0;
    // score和进程的cpu_time以及run_time成反比,也就是该进程运行的时间越长,score值越低。
        if (cpu_time)
            points /= int_sqrt(cpu_time);
        if (run_time)
            points /= int_sqrt(int_sqrt(run_time));

        /*
         * Niced processes are most likely less important, so double
         * their badness points. nice大于0的进程,score翻倍,nice的范围一般是-20~+19,值越大优先级越低。
         */
        if (task_nice(p) > 0)
            points *= 2;

        /*
         * Superuser processes are usually more important, so we make it
         * less likely that we kill those. 对设置了超级权限的进程降低score,具有超级权限的进程更加重要。
         */
        if (has_capability_noaudit(p, CAP_SYS_ADMIN) ||
         has_capability_noaudit(p, CAP_SYS_RESOURCE))
            points /= 4;

        /*
         * We don't want to kill a process with direct hardware access.
         * Not only could that mess up the hardware, but usually users
         * tend to only have this flag set on applications they think
         * of as important. 对设置了超级权限的进程降低score
         */
        if (has_capability_noaudit(p, CAP_SYS_RAWIO))
            points /= 4;

        /*
         * If p's nodes don't overlap ours, it may still help to kill p
         * because p may have allocated or otherwise mapped memory on
         * this node before. However it will be less likely.
    如果和p进程在内存上没有交集的进程降低score
         */
        if (!has_intersects_mems_allowed(p))
            points /= 8;

        /*
         * Adjust the score by oom_adj.
    最后是根据该进程的oom_adj进行移位操作,计算最终的score,这样根据各个策略就计算出来scope值,该值越大,进程被杀死的概率也就越高
         */
        if (oom_adj) {
            if (oom_adj > 0) {
                if (!points)
                    points = 1;
                points <<= oom_adj;
            } else
                points >>= -(oom_adj);
        }

    #ifdef DEBUG
        printk(KERN_DEBUG "OOMkill: task %d (%s) got %lu points\n",
        p->pid, p->comm, points);
    #endif
        return points;
    }



    static void __oom_kill_task(struct task_struct *p, int verbose)
    {
        if (is_global_init(p)) {
            WARN_ON(1);
            printk(KERN_WARNING "tried to kill init!\n");
            return;
        }

        if (!p->mm) {
            WARN_ON(1);
            printk(KERN_WARNING "tried to kill an mm-less task!\n");
            return;
        }

        if (verbose)
            printk(KERN_ERR "Killed process %d (%s)\n",
                    task_pid_nr(p), p->comm);

        p->rt.time_slice = HZ;
        set_tsk_thread_flag(p, TIF_MEMDIE);

        force_sig(SIGKILL, p);
    }

函数badness()就是根据各种条件进行判断,找到一个最应该杀死的进程。主要的选择条件是下面的几点:

(1)score初始值为该进程占用的total_vm;

(2)如果该进程有子进程,子进程独自占用的total_vm/2加到本进程score;

(3)score随着该进程的cpu_time以及run_time的增长而减少,也就是运行的时间越长,被kill掉的几率越小

(4) nice大于0的进程,score*2;

(5)对于拥有超级权限的进程,或者直接磁盘交互的进程降低score;

(6)如果和current进程在内存上没有交集,则该进程降低score;

(7)最后根据该进程的oom_adj,计算得出最终的score;

2.6 关闭OOM可以吗?

关闭OOM机制,虽然不会出现杀进程的现象,但是应用程序在申请内存资源时由于内存资源紧张导致申请不到资源,可能会出现一直循环申请内存,直到申请到为止,在未申请到内存资源之前,会一直占用某个CPU不放,导致机器卡住。

三、如何排查OOM问题

3.1 查看系统日志

查看系统日志/var/log/messages会发现Out of Memory: Kill process 1865(sshd)类似的错误信息。

也可以运行egrep -i -r 'killed process' /var/log 命令或者 dmesg 命令查看被杀掉进程的内存占用情况。

3.2 java排查

(1) 确认是不是内存本身就分配过小

方法:jmap -heap pid

(2) 找到最耗内存的对象

方法:jmap -histo:live pid | more

如果发现某类对象占用内存很大(例如几个G),很可能是类对象创建太多,且一直未释放。例如:

  • 申请完资源后,未调用close()或dispose()释放资源

  • 消费者消费速度慢(或停止消费了),而生产者不断往队列中投递任务,导致队列中任务累积过多

画外音:线上执行该命令会强制执行一次fgc。另外还可以dump内存进行分析。
(3) Linux命令行工具

查看进程创建的线程数,以及网络连接数,如果资源耗尽,也可能出现OOM

  • pstree

  • netstat

进程打开的句柄数和线程数

  • ll /proc/${PID}/fd | wc -l

  • ll /proc/${PID}/task | wc -l (效果等同pstree -p | wc -l)

四、如何避免发生OOM问题

4.1 防止重要的系统进程触发(OOM)机制而被杀死

可以设置参数/proc/PID/oom_adj为-17,可临时关闭linux内核的OOM机制。内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以/proc/PID/oom_score中找到。

使用的解决办法:

1、限制java进程的max heap,并且降低java程序的worker数量,从而降低内存使用

2、发现系统没有开启swap,给系统加了8G的swap空间

其它解决办法(不推荐),不允许内存申请过量:

# echo "2" > /proc/sys/vm/overcommit_memory

# echo "80" > /proc/sys/vm/overcommit_ratio

 

——————————————————————————————

参考来源:1、http://blog.chinaunix.net/uid-20788636-id-4308527.html

2、https://cloud.tencent.com/developer/article/1157275

3、https://blog.csdn.net/zgrjkflmkyc/article/details/77645570

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值