【内存优化】内存优化以及oom排查整体思路

news2024/11/16 9:40:31

linux疑难问题排查实战专栏,分享了作为公司专家,在解决内存、性能、各类死机等疑难问题的排查经验,认真学习可以让你在日后工作中大放光彩。

        本文总结介绍了项目开发过程中oom排查和内存优化的一些方法,主要是从内存问题查看到堆内存、栈内存、数据段内存优化为导向,结合实际优化实例介绍工具和方法的运用,展示每种优化方法优化过程,希望对读者有所帮助。

从下面几个角度展开:

        1. 分析oom打印,确认各个进程物理内存占用情况,是否有明显使用不合理的进程

        2. 确认glibc中的malloc缓存机制

        3. Free、/proc/meminfo确认内存是否在逐步减少

        4. 内存泄漏问题定位

        5. 内存优化:栈、数据段、代码段优化

        6. 其他优化点:drop_cache、线程裁剪、程序瘦身、结构体裁剪等

1、oom__killer概念介绍

        oom__killer(out of memory killer)是Linux内核的一种内存管理机制,在系统可用内存较少的情况下,内核为保证系统还能够继续运行下去,会选择杀掉一些进程释放掉一些内存。

        通常oom_killer的触发流程是:进程A想要分配物理内存(通常是当进程真正去读写一块内核已经“分配”给它的内存)->触发缺页异常->内核去分配物理内存->物理内存不够了,触发OOM。

        一句话说明oom_killer的功能:当系统物理内存不足时,oom_killer遍历当前所有进程,根据进程的内存使用情况进行打分,然后从中选择一个分数最高的进程,杀之取内存。另外可能是内核驱动、内存碎片化严重等问题也会触发OOM。

2、oom问题分析

2.1oom日志分析

1、谁触发OOM

        在系统触发OOM时,都会有现场信息打印,例如下面的:

        先来看一下第一行,它给出了oom killer是由谁触发的信息,order=0告诉我们所请求的内存的大小是多少,即ai0_P0_MAIN请求了2的0次方这么多个page的内存,也就是一个page(4KB)。

2、oom时内存情况

        可以看到normal区free的内存只有4084KB,比系统允许的最小值(min)还要低,这意味着application已经无法再从系统中申请到内存了,并且系统会开始启动oom killer来缓解系统内存压力。

        这里我们说一下一个常见的误区,就是有人会认为触发了oom-killer的进程就是问题的罪魁祸首,比如我们这个例子中的这个ai0_P0_MAIN进程。其实日志中invoke oom-killer的这个进程有时候可能只是一个受害者,因为其他应用/进程已将系统内存用尽,而这个invoke oomkiller的进程恰好在此时发起了一个分配内存的请求而已。在系统内存已经不足的情况下,任何一个内存请求都可能触发oom killer的启动。

        oom-killer的启动会使系统从用户空间转换到内核空间。内核会在短时间内进行大量的工作,比如计算每个进程的oom分值,从而筛选出最适合杀掉的进程。我们从日志中也可以看到最终杀死的进程。

3、进程内存占用分析

        Rss是物理内存的占用,可以看到build和busybox占用较多,build是主程序,占用合理,busybox占用250M明显不合理(单位是4KB)。

        此oom打印为真实开发案例:由于测试工程师运行测试脚本压力测试,测试脚本大量占用内存导致oom,非问题。

2.2Glibc缓存确认

