Nginx和TCMalloc(google perfect tool)

    近段时间在琢磨业务服务器上面的Nginx的事情,每天的访问量接近2亿了,虽然业务很简单,不过感觉量还是很吓人,略担心Nginx的状况,在网上转了一圈,看到有用TCMalloc来优化Nginx的内存管理,提高并发能力,所以研究下这个好东西。
    TCMalloc(Thread-Caching Malloc)是google开发的开源工具──“google-perftools”中的成员。与标准的glibc库的malloc相比,TCMalloc在内存的分配上效率和速度要高得多。

--------------------------------------------------华丽的分割线(没兴趣的请往下跳,直接看安装和性能测试)----------------------------------------------

    TCMalloc原理

    James Golick,原文地址:http://jamesgolick.com/2013/5/19/how-tcmalloc-works.html

    tcmalloc是一款专为高并发而优化的内存分配器。tcmalloc的tc含义是thread cache,tcmalloc正是通过thread cache这种机制实现了大多数情 况下的无锁内存分配。

    tcmalloc与大多数现代分配器一样,使用的是基于页的内存分配,也就是说,这种内存分配的内部度量单位是页,而不是字节。这种内存分配可以有效地减少内存碎片,同时,也可以增加局部性。此外,也可以使得元数据的跟踪更为简单。tcmalloc定义一页为8K字节,在大多数的linux系统中,一页是4K字节,也就是tcmalloc的一页是linux的两页。tcmalloc中的内存分配块整体来说分为两类,“小块”和“大块”,“小块”是小于kMaxPages的内存块,“小块”可以进一步分为size classes,而且“小块”的内存分配是通过thread cache或者central per-size class cache而实现。“大块”是大于等于kMaxPages的内存块,“大块”的内存分配是通过central PageHeap实现。

    Size Class

    一般来说,tcmalloc为“小块”创建了86个size classes,每一个class都会定义thread cache的特性,碎片化以及waste特征。对于一个特定的size class,一次性分配的页数就是一个thread cache特性。tcmalloc细致地定义了这个页数,使得central cache和thread cache之间的转换能够保持以下两者的平衡:thread cache周边的wasting chunk,以及访问central cache太过于频繁而导致的锁竞争。定义这个页数的程序代码也保证了每一种class的waste比例最多为12.5%,当然,malloc API需要保证内存对齐。size class data存于SizeMap中,并且是启动阶段第一个初始化的对象。

    Thread Caches

    thread cache是一种惰性初始化的thread-local数据结构,每个size class包含一个free list(单向),此外,他们包含了表示他们容量的总大小的元数据。在最佳的情况下,从thread cache分配内存和释放内存是无锁的,并且时间复杂度是线性的。如果当前thread cache不包含需要分配的内存块时,thread cache从central cache获取那种class的内存块。如果thread cache的空间太多,thread cache的内存块会返还给central cache,每一个central cache都有一个锁,这个锁用来减少内存返还时的竞争。虽然thread cache会有内存块的迁移,但是thread cache的大小会根据两个有趣的方式限定在一个范围内。

    其一,所有的thread cache的总大小会有一个上限。每一个thread cache都会在内存块的迁移,分配和释放时跟踪它当前的内存块总容量。起初,每一个thread cache都会被分配相同的内存空间。但是,当thread cache的容量动态变化时,会有一个算法使得一个thread cache可以从其他的thread cache偷取没有使用的空间。

    其二,每一个free list都有一个上限,这个上限会随着内存块从central cache迁入时以一种有趣的方式增加,如果list超过了上限,内存块会释放给central cache。当内存的释放或者有central cache的内存块的迁入而导致thread cache超过了上限,thread cache首先会试图查找free list中特别的headroom,以检查thread cache是否有多余的需要释放给central cache的内存。当free list满足了条件时,被加进free list的内存块就是多余的 。如果还是没有空出足够的空间时,thread cache会从其他的thread cache中偷取空间,当然需要拥有pageheap_lock。central cache有其自己的系统,用来管理tcmalloc整个系统中所有的cache。每一个要么是1M字节的内存块或者是1个入口(大于1M字节)。当central cache需要更多的空间时,他们可以使用thread cache类似的方式从其他的central cache偷取内存空间。当thread cache需要返还内存块给central cache,而central cache又已经满了无法获取更多的空间时,central cache会释放这些内存对象给PageHeap,也就是起初central cache获取他们的地方。
    Page Heap

    PageHeap算是整个系统的根本,当内存块没有作为cache,或者没有被应用程序申请时,他们位于PageHeap的free list中,也就是他们起初被TCMalloc_SystemAlloc分配的位置,最终又会被TCMalloc_SystemRelease释放给操作系统。当“大块”内存被申请时,PageHeap也提供了接口跟踪heap元数据。PageHeap管理Span对象,Span对象表示连续的页面。每一个Span有几个重要的特性。

    一,PageID start是由Span描述的内存起始地址,PageID的类型是uintptr_t。

    二,Length length是Span页面的数量,Length的类型也是uintptr_t。

    三,Span *next和Span *prev是Span的指针,当Span位于PageHeap的free list双向链表中。

    PageHeap有kMaxPages + 1的free list,span length从0到kMaxPages一一对应一个free list,大于kMaxPages的有一个free list,这些list都是双向的,并且分为了normal和returned两个部分。

    一,normal部分包含这样的Span,他们的页面明确地映射到进程地址空间。

    二,returned部分包含这样的Span,他们的页面通过调用含有MADV_FREE参数的madvise返还给操作系统。操作系统在必要的时候可以回收这些页面,但是当应用程序在操作系统回收前使用了这些页面,madvise的调用实际上是无效的。甚至当内存已经被回收,内核会重新把这些地址映射到一块全零的内存。因此,重新利用returned的页面不仅是安全的,而且还是减少heap碎片化的一种重要的策略。
    PageHeap包含了PageMap,PageMap是一个radix tree数据结构,会映射到他们对应的Span对象。PageHeap也包含PageMapCache,PageMapCache会映射内存块的PageID到他们在cache中的内存块对应的size class。这是tcmalloc存储元数据的机制,而不是使用headers和footers对应实际的指针。尽管这样有些浪费空间,但是这样在实质上可以更有效地缓存,因为所有相关的数据结构都被“slab”式地分配了。

    PageHeap通过调用PageHeap::New(Length n)分配内存,其中n是需要分配的页面数。

    一,大于等于n的free list(除非n大于等kMaxPages)会被遍历一遍,查找是否有足够大的Span。如果找到了这样的Span,这个Span会从list移除,然后返回这个Span,这种分配是最合适的,但是因为地址不是有序的,因此从内存碎片化的角度来说是次优的,大概算是一种性能上的折中。normallist会在继续检查returned list前全被检查一遍。但是我也不知道为什么。
    二,如果步骤一没有找到合适的Span,算法将会遍历“大块”list,并且查找最合适的地址有序的Span。这个算法的时间复杂度是O(n),它不仅会遍历所有的“大块”list,在并发大幅波动的情况下,这可能会非常耗时,而且还会遍历有碎片的heap。我针对这种情况写过一个补丁,当“大块”list超过了一个可配置的总大小时,通过重组“大块”list为一个skip list来提高应用程序的大内存分配的性能。
    三,如果找到的Span比需要分配的内存大至少一个页面尺寸时,这个Span会被切分为适合内存分配的尺寸,在返回分配内存块之前,剩下的内存会添加到合适的free list中。

    四,如果没有找到合适的Span,PageHeap会在重复这些步骤前尝试增长至少n个页面。如果第二次查找还是没有找到合适的内存块,这个内存分配最终会返回ENOMEM。

    内存释放是通过PageHeap::Delete(Span *span)实现,该方法的作用是把Span合并到合适的free list中。

    一,从PageMap查找该Span的相邻Span对象(左和右),如果在一边或者两边找到了free的内存,他们会从free list中去除,并且合并到Span中。

    二,预先要知道span现在属于哪个free list。

    三,PageHeap会在检查是否需要释放内存给操作系统,如果确实需要,则释放。

    每次Span返还给PageHeap,Span的成员scavenge_counter_会减少Span的length,如果scavenge_counter_降到0,则从free list或者“大块”list释放的Span会从normal list中去除,并添加到合适的returned list中等待回收。scavenge_counter_被重置为:

