Linux OOM Killer实战解析:从日志分析到问题定位
1. 当你的Linux服务器突然“发疯”OOM Killer登场不知道你有没有遇到过这种情况服务器上跑得好好的一个服务突然就没了查日志发现进程被系统“杀”了留下一脸懵的你。或者你的嵌入式设备在长时间运行后突然卡死重启查看内核日志满屏都是看不懂的内存信息。如果你点头了那你大概率是遇到了Linux世界里那个让人又爱又恨的“清道夫”——OOM Killer。OOM全称Out-Of-Memory翻译过来就是“内存耗尽”。想象一下你的系统内存就像一个固定大小的房间每个进程都是住客。当新来的进程或者老进程想扩张地盘申请“床位”内存时却发现房间已经塞得满满当当连个落脚的地方都挤不出来了。这时候内核里的OOM Killer机制就会被唤醒。它的任务很残酷但也很直接从现有的“住客”里挑一个最“该死”的干掉腾出空间让系统能继续运转下去。这个机制是Linux内核的最后一道防线防止整个系统因为内存耗尽而彻底崩溃。所以当你看到“Out of memory: Kill process”这样的日志时别慌这恰恰说明系统在努力自救。但这也绝对是一个需要你高度警惕的信号它告诉你内存管理出问题了这个问题可能来自多个方面也许是你写的应用程序存在内存泄漏像个漏水的水龙头一点点把内存“漏”光了也可能是内核的某个驱动模块在不停地“吃”内存却不释放还有一种常见但容易被忽视的情况就是内存碎片化——内存总量看起来还有不少但都被切成了零零碎碎的小块当需要一个连续的大块内存时却怎么也找不到了。接下来的内容就是带你化身“内存侦探”手把手教你如何从那一大段看似天书的OOM日志里抽丝剥茧找到问题的真凶。我们不会只停留在理论我会用我踩过无数次坑换来的经验结合真实的日志片段告诉你每一步怎么看、怎么想、怎么做。即使你之前对内核内存管理一窍不通跟着走完这一趟你也能对OOM事件了如指掌从容应对。2. 案发现场解读OOM内核日志当OOM事件发生时内核会像尽职的警察一样把案发现场的详细情况记录在系统日志里通常是/var/log/messages或/var/log/kern.log或者直接用dmesg命令查看。这份“现场报告”信息量巨大是我们破案的关键。别被它长长的篇幅吓到我们把它拆成几个关键部分一块块啃下来。2.1 第一现场谁报的警日志的第一行往往是最重要的它告诉了我们事件是如何开始的。我们来看原始文章里的例子[41311.854276] udevd invoked oom-killer: gfp_mask0x27080c0(GFP_KERNEL_ACCOUNT|__GFP_ZERO|__GFP_NOTRACK), nodemask0, order1, oom_score_adj-1000我来给你翻译一下这行“黑话”[41311.854276]这是时间戳表示系统启动后41311.854276秒发生了这个事件。udevd invoked oom-killer这是核心信息它告诉我们是udevd这个进程设备管理守护进程在尝试分配内存时失败了从而**触发invoked**了OOM Killer机制。注意udevd是触发者但不一定是受害者被杀掉的进程。gfp_mask0x27080c0(...)这是申请内存时使用的“标志位”描述了想要什么样的内存。GFP_KERNEL是最常见的标志表示这是内核的常规申请__GFP_ZERO表示希望拿到一块已经清零的内存__GFP_NOTRACK涉及内核调试工具一般不用管。看到这些常规标志通常说明这次内存申请行为本身是正常的。order1这个参数决定了申请的内存块大小。计算公式是(4KB * 2^order)。这里order1也就是申请4KB * 2^1 8KB的内存。8KB是一个非常小的申请连这么小的内存都分配失败恰恰说明当时系统内存已经极度紧张。oom_score_adj-1000这是udevd进程的OOM调整分数。这个值范围在-1000到1000之间值越小进程越不容易被OOM Killer选中杀掉。-1000意味着这个进程被刻意保护起来了通常是一些关键的系统守护进程。第一步排查点看到这里我们首先会想一个只申请8KB内存的常规操作都失败了系统到底有多“拮据”同时触发者是udevd它在做什么操作是不是在频繁地创建设备节点但这只是引子我们需要继续往下看。2.2 调用栈案发的来龙去脉紧接着触发信息后面的是一段内核的函数调用堆栈Call Trace。它就像一份行动路线图记录了从udevd申请内存开始到最终触发OOM Killer的整个函数调用过程。[41311.878101] CPU: 0 PID: 1069 Comm: udevd Not tainted 4.9.191 #147 ... [c017ae5c] (__alloc_pages_nodemask0x884/0x928) from [c0116428] (copy_process.part.30x160/0x1574) [c0116428] (copy_process.part.3) from [c011798c] (_do_fork0xb0/0x358) [c011798c] (_do_fork) from [c0117ca4] (sys_fork0x20/0x28)这段堆栈信息非常技术化但我们不需要理解每一个函数。我们关注的重点是终点堆栈的尽头最下面是sys_fork-_do_fork-copy_process。这说明udevd触发OOM是因为它在尝试fork一个子进程fork系统调用会复制父进程的内存空间需要分配新内存。关键节点__alloc_pages_nodemask是内核底层分配物理页面的核心函数它返回失败直接导致了上游的copy_process失败。有无可疑角色快速扫一眼整个调用链出现的函数名都是像copy_process、_do_fork这样的内核通用函数没有出现某个特定驱动的函数比如xxx_driver_alloc。这是一个好迹象说明问题大概率不是由某个有bug的驱动函数直接引起的我们把怀疑范围可以暂时收窄到内存状态本身或用户态进程。2.3 内存“体检报告”系统当时到底有多惨这是OOM日志里信息最密集、也最关键的部分它给系统内存做了一次全景扫描。我们分块来看。第一部分内存分类统计Mem-InfoMem-Info: active_anon:1158 inactive_anon:25 isolated_anon:0 active_file:49363 inactive_file:59329 isolated_file:0 unevictable:0 dirty:1 writeback:0 unstable:0 slab_reclaimable:1446 slab_unreclaimable:1541 mapped:988 shmem:26 pagetables:53 bounce:0 free:2975 free_pcp:174 free_cma:0这些项都是以内存页Page通常4KB为单位的。我们挑几个最重要的说active_anon/inactive_anon匿名页。这是和进程直接相关、且没有对应磁盘文件的内存比如程序的堆heap、栈stack。active表示最近被访问过inactive表示最近没怎么用。两者之和就是所有进程占用的“常驻内存”的大头。active_file/inactive_file文件页。这是缓存cache和缓冲区buffer的内存。比如你读了一个文件内核会把它缓存在内存里下次再读就快了。这部分内存是可以被回收的回收后下次需要再从磁盘读。slab_reclaimable/slab_unreclaimableSlab内存。这是内核为自己各种数据结构如进程描述符、网络套接字等分配的内存池。reclaimable部分可以回收unreclaimable部分则不能。如果slab_unreclaimable异常高要警惕内核模块或驱动有内存泄漏。free这就是系统当前完全空闲的内存页数。2975 pages约等于11.6MB。在总内存512MB的系统里只剩这么点空闲内存确实非常危险了。第二部分详细内存状态Node 0 ...Node 0 active_anon:4632kB inactive_anon:100kB active_file:197452kB inactive_file:237316kB ... Normal free:11900kB min:2832kB low:3540kB high:4248kB ...这里把上面的页数转换成了KB看得更直观。最重要的是Normal free:11900kB约11.6MB以及后面的三个水位线min最低水位线。空闲内存低于此线内核会开始积极回收内存。low低水位线。低于此线内核启动同步内存回收可能阻塞申请内存的进程。high高水位线。内核回收内存的目标希望空闲内存能维持在此线之上。当前空闲内存11900kB虽然高于min2832kB但请注意下面这行Normal: 1445*4kB (UMH) 629*8kB (UMH) 48*16kB (UMH) 4*32kB (MH) 3*64kB (M) 0*128kB ...这行展示了空闲内存的碎片化情况。它告诉我们空闲内存虽然总计11900kB但最大的一块连续内存只有3*64kB 192KB标记为M可能表示可移动类型。其余全是更小的碎片大量4KB、8KB的块。这就是典型的内存碎片化——总量有但全是碎的当需要分配一个稍大的连续内存块比如order2即大于16KB时就会失败。第三部分交换空间Swap信息Free swap 0kB Total swap 0kB这是一个非常重要的信息它显示系统的交换分区Swap大小为0也就是根本没启用Swap。在没有Swap的系统中一旦物理内存耗尽OOM Killer是唯一的选择没有缓冲余地。这也是很多生产环境服务器为了性能考虑关闭Swap后更容易触发OOM的原因。2.4 死亡名单与最终判决日志的最后内核列出了当时所有候选进程的“罪证”并做出了最终裁决。[ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name [ 1159] 0 1159 5664 1681 17 0 0 0 playerdemo Out of memory: Kill process 1159 (playerdemo) score 13 or sacrifice child Killed process 1159 (playerdemo) total-vm:22656kB, anon-rss:3680kB, file-rss:3044kB, shmem-rss:0kB进程列表展示了每个进程的ID、用户、虚拟内存大小total_vm、物理内存占用rss常驻内存集以及最重要的oom_score_adj。计分规则OOM Killer会为每个进程计算一个“坏分数”oom_scorerss占用越大、oom_score_adj越高的进程分数就越高。oom_score_adj是我们可以调整的范围-1000到1000用来保护关键进程或标记可杀进程。最终判决系统计算后决定杀死playerdemo进程PID 1159它的oom_score是13。从数据看它的rss为1681页约6.6MBanon-rss匿名页约3.6MB并不算特别夸张。它被选中可能是因为其他更占内存的进程如bash、systemd被oom_score_adj保护值为负或0而它分数最高。看到这里我们已经完成了一次完整的现场勘查。playerdemo是受害者但真凶是谁是它自己内存泄漏还是被碎片化的内存“误伤”我们需要进入下一阶段的深度调查。3. 追凶指南三种常见OOM场景的排查路径通过分析日志我们对系统死前的状态有了清晰认识。现在根据现场留下的线索我们可以沿着不同的路径去追查真凶。主要有三大怀疑方向。3.1 场景一应用程序内存泄漏这是最常见的情况。你的应用程序比如上面的playerdemo在运行中不断分配内存malloc或new但却因为编程疏忽如忘记free、delete或循环引用等没有正确释放。内存像沙漏一样慢慢流失最终进程的rss大到被OOM Killer盯上。排查特征OOM日志中被选中的进程victim的rss和anon-rss异常高远超其正常水平。在系统运行期间通过top或ps命令观察该进程的RES即rss字段会随时间持续增长即使在其空闲时也不下降。系统总的空闲内存free可能缓慢下降但slab和缓存file内存变化不大。实战排查工具Valgrind这是C/C程序员的“内存检测神器”。它能在程序运行时插入检查代码精准定位内存泄漏的位置。# 1. 编译程序时务必加上 -g 选项保留调试符号不要strip。 gcc -g -o my_app my_app.c # 2. 使用Valgrind运行你的程序--leak-checkfull 表示完全检查。 valgrind --leak-checkfull --show-reachableyes ./my_app程序运行结束后Valgrind会输出一份详细的报告告诉你哪些内存块在程序结束时没有被释放并且可以精确到源代码文件和行号。对于后台服务你可能需要让程序运行一段时间后再优雅退出以观察累积泄漏。进阶排查监控/proc/[pid]/status对于已经在线运行的服务你可以定期抓取其内存状态watch -n 5 cat /proc/$(pidof your_app)/status | grep -E VmRSS|VmSize|VmData观察VmRSS物理内存和VmData数据段的变化趋势。如果只增不减泄漏的可能性就很大。3.2 场景二内核或驱动内存泄漏这种情况更隐蔽也更棘手。问题出在内核空间可能是某个内核模块、设备驱动在分配了内核内存kmalloc,vmalloc后没有释放。这部分内存体现在OOM日志的slab_unreclaimable字段中。排查特征OOM日志中slab_unreclaimable的数值异常高并且随着系统运行时间持续增长。使用slabtop命令需root动态观察会发现某个或某几个slab对象如kmalloc-128,dentry,inode_cache的OBJS或USE数量只增不减。即使杀掉所有用户进程这部分内存也不会被释放。深度排查三板斧第一板斧对比/proc/slabinfo在系统启动后、业务开始前保存一份干净的slabinfo快照cat /proc/slabinfo slabinfo_start.log。让系统在疑似泄漏的环境下运行足够长的时间比如24小时压力测试。再次保存快照cat /proc/slabinfo slabinfo_end.log。使用diff或编写脚本对比两个文件重点关注active_objs活跃对象数和num_objs总对象数增长明显的slab缓存。例如发现kmalloc-128的对象数从1万涨到了10万那它就很可疑。第二板斧追踪分配调用栈找到可疑的slab缓存后比如kmalloc-128内核提供了更详细的追踪接口需要内核配置CONFIG_SLUB_DEBUGy等。# 查看是哪些内核函数分配了 kmalloc-128 的内存 cat /sys/kernel/slab/kmalloc-128/alloc_calls这个文件会列出分配该大小内存的内核函数地址及次数。如果某个函数分配次数异常多它就是重大嫌疑犯。但这里显示的是函数地址你需要使用addr2line工具或内核符号表/proc/kallsyms将其解析为函数名。addr2line -e /usr/lib/debug/boot/vmlinux-$(uname -r) 0xffffffffc0123456第三板斧动态插桩打印堆栈如果第二步找到了可疑函数但不知道是谁调用了它可以在内核源码中该函数入口处加入dump_stack()函数。这样每次这个函数被调用时就会把当前的调用堆栈打印到内核日志中。通过分析堆栈你就能找到源头是哪个驱动或模块。注意这需要你能够重新编译和安装内核模块甚至内核本身属于高级操作。3.3 场景三内存碎片化这是原始文章例子中的真实原因。系统总内存看起来还够甚至还有10多MB空闲但全是4KB、8KB的碎片。当内核或应用程序需要分配一块较大的连续物理内存例如一个order3的32KB页面或者DMA所需的内存时就会失败。排查特征OOM日志中free内存可能还有不少但下面的Normal:行显示最大连续块很小如只有3*64kB。触发OOM的申请order可能并不大比如order2申请16KB但系统就是无法满足。日志中可能直接打印了COMPACTION is disabled!!!这样的警告表明内核的内存碎片整理 compaction 功能被禁用。查看/proc/pagetypeinfo文件会发现Normal区的Free pages大量集中在order0即单页而高阶order3的连续空闲页数为0或极少。解决方案与预防启用内核Compaction确保内核配置了CONFIG_COMPACTIONy。这个功能就像磁盘碎片整理它会尝试移动可移动的页面把小碎片合并成大块连续空间。在日志例子中就是因为这个功能被禁用COMPACTION is disabled!!!导致碎片无法缓解。使用CMA连续内存分配器对于嵌入式等对连续大块内存有强烈需求的场景可以在内核中配置CMA区域。这是一块预留的、物理连续的内存区域专供特定驱动如GPU、视频编解码使用。调整申请策略对于驱动程序如果可能使用dma_alloc_coherent等带有GFP标志的API这些API在分配失败时可能会尝试更积极的回收和整理。终极方案重启服务对于由用户态进程长期运行导致的外部碎片有时最有效的方法是定期重启该进程。很多大型互联网公司也正是这样做的通过容器或服务编排系统定期重启实例来释放积累的内存碎片。4. 防患于未然OOM的监控、调优与应急处理破案很重要但更好的情况是让案子根本不要发生。这一部分我们聊聊如何建立监控防线如何调整系统减少OOM发生以及当OOM即将发生或刚发生时有哪些应急手段。4.1 建立监控预警系统不能等进程被杀了再查日志我们要在内存紧张时就收到警报。监控关键指标使用PrometheusGrafana、Zabbix等监控系统持续采集以下数据节点内存使用率100% - (free buffers cached)/total。slab_unreclaimable的增长趋势。关键进程的RSS和VSZ。/proc/pagetypeinfo中高阶空闲页的数量。设置合理阈值为内存使用率设置警告如85%和紧急如95%阈值。监控slab_unreclaimable如果其值超过总内存的10%并持续增长就要发出警告。利用内核事件更高级的做法是监听内核的oom_notify事件或者解析/dev/kmsg一旦出现invoked oom-killer字样的日志立即触发告警并保存现场如自动执行dmesg -T并归档。4.2 核心调优参数oom_score_adj这是你保护核心进程、标记次要进程最直接的武器。oom_score_adj取值范围是-1000到1000它会被换算成基础oom_score的一部分。值越低进程越不容易被杀死。如何调整# 查看进程当前的adj值 cat /proc/[PID]/oom_score_adj # 保护一个极其重要的进程比如数据库 echo -1000 /proc/[pid-of-mysqld]/oom_score_adj # 标记一个可以优先牺牲的进程比如某个日志收集客户端 echo 500 /proc/[pid-of-log-agent]/oom_score_adj最佳实践在启动关键服务的systemd service文件或容器启动脚本中就通过OOMScoreAdjust指令systemd或--oom-score-adj参数Docker来预设这个值。永远不要假设默认值就是安全的。4.3 应急处理与取证当收到告警但进程还未被杀时或者刚刚发生OOM你需要快速反应保存现场。立即保存内核日志dmesg -T /tmp/dmesg_$(date %s).log。OOM日志在环形缓冲区里可能会被后续日志覆盖第一时间保存最重要。快速系统快照# 保存内存状态 cat /proc/meminfo /tmp/meminfo_snapshot.log # 保存slab状态 cat /proc/slabinfo /tmp/slabinfo_snapshot.log # 保存所有进程内存信息 ps aux --sort-rss /tmp/ps_snapshot.log # 保存页表信息 cat /proc/pagetypeinfo /tmp/pagetypeinfo_snapshot.log尝试手动释放内存# 清理页面缓存PageCache对业务可能有短暂影响但能快速缓解压力 sync; echo 1 /proc/sys/vm/drop_caches # 清理dentries和inodes缓存 sync; echo 2 /proc/sys/vm/drop_caches # 清理页面缓存、dentries和inodes sync; echo 3 /proc/sys/vm/drop_caches注意这只是一个临时救急手段它会清空磁盘缓存可能导致后续磁盘读操作变慢。它不会释放进程占用的匿名内存RSS或不可回收的slab内存。找出“内存大户”快速使用top命令按M大写根据RES内存排序或者使用htop直观地看到哪个进程占用了最多物理内存。4.4 关于Swap的思考原始案例中Swap为0这加剧了OOM的发生。是否使用Swap是一个权衡启用Swap的好处给系统一个缓冲池当物理内存不足时可以将不活跃的页面换出到磁盘避免立即触发OOM Killer给管理员更多反应时间。启用Swap的坏处如果内存不足是常态频繁的Swap换入换出Swap Thrashing会导致磁盘IO飙升系统响应慢如蜗牛这比杀死一个进程更糟糕。我的经验是对于服务器建议配置一个适当大小的Swap比如内存的50%-100%但不超过一定上限如64GB。更重要的是要设置/proc/sys/vm/swappiness参数默认值60。这个值0-100控制内核使用Swap的积极性。对于数据库等期望缓存更多的应用可以调低如10-30对于桌面系统可以保持默认或调高。你可以通过echo 10 /proc/sys/vm/swappiness来临时调整。内存管理是Linux系统里既复杂又迷人的一部分。处理OOM问题就像一场侦探游戏日志是线索工具是装备而你对系统原理的理解就是破案的思维模型。最开始看那些日志肯定会头大但按照我们今天梳理的流程——先看触发源和调用栈再精读内存“体检报告”最后根据特征选择排查路径——多分析几次你就会越来越熟练。记住每一次OOM都不是偶然它一定是系统状态、应用行为和内核机制共同作用的结果。耐心分析你总能找到那个“内存黑洞”。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2416347.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!