2.2.1内存站岗问题原因

        Glibc使用了ptmalloc的内存管理方式。Glibc申请内存时是从分配区申请的,分为主分配区和非主分配区,分配区都有锁,在分配内存前需要先获取锁,然后再去申请内存。

        一般进程都是多线程的,当多个线程同时需要申请内存时,如果只有一个分配区,那么效率太低。glibc为了支持多线程的内存申请释放,会在多个线程同时需要申请内存时根据cpu核数分配一定数量的分配区,将分配区分配给线程。

        内存申请基本原理:当用户调用malloc申请内存时,glibc会查看是否已经缓存了内存,如果有缓存则会优先使用缓存内存,返回一块符合用户请求大小的内存块。

        如果没有缓存或者缓存不足则会去向操作系统申请内存(可通过brk、mmap申请内存),然后切一块内存给用户,如图所示。

        内存释放基本原理:当业务模块使用完毕后调用free释放内存时,glibc会检查该内存块虚拟地址上下内存块的使用状态(fast bin除外)。若其上一块内存空闲,则与上一块内存进行合并。若下一块内存空闲,则与下一块内存进行合并。

        若下一块内存时top chunk(top chunk一直是空闲的),则看top chunk的大小是否超过一个阈值,如果超过一个阈值则将其释放给OS,如图所示。



        内存站岗概念:内存站岗指的是glibc从OS申请到内存后分配给业务模块,业务模块使用完毕后释放了内存,但是glibc没有将这些空闲内存释放给OS,也就是缓存了很多空闲内存无法归还给系统的现象。

        内存站岗原因:glibc设计时就确定其内存是用于短生命周期的,因此在设计上内存释放给OS的时机是当top chunk的大小超过一个阈值时会释放top chunk的一部分内存给OS。当top chunk不超过阈值就不会释放内存给OS。

        那么问题来了,若与top chunk相邻的内存块一直在使用中,那么top chunk就永远也不会超过阈值,即便业务模块释放了大量内存,达到几十个G 或者上百个G,glibc也是无法将内存还给OS的。

2.2.2问题确认

        在主程序中隔一段时间调用一次malloc_stats、mallinfo,都是glibc提供的函数,用户打印内存分配函数(如malloc、free)的详细调试信息。

代码实现:

static void print_mallinfo()
{
    malloc_stats();

    struct mallinfo mi = mallinfo();

    printf("\t arena=%d, ordblks=%d, smblks=%d, hblks=%d, hblkhd=%d  \n\t usmblks=%d, fsmblks=%d, "
          "uordblks=%d, fordblks=%d, keepcost=%d\n\n",
        mi.arena,    /* non-mmapped space allocated from system */
         mi.ordblks,  /* number of free chunks */
         mi.smblks,   /* number of fastbin blocks */
         mi.hblks,    /* number of mmapped regions */
         mi.hblkhd,   /* space in mmapped regions */
         mi.usmblks,  /* maximum total allocated space */
         mi.fsmblks,  /* space available in freed fastbin blocks */
         mi.uordblks, /* total allocated space */
         mi.fordblks, /* total free space */
         mi.keepcost  /* top-most, releasable (via malloc_trim) space */
     );
 }

打印内容:

如果系统申请的内存(system bytes)远远大于在使用的内存(in use bytes),那么就怀疑可能是内存站岗问题导致glibc缓存过多。

2.2.3解决办法

1、减少子分配区个数

mallopt(M_ARENA_MAX, 2);

2、降低mmap阈值:默认值是128KB

减少内存碎片,大于64KB就直接使用mmap的方式申请内存。

mallopt(M_MMAP_THRESHOLD, 64*1024);

3、释放空闲区以减少内存碎片

malloc_trim()

        当我们通过free()释放区块时,如果相邻的空闲区块可以合并,glibc会将它们合并。但是,如果相邻的空闲区块来自不同的bin,glibc则无法将它们合并,这就会产生内存碎片。malloc_trim()的作用就是尝试释放这些未能合并但空闲的小区块,尽量减少内存碎片

        malloc_trim可以在可用内存小图某个阈值之后隔一段时间调用一次。

2.3内存泄漏

2.3.1确认是否发生泄漏

        内存泄漏排查的前提一定是确认存在泄漏的,经过2.2章节首先排除Glibc缓存的影响,那怎样就认为是发生了内存泄漏呢?

        随着程序运行,占用内存大小持续增长,及时程序闲置也是如此,那我们认为是存在内存泄漏的。

        Meminfo查看,free和MemAvailable都在逐步减少,直至oom死机,那么就是存在内存泄漏问题的。

 # cat /proc/meminfo 
MemTotal:         365924 kB   //总内存大小
MemFree:           74584 kB   //剩余内存
MemAvailable:     251220 kB   //可用内存:包含了部分buffer和cache
Buffers:           58952 kB   //设备操作缓存,如访问/dev/sda1
Cached:           164596 kB   //文件操作缓存,如访问某个文件

