一 问题背景
在一次广告检索内核的迁移过程中,偶然发现迁移新内核后的检索服务在一定条件下会触发rt毛刺问题。rt毛刺现象出现的条件如下,任何一条均可复现:
- 使用超高压力将服务压至崩溃状态,然后将压力恢复正常压力状态,毛刺现象复现
- 使用正常压力长时间对服务加压,持续30分钟左右之后,毛刺现象复现
通过性能埋点监控发现,在rt毛刺点,rt的增长主要集中在多线程并行使用内存池申请内存的代码部分;通过perf性能热点追踪工具发现,每当rt毛刺出现时,热点列表里都会多出pageblock_pfn_to_page这个热点项。pageblock_pfn_to_page热点项自身热点占比会不断增长,最终会占据8%的cpu时间,并且一直持续。
二 问题分析
2.1 pageblock_pfn_to_page引论(本小节90%内容引自文章https://www.cnblogs.com/tolimit/p/5286663.html)
关于pageblock_pfn_to_page,网上找到一篇linux内核源码分析文章https://www.cnblogs.com/tolimit/p/5286663.html,写得非常好,建议详读。简单来说pageblock_pfn_to_page是linux在做内存压缩。
我们知道内存是以页框为单位,每个页框大小默认是4K(大页除外),而在系统运行时间长后就会出现内存碎片,内存碎片的意思就是一段空闲页框中,会有零散的一些正在使用的页框,导致此段页框被这些正在使用的零散页框分为一小段一小段连续页框,这样当需要大段连续页框时就没办法分配了,这些空闲页框就成了一些碎片,不能合并起来作为一段大的空闲页框使用,如下图:
白色的为空闲页框,而有斜线的为已经在使用的页框,在这个图中,空闲页框都是零散的,它们没办法组成一块连续的空闲页框,它们只能单个单个进行分配,当内核需要分配连续页框时则没办法从这里分配。为了解决这个问题,内核实现了内存压缩功能,其原理很简单,就是从这块内存区段的前面扫描可移动的页框,从内存区段后面向前扫描空闲的页框,两边扫描结束后,将可移动的页框放入到空闲页框中,最后最理想的结果就如下图:
这样移动之后就把前面的页框整理为了一大段连续的物理页框了,当然这只是理想情况,因为并不是所有页框都可以进行移动,像内核使用的页框大部分都不能够移动,而用户进程的页框大部分是可以移动了。
内存压缩发生时机:
现在再来说说什么时候会进行内存压缩。它会在四个地方调用到:
- 内核从伙伴系统以min阀值获取连续页框,但是连续页框又不足时。
- 当需要从指定地方获取连续页框,但是中间有页框正在使用时。
- 因为内存短缺导致kswapd被唤醒时,在进行内存回收之后会进行内存压缩。
- 将1写入sysfs中的/vm/compact_memory时,系统会对所有zone进行内存压缩。
内存压缩是一个相当耗费资源的事情,它并不会经常会执行,即使因为内存短缺导致代码中经常调用到内存压缩函数,它也会根据调用次数选择性地忽略一些执行请求,见内存压缩推迟。
系统判定是否执行内存压缩的标准是: - 在分配页框过程中,zone显示是有足够的空闲页框供于本次分配的,但是伙伴系统链表中又没有连续页框段用于本次分配。原因就是过多分散的空闲页框,它们没办法组成一块连续页框存放在伙伴系统的链表中。
- 在kswapd唤醒后会对zone的页框阀值进行检查,如果可用页框少于高阀值则会进行内存回收,每次进行内存回收之后会进行内存压缩。
即使满足标准,也不一定会执行内存压缩,具体见后面的内存压缩推迟和compact_zone()函数。
通过上面的线索,结合rt毛刺点的rt增长主要集中在内存池申请内存环节的现象,有充分的理由怀疑,是由于内存池的内存管理策略导致内存页面出现大量不连续空洞,从而引起了内存压缩的性能热点。
2.2 内存池内存管理策略
内存池中,为了保证内存申请/释放的效率,降低向系统申请/释放内存的频率,引入了freelist机制。简单来说,就是当一块内存被释放时,不是直接通过free还给linux内核,而是先将这块内存缓存在单向链表freelist中,以便后续使用;在申请内存时,内存池先检查freelist是否有符合要求的内存块,若有,则直接分配,若没有,则向linux内核申请;分配后的内存将被记录在“已分配内存块”链表page in use中。
但是为什么这样的策略会导致内存压缩的热点呢?这里其实和几个实现细节有关。
- 在检查freelist是否有符合要求的内存块时,内存时使用的方式是:只检查freelist链表中的额第一个内存块是否符合要求,如过不满足,则向linux内核申请内存
- 当对内存池进行reset时(即释放所有page in use中内存块),采用的策略是,freelist头部不变,把page in use链表接到freelist尾部
- 为了防止freelist不断增长带来的内存泄漏,每次reset的时候会对freelist进行截断,若freelist中的内存块size之和大于S,则将freelist尾部的内存块free掉,只保留头部的内存块
如图所示,在上述策略下会导致一个问题,那就是服务进程对freelist中各个内存块的持有时间参差不齐。在高并发场景下,时间上的差别导致物理内存空间上的距离,会导致物理内存页占用不连续,导致内存压缩的热点。特别的,当服务申请的内存块大概率大于第一个内存块大小时,freelist中缓存的内存块总是无法使用。
2.3 内存策略优化
解法一:内存分配时遍历freelist寻找合适内存块
该解法会导致内存分配性能下降
解法二:上调单次分配内存块的最小值(上调每个内存块最小值)
该解法会造成内存池内存使用率降低
解法三:freelist头部内存块指针动态变化
该方案是兼顾内存分配效率和内存使用率的中间方案,每次reset时,将freelist链表接到page in use链表的尾部,然后将freelist链表入口指针重新赋值为page in use的头部指针。这样一来:
- freelist头部内存块size会随着分配申请的size而动态变化,降低了freelist不可用的概率
- 在进程启动后,经过少量内存压缩操作后,freelist的成员内存块会趋于稳定,不再变化,减少了物理内存页空洞
本次优化采用了解法三,最终解决了pageblock_pfn_to_page热点问题
三 结论与经验
在制定内存池内存管理策略时,除了要考虑内存分配效率、内存利用率等问题之外,还要结合业务场景考虑“内存持有时间差异”。设计中,应尽可能避免“长时间持有”和“短时间持有”的内存块共存的情况。
补充:在后续优化的过程总发现,linux内核为了方便用户进程使用hugepages大内存页功能(功能介绍见https://yuque.antfin-inc.com/zaifeng.xy/fbo7fz/kxdizy),默认开启了transparent_hugepages功能,会在用户进程不感知的情况下使用大内存页,旨在提升访存效率。但是,由于在开启transparent_hugepages时,linux内核会维护一块很大的连续内存用作内存池,当用户进程内存使用量大且会造成占用内存不连续时(如自己维护了一个内存池),会导致linux内核不断尝试对内存进行序列化压缩,严重时会导致进程出现长时间的性能毛刺。解决方法为关闭transparent_hugepages功能,方法如下:
sudo bash -c “echo never >/sys/kernel/mm/transparent_hugepage/enabled”
sudo bash -c “echo never >/sys/kernel/mm/transparent_hugepage/defrag”
如果感兴趣,欢迎关注微信技术公众号