min(kMaxReleaseDelay, (1000.0 / FLAGS_tcmalloc_release_rate) * number_of_pages_released)。

因此,调整FLAGS_tcmalloc_release_rate在内存释放时非常有用。

-----------------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------------

    开始安装~\(≧▽≦)/~
    一.检查操作系统,如果是64位操作系统,那么就需要先安装libunwind库,32位操作系统不要安装。libunwind库为基于64位CPU和操作系统的程序提供了基本的堆栈辗转开解功能,其中包括用于输出堆栈跟踪的API、用于以编程方式辗转开解堆栈的API以及支持C++异常处理机制的API。
    附上目前最新的包Nginx和TCMalloc(google perfect tool)libunwind-1.1.tar.gz.rar,老规矩,解压完把后缀名改成tar.gz就是Linux下的.gz包
    解压以后进入目录(tar zxvf xxxxxxx.gz)
    CFLAGS=-fPIC ./configure
   make CFLAGS=-fPIC
    make CFLAGS=-fPIC install
    PS:没有深究CFLAGS=-fPIC的用意,有兴趣的朋友可以告诉我有啥用╮(╯_╰)╭

    二.    安装google-perftools
    附上目前最新的包Nginx和TCMalloc(google perfect tool)gperftools-2.1.tar.gz.rar,老规矩,解压完把后缀名改成tar.gz就是Linux下的.gz包
   PS:32位系统不需要安装libunwind,但是在安装时记得加上–enable-frame-pointers
    解压以后进入目录(tar zxvf xxxxxxx.gz)
    ./configure
    make
    make install

    三.把TCMalloc库添加到Linux系统中:
    echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf
    /sbin/ldconfig

    四.重新编译Nginx
    在./configure的时候添加--with-google_perftools_module,
    顺便可以加上--with-cc-opt='-O3',Nginx默认使用的GCC编译参数是-O,使用O2以上的参数可以微量增加1%左右性能,看个人喜好用O3还是O2,两者差不多。
    还有一个可以用的参数--with-cpu-opt=X
    如果是Intel的Xeon,那么X就写pentium,AMD的就可以写operon,这个参数可以针对特定的CPU进行GCC的优化,不过我在用的时候出现了错误:./configure:error:can not detect int size,把这个选项去掉就好了,不过网上也有人说清空export LIBS= 和export CFLAGS=,没试过这样行不行╮(╯_╰)╭ 懒

    五.为线程添加目录
    mkdir /tmp  (根目录下)
    chmod 0777 /tmp

    六.修改Nginx配置文件
    在worker_rlimit_nofile下面添加google_perftools_profiles /tmp/tcmalloc;(记得分号写上)
    没有worker_rlimit_nofile?(╮(╯_╰)╭那就写在worker_process和events之间吧)

    七.检查是否生效
    启动Nginx,然后lsof -n | grep TCMalloc,如果生效了的话,每一个worker都会有一条记录,如:nginx 4452 www 12w REG 104,17 0 14418049 /tmp/tcmalloc.4452
    在/tmp目录下也会有tcmalloc.4452文件