2.3.2泄漏源定位

        内存泄漏定位还是蛮多的:ASAN、valgrind、gperfTools、malloc重载等,在之前文章中我们已经介绍过了,读者可根据自己的需求进行选择。

详见:内存泄漏问题排查

3、内存优化

        在经过第二章的学习,我们已经可以排除Glibc缓存和内存泄漏问题导致的内存不足死机了,本章我们开始学习如何进行内存优化,发现我们程序中的不合理使用。

3.1栈内存优化

        在之前文章我们已经讲解过了,详见:栈上局部变量定义过大导致设备oom

大概思路:

        1、ulimit –a确认栈内存大小;如:4096KB

        2、通过cat proc/PID/smaps命令查看此进程中内存的申请信息,并搜索所有的4096KB为栈内存

        3、依次查看每个栈对应的真实物理内存Rss,可能绝大多数都是30KB以下,超过100KB的我们就需要关注一下是否代码实现时定义了超大的局部变量

        4、反汇编搜索“sp, sp, #”字段,后面跟随着是栈的大小

        5、可以使用脚本处理,按照大小排序,从上至下按照栈大小结合代码逐一排查

3.2堆内存优化

        堆内存的优化首先得确认何处申请了堆内存,堆内存申请的小,才能确认此处的堆内存申请是否合理,一般方法:

        1. 使用gerftools,打印出所有内存申请的堆栈,按照从打到小排序,确认内存申请是否合理。(不推荐)

        2. 各个模块实现自己的调试信息,在malloc前进行封装,每次的申请和释放内存管理起来,可用调试命令查看各模块申请未释放的内存大小、名字,各模块独自结合代码分析占用是否合理。(推荐)

        3. Malloc重载,所有申请未释放的内存打堆栈,依次解析出符号分析。(不推荐)

3.3数据段

        在之前文章我们已经讲解过了,详见:《【内存】数据段大小分析.docx》

        数据段的分析主要包含:.bss、.rodata、.data段

大体思路:

        1. 使用nm打印出不各个数据段中发符号的地址、大小、变量名称:arm-linux-nm --format=sysv build | grep .bss > bss.txt

        2. 使用脚本提取变量名称和大小

        3. 按照大小排序,从大到小依次优化每个变量

3.4代码段

        在之前文章我们已经讲解过了,详见:《【内存】代码段大小分析.docx》

大体思路:

1. 生成可执行程序build的反汇编文件

arm-mix410-linux-objdump -d build > objdump.log

2. nm解析执行程序依赖的各个库,并针对每个库生成一个符号文件

