实用干货:AIX 进程内存分配与回收策略及应用开发建议 | 运维进阶
【作者】陈炽卉,15年Power服务器领域工作经验,高级技术支持中心性能调优专家,红皮书《Power Systems Performance Guide: Implementing and Optimizing》主要作者之一。长期在金融、电信行业从事技术支持工作;对应用开发、系统优化、故障定位、基准测试等方面均有丰富经验。
目录
1. AIX 内存分配回收策略
1.1 内存分配观察示例—递增分配
1.2 内存分配观察示例—递减分配
1.3 针对长运行程序的空闲内存回收
1.4 mallopt 示例 1
1.5 mallopt 示例 2
1.6 内存回收 disclaim 策略
1.7 disclaim/disclaim64 代码示例
1.8 内存碎片对内存回收的影响
1.9 通用建议
2. 内存监控
2.1 观察系统中内存占用最高的进程
2.2 寻找内存持续增长的进程
2.3 如何通过共享内存 ID 对应关联到该共享内存的进程
2.4 如何获取 AIX Kernel 的内存使用率
2.5 如何判断系统是否存在内存不足
3. 应用开发工具
3.1 dbx 使用以及 coredump 定位
3.2 内存非法使用检查
3.3 内存泄漏检查(report_allocations)
3.4 内存泄漏检查示例
3.5 proctools 介绍
3.6 probevue 介绍
3.7 如何将 C 文件与汇编文件对应
1.AIX 内存分配回收策略
一般而言, 系统会直接在进程空间的free列表中维护其free释放的内存, 以供后续新的分配直接使用,这样可以提高分配效率,不需要每次内存分配都经过系统内核。进程退出后,系统会回收该进程占用的全部内存。
注:选择不同的分配策略时,对空闲内存空间的管理策略会有所差异。例如默认的管理结构是cartesian树;而采用watson分配算法时,使用的管理结构是红黑树。
Cartesian笛卡尔树参考结构:
1.1 内存分配观察示例—递增分配
进程的详细内存分配情况可以使用svmon来观察,参考如下示例。
需要注意,为方便svmon观察,示例代码需要在malloc之后调用memset进行初始化;因为操作系统实际上并不会立即对已申请但尚未访问到的内容分配实际存储空间, 而是推迟到第一次访问时才会实际分配---这即是缺页机制的工作原理。
如下是一个申请空间递增的应用,分配/释放大小为2MB->4MB->8MB->16MB, 则通过各阶段的svmon可以看到, 内存页面会持续增长, 从2MB一直增加到16MB (注意 不是2MB+4MB+..+16MB=30MB) 。
Malloc分配2MB,未初始化时:
Address Range为0~512页,即代表512×4096=2MB虚拟地址空间。Virtual取值为2,表示该空间尚未实际分配。
初始化后:
Virtual取值为513,表明虚存空间已经实际分配。
释放之前申请的2MB,重新 申请 4MB 并初始化后:
1024×4096=4MB,此前释放的512页虚拟地址空间被重复利用。
释放之前申请的4MB,重新 申请8 8 MB 并初始化后:
此前释放的1024页虚拟地址空间被重复利用。
释放之前申请的8MB,重新 申请 16 MB 并初始化后:
1.2 内存分配观察示例—递减分配
如示例, 如果是一个申请空间递减的应用, 分配/释放大小为16MB->8MB->4MB->2MB, 通过各阶段的svmon(svmon -nrP <pid>)可以看到,内存页面始终维持在16MB。
Malloc分配16MB,未初始化时:
Address Range为0~4096页,即代表4096×4096=16MB虚拟地址空间。Inuse/Virtual取值为2,表示该空间尚未实际分配。
初始化后:
Virtual=4097页, 虚拟内存已经实际分配。
释放之前申请的16MB,重新 申请8 8 MB 并初始化后:
释放之前申请的8MB,重新 申请4 4 MB 并初始化后:
释放之前申请的4MB,重新 申请2 2 MB 并初始化后:
可以看到svmon输出结果没有变化;原因是虽然应用调用了free释放了16MB内存,但系统的处理策略是将该内存置于进程自身的空间块树中管理。下一个8MB分配,实际上是直接从进程已有的16MB空闲块中获取的。但对系统而言,进程管理的空闲块树也对应为该进程的内存消耗,所以其内存占用没有变化。
测试代码:
1.3 针对长运行程序的空闲内存回收
由于上面介绍的内存使用方式,一个常见的现象是,对于长运行进程,其占用的私有内存大小等同于最高峰时间的内存大小。
这个现象一般而言,对系统正常运行及性能的影响很小。因为如果系统空闲内存足够,这个问题不存在。而如果内存不足,由于进程本身的空闲块列表没有被引用,根据换页算法,在系统缺页(即可用内存不足)时,很容易被换出。
但如果应用进程希望对其空间内存块进行更自主的管理,可以使用mallopt接口。例如,对长运行的程序,定期在其闲时(例如每周日凌晨业务量极低时),调用 mallopt(M_DISCLAIM, 0)释放进程私有空间的free列表。这样可以避免在程序本身没有内存泄漏的情况下,因业务高峰期大量的内存申请造成的进程私有free list增长,而使得进程内存占用过大。实现起来也相当简单(参考如下 mallopt 示例)。
参考mallopt函数的帮助信息:
1.4 mallopt 示例 1
1.5 mallopt 示例 2
1.6 内存回收 disclaim 策略
如果需要设置使得系统在应用调用 free 之后,即回收其内存,可以在程序启动前设置环境变量(这两种方式都可能对程序性能有很大影响):
PSALLOC=early或者MALLOCOPTIONS=disclaim
由于PSALLOC=early实际上意味着设置MALLOCOPTIONS=disclaim,且提前分配Paging Space;所以其性能开销还要高于MALLOCOPTIONS=disclaim。但这种机制能够避免Paging Space耗尽时,进程被系统kill掉的可能;详情可以通过在如下aix infocenter网站查询PSALLOC获取。
注:
老的MALLOCTYPE=3.1也有此效果,但该分配策略往往性能一般,且只适用于32位程序,现在的系统一般不使用了。
在设置环境变量MALLOCTYPE=watson的情况下,mallopt(M_DISCLAIM,0)无效;
mallopt(M_DISCLAIM,0)只对系统默认的分配策略(即cartesian树分配算法)有效。
也可以在代码中直接用disclaim调用。注意 disclaim 调用需要在 free /delete 之前;超过 4G 的内存块需要使用 disclaim64 函数。
1.7 disclaim/disclaim64 代码示例
1.8 内存碎片对内存回收的影响
需要注意的是,上面各种释放进程私有空闲内存列表的方法,在应用出现严重内存碎片的情况下,都存在不足。这种情况需要实际应用程序通过合理设计,避免严重内存碎片。
例如一些缓存数据库表的数据结构,可能涉及大量记录的增删,且一般不会一次性全部删除,类似这种情况可能最终造成严重内存碎片;从而使得进程私有空间无法有效回收, 只能存在于进程自身的私有free list中。
举一个极端的例子,一个进程申请了4000页内存,但释放时,恰好在每个内存页上保留了一个16字节的数据结构(其他空间均释放)。这样进程的私有内存空间实际上无法进行收缩。因为mallopt(M_DISCLAIM, 0)和disclaim等方法都需要以多个页面为单位进行实际回收。
但如果应用设计良好,一般可以避免这类问题:例如一些事务性的应用,在事务处理开始时,大量申请内存;在处理结束后, 就将这些内存释放。这种情况下, 出现大量内存碎片的概率较低。
1.9 通用建议
一般对内存受限程序的建议是:
a. 尽量使用malloc而不是calloc
b. 提高引用局部性,即仅在数据结构马上要被使用前,对其进行初始化。
c. 对大的数据结构,如果使用一次后,后续不再使用,建议调用disclaim回收其分配的内存。
参考:
http://publib.boulder.ibm.com/infocenter/aix/v6r1/index.jsp
性能管理和调整 > 性能管理 > 性能规划和实现 > 有效的程序设计和实现 > 内存限制程序:
要将数据工作集减至最小,请尝试集中常用数据以及避免对虚拟存储器页面不必要的引用。
特别是:
i. 用 malloc() 或 calloc() 子例程来仅请求实际需要的空间大小。当实际情况只使用数组的一小部分时,切勿请求然后初始化最大的数组。当您得到一个新页面用来初始化数组元素时,您实际上是强制 VMM 从别处窃取一个实内存页面。随后, 当拥有该页面的进程尝试再次访问它时,会造成缺页故障。malloc() 和 calloc() 子例程的差异不仅仅在接口上。
ii. 因为 calloc() 子例程将分配的存储器置零, 它与每一个分配的页面相关, 而 malloc() 子例程只与第一个页面相关。如果您用 calloc() 子例程分配一大块区域, 然后最初只使用一小部分,那么您对系统施加了不必要的负载。不仅这些页面必须初始化;而且如果它们的实内存页面被系统回收(因为系统调页), 那么已初始化但从未使用的页面必须写出到调页空间。这种情况浪费 I/O 和调页空间。
iii. 大结构(如缓冲区)的链表可以引起类似的问题。如果您的程序执行大量寻找某个特定关键字的链式跟踪,请考虑保持链接指针和关键字与数据分离或使用散列表方法来代替。
iv. 引用局部性也意味着时间上的局部性, 而不仅仅是地址空间上。仅在使用之前初始化数据结构(如果使用的话)。在重负载系统中,在初始化和使用之间长时间驻留的数据结构有帧被窃取(因为系统调页)的危险。然后您的程序就会在开始使用数据结构时发生不必要的缺页故障。
v. 同样,如果早先使用一个大结构,然后与程序剩余部分无关联,它应该被释放。使用 free()子例程来释放由 malloc() 或 calloc() 子例程分配的空间是不够的。free() 子例程仅仅释放结构占用的地址范围。要释放实内存和调页空间,也可使用 disclaim() 子例程来放弃空间。对disclaim() 的调用应该在调用 free() 之前进行。
2. 内存监控
2.1 观察系统中内存占用最高的进程
后台运行 3 个 nmem64 进程:
./nmem64 -m 2048 -s 3000 -z 80 &
按进程使用的虚拟内存进行排序,显示占用最高的前三项:
显示虚拟内存占用最高的 3 个进程的详细的内存段分布信息,如下:
可以看到,消耗最多虚拟内存的段都是 nmem64 进程的数据段。
2.2 寻找内存持续增长的进程
可以使用 ps vg 记录当前系统中各进程的内存消耗情况,然后通过比较多次 ps vg 的结果来判断是否存在一些进程有持续的内存增长。
说明:
进程存在持续内存增长并不一定意味着出现了内存泄漏。由于 AIX 内存分配采用了访问时分配的策略, 进程申请大量内存时系统并不会第一时间分配内存, 而是在进程使用过程中实际访问时才进行分配。由于这种分配策略, 进程在启动初期可能存在内存持续增长的可能 (例如数据库缓存需要一定时间才能完全填充) ;但其增长曲线应该是收敛到具体值的。
测试脚本如下:
2.3 如何通过共享内存 ID 对应关联到该共享内存的进程
在 AIX 系统层面,只要给定共享内存 id,就可以获取 attach 该共享内存的进程列表,方法如下:
可以根据其共享段 SID,获得相应的关联进程 pid,如下。注意 ipcs 上看 NATTACH=53,即有 53 个进程 attach 到该共享内存,因此 svmon 结果中,进程列表部分对应列出了 53 个进程 pid。
根据相应的 pid 可以获取进程的具体信息:
2.4 如何获取 AIX Kernel 的内存使用率
kernel大部分内存占用采用跟普通进程一样的内存段方式组织, 比如kernel heap(这部分内存消耗包含文件系统的元数据、内核扩展、第三方驱动等等), 网络buffer,磁盘管理LVM buffer占用, 以及一些RAS特性的buffer占用, 例如light-weight memory trace buffers, component tracebuffers等等。这部分都可以通过svmon -Ss 查询出( 注意这条命令阻塞时间较长)。
少部分采用非段方式组织的主要是AIX内存管理的元数据如页表之类。
可以通过 perfpmr.sh - - x memdetails.sh 获取kernel内存占用的整体情况,内存分布将输出在perfpmr.int中。
也可以通过如下方法直接观察 AIX Kernel 内存使用情况:
1. 观察AIX内存使用的命令(基于kdb,建议仅在测试环境验证)
2. 建议
从我们目前实验的几个系统看,超过100G的大分区,一般内核的内存占用实际都在 5%以下。如果客户环境中采用了较多第三方驱动或组件,比例可能偏高一些,建议搭建环境验证一下。
此外, 如果系统中存在超大的JFS2文件系统, 包含大量的巨型文件 (比如单个文件数十、 数百GB) ,或者有数以十万、百万计的小文件;则可能观察到较高的JFS2 元数据缓存占用,或者inode缓存占用。这部分内存消耗也会计入AIX kernel,可以通过 cat/proc/sys/fs/jfs2/memory_usage观察到:
# cat /proc/sy s/fs/jfs2/memory_usage
metadata cache: 38928384
inode cache: 307888128
total: 346816512
另,文件系统的元数据缓存和inode缓存可以通过如下ioo参数控制
这两个参数是比例系数,设置为400时,元数据和inode缓存最多能占据系统内存的16%左右。
一般而言,如果分区内存不大或者元数据相关操作不多,使用默认值(AIX7.1为200)通常是可行的。但如果分区内存足够大(比如100GB以上),元数据操作较多,则可以考虑将这两个参数设置为100,可以使得元数据和inode缓存最多占用系统内存的比例下调至4%左右。
2.5 如何判断系统是否存在内存不足
预期的内存需求是 virtual 页面数加上文件页面数(包括 pers 和 clnt) ,如果这两者之和大于实际配置的内存页面数,即可认为存在内存不足,如下示例:
AIX 6.1 TL2版本之后,加入了一个新的available列:
The "available" means number of bytes of memory that can be used without paging working storage.
available计算大致的公式是:available = free + numperm - min(numperm,minperm)- minfree
注:numperm即文件缓存所占内存;minperm为文件系统内存需要保障的比例(minperm%)换算得到的内存需求;minfree是系统内存池需要保持的空闲容量(vmo参数);available 如果不够了,就会开始 paging 了, 这是目前最直接的判断方法。
3.4 应用开发工具
3.1 dbx 使用以及 coredump 定位
Core dump 定位参考:
http://www.ibm.com/developerworks/cn/aix/library/0806_chench_core/dbx 使用参考“General Programming Concepts” “调试程序”部分。下载地址:
http://pic.dhe.ibm.com/infocenter/aix/v6r1/topic/com.ibm.aix.genprogc/doc/genprogc/genprogc_pdf.pdf
3.2 内存非法使用检查
大部分的 coredump 都与内存非法使用有关。IBM Rational PurifyPlus 是解决这类问题的最佳工具。
使用 Purify 插桩编译后再进行功能测试, 如果测试中发现内存非法使用之类 (包括内存泄漏)的问题, Purify 可以提供 GUI 界面直接关联到出现问题的应用源代码, 这样能够很方便的定位内存使用类的问题。
此外,Quantify 组件可以用于分析程序的性能问题。这些工具功能都非常完备。
AIX 下可免费使用的非法内存使用检查工具是 Malloc Debug Tool,设置好 MALLOCTYPE 和 MALLOCDEBUG 选项后, 启动程序即可打开 malloc 调试功能。如下是常用的一组选项:
MALLOCTYPE=debug
MALLOCDEBUG="log,postfree_checking,validate_ptrs,catch_overflow,allow_overread,report_allocations,continue,output:/tmp/malloclog.txt"
如果允许越界读(一般情况下不会造成严重错误) ,可以在 MALLOCDEBUG 中加入allow_overread。
打开 MALLOCDEBUG 后默认的行为是发现内存使用错误后调用 abort 终止程序,这样可以在第一时间 core dump 并退出,方便定位问题。但如果不希望终止可以使用 continue 参数,这样对于可以恢复的错误,将仅生成 core,进程会继续执行。
或:
MALLOCDEBUG="report_allocations,log:extended,stack_depth:8,catch_overflow,allow_overread,postfree_checking,validate_ptrs,continue,output:/tmp/malloclog.txt"
具体可以参考:
http://pic.dhe.ibm.com/infocenter/aix/v7r1/topic/com.ibm.aix.genprogc/doc/genprogc/debug_malloc.htm
注意:
使用 MALLOCDEBUG 选项之后,为调试之目的,系统的内存使用可能会成倍增加,所以需要事先预留好系统的可用内存。
3.3 内存泄漏检查(report_allocations)
MALLOCTYPE/MALLOCDEBUG 环境变量选项也可以用于内存泄露检查,如:
MALLOCTYPE=debug
MALLOCDEBUG="report_allocations,output:/tmp/malloclog.txt"
更详细的记录:
MALLOCTYPE=debug
MALLOCDEBUG="report_allocations,log:extended,stack_depth:6,output:/tmp/malloclog.txt"
原理:
report_allocations 选项可以用于帮助发现内存泄露问题, report_allocations 选项采用数据库记录所有分配情况:当成功分配内存时,采用数据库记录分配情况;而在该内存被释放时,则从数据库从删除分配记录;
当进程退出时,系统将所有未释放内存的详细信息记录到日志中。
注意事项:
1. 从上面的原理介绍可以看到, 程序必须退出之后才能确切知道那些内存没有对应的释放操作。因此如果是长运行的程序,必须注册一个退出函数,常见的方法是注册一个信号处理函数,在收到 SIGTERM 或 SIGINT 时调用 exit(0) ,参考如下示例 (sig_handle 定义和注册过程代码已用红色标注)。
2. 上述 debug 环境变量应该设置为仅仅对目标测试程序生效,不能直接在用户环境中export,否则将造成大量无关程序的分配信息被记录到日志中,对定位问题造成干扰。
3. 激活 Malloc log 之后,对于 32 位程序,每次内存分配大约有 50-100 字节的额外开销;对 64 位程序,每次内存分配有 100-200 字节的额外开销。因此,激活调试选项的应用的内存使用量会有一定的增长,在测试时需要预先准备足够的系统内存。
4. 示例如下:
参考文章:
http://www.ibm.com/developerworks/cn/aix/library/aumallocdebug.html
3.4 内存泄漏检查示例
示例 1(基本输出) :
示例 2(扩展输出) :
3.5 proctools 介绍
AIX 提供了一系列基于/proc 文件系统的 proctools 工具集用于分析应用问题,例如 procstack用于打印进程当前的栈信息;procldd 显示进程加载的共享库信息;procfiles 显示进程打开的文件信息;procwdx 显示进程当前的工作目录;procsig 显示进程的信号处理规则(如SIGHUP,SIGTERM,SIGQUIT,SIGINT,SIGSEGV 等等) ;proccred 显示进程的 effective、real、saved user ID 和 effective、real、saved group ID;
参考:
http://publib.boulder.ibm.com/infocenter/pseries/v5r3/topic/com.ibm.aix.prftools/doc/prftools/prftools09.htm#wq513
3.6 probevue 介绍
AIX6.1 开始,AIX 提供了 probevue 功能,可以用于动态跟踪和调试。Probevue 的语法类似C 语言,参考:
http://www.ibm.com/developerworks/wikis/display/WikiPtype/Probevue
此外,AIX7.1 TL2 and AIX6.1 TL8 提供了新的 probevue 特性,即关联数组"assocciate array".可以基于这个特性实现类似 truss –c 的功能。例如如下 aso.sh 示例,可以对进程或系统范围内的调用进行统计。
3.7 如何将 C 文件与汇编文件对应
优化编译可能会根据语义调整代码位置,比如将循环不变量外提,赋值传递等等,这样调试的难度会增加。调试-O2 -g程序时, 打印的行信息可能并不准确。这种情况有时需要结合汇编来分析,参考下面的例子:
1. 使用 "-qsource -qlist"编译选项, 使用该选项后,针对每个源码文件,编译器都会生成一个对应的".lst"文件,该文件提供了汇编和C/C++语句的对应关系。
2. 每个".lst"文件都有两个部分,
a. 一部分是源码与行号:
8 | j = i / 7;
b. 另一部分是相应源码行号对应的汇编码;
注意一些语句可能没有直接对应的汇编定义, 比如变量定义, 或者一些已经被优化掉的逻辑之类。
参考如下示例:
参考文档:
https://www.ibm.com/developerworks/aix/library/au-memoryallocation/index.html
原题:AIX 进程内存分配与回收策略及应用开发建议 点击阅读原文,可下载原文PDF文档,或在原文下提问交流 觉得本文有用,请转发、点赞或点击“在看”,让更多同行看到
资料/文章推荐:
欢迎关注社区 "AIX"技术主题 ,将会不断更新优质资料、文章。地址:
https://www.talkwithtrend.com/Topic/117
下载 twt 社区客户端 APP
长按识别二维码即可下载
或到应用商店搜索“twt”
长按二维码关注公众号
*本公众号所发布内容仅代表作者观点,不代表社区立场