---------------------------------------------------------------------------华丽的分割线------------------------------------------------------------------------------

    性能测试,懒得做了,直接贴前人的成果吧........
    转自http://www.cnblogs.com/lovemdx/p/3199886.html
    精简一下.....
    

    ptmalloc 是glibc的内存分配管理

    tcmalloc 是google的内存分配管理模块

    jemalloc 是BSD的提供的内存分配管理

    三者的性能对比参考从网上的一个图如下:
    Nginx和TCMalloc(google perfect tool)

    自己测试了一下:

    代码省略..........

    都是执行一个map insert 100W次操作。

    为了测试方便,我们生成了3个binary,分别链接使用jemalloc和tcmalloc 和ptmalloc的库做对比:

    然后分别执行各程序,使用time统计时间如下:

    time./jemalloc_test 
    Hello world

    real    0m9.927s 
    user   0m9.650s 
    sys     0m0.250s

    time ./tcmalloc_test 
    Hello world

    real    0m9.836s 
    user   0m9.410s 
    sys     0m0.410s

    time ./ptmalloc_test 
    Hello world

    real    0m11.890s 
    user   0m11.520s 
    sys     0m0.360s

    jemalloc和tcmalloc的性能不分伯仲,而ptmalloc则要低一些。

---------------------------------------------------------------------------华丽的分割线------------------------------------------------------------------------------

    在Nginx上面使用了TCMalloc之后,目前没出现什么问题,至于负载,本来就没有到高负载的程度,所以也看不出来什么太明显的提升,总之就算提升不明显,那也是提升,Over~

上一篇:Tomcat 使用apr优化


下一篇:开发人员必知的Git技能及Git工作流总结!(三)