arm-mix410-linux-nm -A lib*.a | grep T > ./*.a.log

3. 依次遍历每个*.a.log文件,解析出函数名,在objdump.log文件中搜索,确认开始地址和结束地址以及函数代码段大小,生成*.a.log.ok文件

4. 依次遍历每个*.a.log.ko文件,计算每个库的代码段大小

3.5其他优化项

3.5.1drop_cache

定期释放linux的page cache和reclaimable slab cache,加快内存回收:

#清理文件页、目录项、inodes等各种缓存
$echo 3 > /proc/sys/vm/drop_caches

3.5.2线程裁剪

在实际的大型项目中,由于代码的不断演进,维护人员的不断变化,会存在很多无效的线程,如:

1. A项目使用的线程,B项目无需创建,但是由于代码没有做相应的控制,即使B项目部需要,但是仍然创建了

2. 某个业务功能,根据用户个数或者通道个数来创建线程,每新增一路则新增一个线程,但实际上多个对象共用一个线程也是能处理过来的。

增对上述情况,我们可以先查看线程个数,然后增加每个线程进行优化:

1. gdb thread apply all bt:查看所有线程,并结合代码依次确认其合理性

2. pidstat查看:查看pid对应的所有线程

./pidstat -p pid -t 

注:

1. 想在上述两种方法中看到线程名字,需要在线程执行体中调用:

prctl(PR_SET_NAME, "thread_name");  //注意是在线程执行体中调用

2. 优化出的物理内存并不是ulimit所看到的线程大小,ulimit对应的是虚拟内存,物理内存实际没这么大

3.5.3程序瘦身

针对ko驱动、动态库和可执行程序,进行strip,删除调试信息

strip -S lib_exe_name

缺点:调试时不方便,无法解析符号

3.5.4结构体裁剪

减少复杂嵌套结构体的大小:

1. 根据情况减小数组下标

2. 根据本产品业务需求,可用编译宏进行控制处理,删除较大的无效结构体成员

3.5.5内存文件系统

        Mount查看当前设备挂载情况,找到/dev/mem行,/dev/mem是将后面的目录挂载到内存,目录中文件是存储在内存中的,可以对该目录中文件大小做一个优化。

        查看/var目录,对/var目录做一个优化。

3.5.6驱动加载

        驱动加载也会申请内存,一些无用的驱动可以取消加载

4、总结

        本文总结介绍了项目开发过程中oom排查和内存优化的一些方法,主要是从内存问题查看到堆内存、栈内存、数据段内存优化为导向,结合实际优化实例介绍工具和方法的运用,展示每种优化方法优化过程,希望对读者有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/734888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云轻量服务器和ecs区别(最新更新)

阿里云服务器ECS和轻量应用服务器有什么区别?云服务器ECS是明星级云服务器,轻量应用服务器可以理解为简化版的云服务器ECS,轻量适用于单机应用,云服务器ECS适用于集群类高可用高容灾应用,阿里云百科来详细说下阿里云轻…

组合模式:如何设计实现支持递归遍历的文件系统目录树结构?

组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。这里的“数据”,你可以简单理解为一组对象集合,待会我们会…

使用 geopandas 和 shapely(.shp) 进行地理空间数据处理和可视化

文章目录 前言1. 安装所需库2. 读取 Shapefile 文件3. 可视化地图4. 用户输入坐标和清除指定区域内的图形5. 可视化删除指定区域内的图形之后的地图6. 保存为新的 Shapefile (.shp)文件完整代码及解析分析说明 测试文件地址特别说明完结 前言 在地理信息系统(Geogra…

力扣竞赛勋章 | 排名分数计算脚本

文章目录 力扣竞赛勋章介绍竞赛评分算法脚本(本文的重点内容)运行结果 代码修改自:https://leetcode.cn/circle/discuss/6gnvEj/ 原帖子的代码无法正常运行。 力扣竞赛勋章介绍 https://leetcode.cn/circle/discuss/0fKGDu/ 如果你想知道自…

【Elasticsearch】初识elasticsearch

目录 初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 1.1.2.ELK技术栈 1.1.3.elasticsearch和lucene 1.1.4.为什么不是其他搜索技术? 1.1.5.总结 1.2.倒排索引 1.2.1.正向索引 1.2.2.倒排索引 1.2.3.正向和倒排 1.3.es的一些概念 1.3.1.文档…

前端各种方法自我整理

Javascript方法 slice [slaɪs]切片 slice (-2)取出数组中倒数两个植变生成一个新数组 slice(0,3)取出数组下标0到下标3的值,生成新数组 includes [ɪnˈkluːdz]包含 查看数组或字符串内是否有该值,有返回true,无返回false 例子&#…

Vue--》Vue3打造可扩展的项目管理系统后台的完整指南(十二)完结篇

今天开始使用 vue3 + ts 搭建一个项目管理的后台,因为文章会将项目的每一个地方代码的书写都会讲解到,所以本项目会分成好几篇文章进行讲解,我会在最后一篇文章中会将项目代码开源到我的GithHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏…

linux常用压缩/解压缩命令的使用

目录 gzipbzip2tar gzip gzip 的常用选项: -l(list) 列出压缩文件的内容。 -k(keep) 在压缩或解压时,保留输入文件。 -d(decompress) 将压缩文件进行解压缩。 如果 gzip 不加任何选项,此时为压缩。压缩完该文件会生成后缀为.gz 的压缩文…

java读取excel,指定列A列为空,将下方空行上移,并将指定列F列数据拼接

java读取excel,将空行上移 改造前: 效果图: 上代码: import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook;import java.io.FileInputStream; import java.io.FileOutputStream; import jav…

SqueezeNet算法解析—鸟类识别—Paddle实战

文章目录 一、理论基础1.前言2.设计理念2.1 CNN微架构(CNN MicroArchitecture)2.2 CNN宏架构(CNN MacroArchitecture)2.3 模型网络设计探索过程2.4 结构设计策略2.5 Fire模块 3.网络结构4.评估分析 二、实战1.数据预处理2.数据读取…

树莓派登录密码忘记了怎么办????

我们常常会不记得我们的密码,比如说我近期想玩一下我们树莓派开发版,登录的时候忘记了怎么办,第一想到的就是重刷系统,从头再来,今天我给大家带来了一个解决忘记了登录密码如何去修改它,从而进入系统里。 …

分享一款超级好用的下载软件,IDM--不限速,破解版,IDM破解版下载安装

本片文章分为四个部分: 第一部分,下载安装IDM 第二部分,IDM破解 第三部分,浏览器插件的安装 第四部分,百度网盘使用IDM下载 下载IDM 首先大家可以在浏览器搜索IDM 网址:https://www.internetdownloadma…

Zynq PS无法读取SD卡(TF卡)异常分析及如何读写SD卡

最近我正在进行一个Zynq项目,我使用了Xilinx SDK自带的系统库函数来读取SD卡中的配置信息。然而,一直读取异常。 我进行了一系列的调试和检查,包括确认PS部分的代码正确性以及Xilffs的配置是否正确,但是问题仍然没有解决。最后&am…

NYDIG最新报告:比特币今年上半年回报率达83.6%,远超其他资产类别

本文要点: 比特币的强劲势头延续到了二季度,本季度比特币上涨 6.8%,年初至今上涨 83.6%。 二季度末,一系列新的现货比特币 ETF 申请(尤其是 ETF 巨头贝莱德的申请)提振了比特币价格。 在多家机构提交现货 B…

POI下载excel通用方法

POI下载excel通用方法 最近遇到一个业务是需要下载excel&#xff0c;使用POI,这里记录一下实现过程 1、导包 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.9</version></dependency>…

本周大新闻|传微软曾考虑收购Niantic;腾讯引进Quest挑战重重

本周XR大新闻&#xff0c;AR方面&#xff0c;传微软曾考虑收购AR头部公司Niantic&#xff1b;Q-PIXEL公布5000PPI可调色Micro LED&#xff1b;三星智能眼镜商标曝光&#xff1b;Humane公布首款产品Ai Pin&#xff1b;空客推企业级AR解决方案。 VR方面&#xff0c;Vision Pro销…

边缘计算那些事儿-漫谈网络切片关键技术

0、背景 网络切片作为一种比较前沿的技术&#xff0c;当前并没有太多系统的资料可以学习&#xff0c;很多的技术资料都是比较分散地分布在论文和一些技术博客中&#xff0c;笔者当前是通过论文的解读获取相关的技术信息&#xff0c;在过程中笔者总结了相关的技术栈&#xff0c;…

pytorch多分类问题 CrossEntropyLoss()函数的输入size/shape不一致问题

在使用pytorch实现一个多分类任务的时候&#xff0c;许多多分类任务在训练过程中都会有如下的代码&#xff1a; criterion nn.CrossEntropyLoss() loss criterion(output, target) # output.size : [batch_size, class_num] # target.size : [batch_size]许多的初学者会卡在…

Kubernetes 容器类型 Init - pause - sidecar - app容器

目录 Kubernetes 的容器类型 Init 初始化容器 参考文档&#xff1a;Init 容器 | Kubernetes 使用 Init 容器的情况 案例&#xff1a;定义了一个具有 2 个 Init 容器的简单 Pod 你通过运行下面的命令启动 Pod&#xff1a; 发现两个Init容器都没有运行成功 查看更多详细…

一些行业报告--From 艾瑞咨询

一些行业报告--From 艾瑞咨询 1 介绍2 机械臂行业研究 [From 艾瑞咨询 -- 中国工业机器人行业研究报告&#xff08;2023&#xff09;2.1 发展历程2.2 中国工业机器人相关政策2.3 产业链2.4 三大部分六大系统2.5 伺服&控制器 主要玩家 及 关键指标及难点2.6 减速机 主要玩家…