这个问题去年线上系统发生的一个严重的性能问题,导致了大量用户的访问超时。今天花了点时间又看了下,linux内核线性地址空间相关的知识,再复盘下这个问题,温故而知新。

现像

业务做活动期间,系统访问请求很大时,系统load非常高,其中几台机器的load到达70。

磁盘没有成为瓶颈,而分析CPU load时,发现find_vma()这个函数占用了很大的CPU性能。

现场

能过分析一台机器的Trace日志,发现主要时间花在一台机器向另一台机器请求数据。此时磁盘,网络流量都没有到达瓶颈,而被请求的机器,load也不高。

使用perf top分析,发现find_vma()占用了多达23.56%的性能,高峰时这个值更高。

不能看到整个函数调用栈,只能到+GImmap64,建议以后–with-release加上-fomit-frame-pointer这个参数,不然无法查看整个函数调用堆栈http://www.trueeyu.com/?p=1694,会损失大约3%的性能。

cat /proc/xxx/maps

总共9053个mmap区域,大量mmap区域无法合并。

mmap大小分布(平时的值,活动期间应该更高)

2885个 516K
783个 1032K
589个 1548K
558个 132K

512K的占比很高。

原理分析

内核通过find_vma()分配一个可用的线性区域。

64位x86_64的地址空间

vm area的管理结构

mmap系统调用流程

1
2
3
4
5
sys_mmap(addr, len, prot, flags, fd, offset)
|–>sys_mmap_pgoff(add, len, prot, flags, fd, offset >>PAGE_SHIFT)
|–>do_mmap_pgoff()
|–>get_unmapped_area()
|–>arch_get_numapped_area_topdown() //找到一个可用的hole来建立vm

进程分配可用的线性地址

查找的过程实际上是查找两个vm_area_struct之间的hole,如果这个hole满足申请内存大小的要求,则查找成功。

查找时从mmap_base(高地址)向低地址进行查找。

如果mmap没有指定addr的话,实际的查找流程是从mmap_base依次查找每一个空洞是否满足条件(根据len和find_vma()会跳过一些空洞)。这种算法相对于从mmap_base查单链表会好很多?(怀疑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* check if free_area_cache is useful for us */
if (len <= mm->cached_hole_size) { //如果要申请的内存的长度<=上次查找时的空洞大小,重要从mmap开始位置搜索,防止高地址小的空洞没有很好的利用
mm->cached_hole_size = 0;
mm->free_area_cache = mm->mmap_base;
}
…..
addr = mm->mmap_base-len; //开始查找的地址设置为mmap开始地址-要申请的空间大小

do {
/*
* Lookup failure means no vma is above this address,
* else if new region fits below vma->vm_start,
* return with success:
*/

vma = find_vma(mm, addr); //查红黑树找到第一个vm_end > addr的vma
if (!vma || addr+len <= vma->vm_start) //如果没有找到,或是addr+len <= vma->vm_start,证明是找到了第一个满足申请内存条件的空洞
/* remember the address as a hint for next time */
return (mm->free_area_cache = addr); //记录上次查找到的位置

/* remember the largest hole we saw so far */
if (addr + mm->cached_hole_size < vma->vm_start)
mm->cached_hole_size = vma->vm_start – addr; //记录上次查找到的空洞大小

/* try just below the current vma->vm_start */
addr = vma->vm_start-len; //设置addr,重新查找(会跳过一些空洞)
} while (len < vma->vm_start);

如果按照这个流程分析的话,如果从mmap_base开始的大多数空洞都很小,无法容纳要申请的内存的话,会造成大量调用 find_vma(),直到找到为止。

验证

2.9中可以看出,mmap数很多,只能说明红黑树比较高,是性能比较差的一个原因,所以通过减小cache(从而减少线性区域)来解决,有一定效果。

实际上忽略了find_vma()调用次数过多这个现像。

cat /proc/xx/maps,计算每个hole大小,从mmap_base最大的地址查找,需要查找4000+次,才能找到需要的空洞(粗略的计算),这就需要调用4000次vma,这还是在平时load低的时候的一个计算。下图是开始一段空洞大小(大量小的空洞)

分析

  • 向ups请求数据时间较长
  • find_vma()耗CPU较多
  • ups load不高
  • 每日合并完成那段时间,find_vma()耗CPU较少
  • 网络重传率只有0.2%
  • 而pmap中最多的是512K的内存

估计应该是大量申请释放512K内存的问题

原来是一台机器向另一台机器请求数据,有一个数据结构没有reuse内存,而是会频繁申请释放内存,而每次申请内存(512K),都要创建一个线性区域,而这台机器本身还有一些更小的内存区域碎片,所以会造成多次查找红黑树,从而导致find_vma()的性能问题。

最根本的原因是系统申请内存的机制有问题,先是申请了大量小块内存不释放,然后是大量频繁申请释放512K的内存,导致不能迅速找到满足条件的hole.

修复办法

  • 重新编译一个版本上去,查看完整的调用堆栈。
  • 去除这个数据结构的频繁申请释放内存。
  • 最好的方法是修复系统的内存分配方法,以固定大小申请释放。

问题

内核针对小len的申请,查找单链表,比find_vma()查找红黑树会不会更好一些?实际上arch_get_numapped_area()是查找的单链表。为什么不设计成类似于伙伴算法这样的算法?

参考资料

Comments

2015-04-15