0 - 概述
系統(tǒng)的物理內(nèi)存是有限的,而對(duì)內(nèi)存的需求是變化的, 程序的動(dòng)態(tài)性越強(qiáng),內(nèi)存管理就越重要袜蚕,選擇合適的內(nèi)存管理算法會(huì)帶來明顯的性能提升。
MySQL作為常用的數(shù)據(jù)庫(kù)赎线,會(huì)有大量的內(nèi)存操作廷没。每次處理一個(gè)請(qǐng)求的時(shí)候,會(huì)在內(nèi)存中操作數(shù)據(jù)垂寥,不斷的進(jìn)行malloc和free操作。因此另锋,malloc的性能越好滞项,MySQL的處理速度越快。
內(nèi)存管理可以分為三個(gè)層次夭坪,自底向上分別是:
- 操作系統(tǒng)內(nèi)核的內(nèi)存管理文判;
- glibc層使用系統(tǒng)調(diào)用維護(hù)的內(nèi)存管理算法;
- 應(yīng)用程序從glibc動(dòng)態(tài)分配內(nèi)存后室梅,根據(jù)應(yīng)用程序本身的程序特性進(jìn)行優(yōu)化戏仓;
一個(gè)優(yōu)秀的內(nèi)存分配器應(yīng)具有以下特點(diǎn):
- 額外的空間損耗盡量少疚宇;
- 分配速度盡可能快;
- 盡量避免內(nèi)存碎片赏殃;
- 緩存本地化友好敷待;
- 通用性,兼容性仁热,可移植性榜揖,易調(diào)試;
1- 現(xiàn)狀
目前大部分服務(wù)端程序(包括MySQL)使用glibc提供的malloc/free系列函數(shù)抗蠢,而glibc使用的ptmalloc2在性能上遠(yuǎn)遠(yuǎn)弱后于google的tcmalloc和facebook的jemalloc举哟。 而且后兩者只需要使用LD_PRELOAD環(huán)境變量啟動(dòng)程序即可,甚至并不需要重新編譯迅矛。
1.1 - glibc ptmalloc2
ptmalloc2即是glibc使用的malloc版本妨猩。
上圖是 x86_64 下 Linux 進(jìn)程的默認(rèn)地址空間對(duì) heap 的操作,操作系統(tǒng)提供了brk()系統(tǒng)調(diào)用秽褒,設(shè)置了Heap的上邊界壶硅; 對(duì) mmap 映射區(qū)域的操作,操作系 統(tǒng) 供了 mmap()和 munmap()函數(shù)震嫉。因?yàn)橄到y(tǒng)調(diào)用的代價(jià)很高森瘪,不可能每次申請(qǐng)內(nèi)存都從內(nèi)核分配空間,尤其是對(duì)于小內(nèi)存分配票堵。 而且因?yàn)閙map的區(qū)域容易被munmap釋放扼睬,所以一般大內(nèi)存采用mmap(),小內(nèi)存使用brk()悴势。
多線程支持:
- Ptmalloc2有一個(gè)主分配區(qū)(main arena)窗宇, 有多個(gè)非主分配區(qū)。 非主分配區(qū)只能使用mmap向操作系統(tǒng)批發(fā)申請(qǐng)HEAP_MAX_SIZE(64位系統(tǒng)為64MB)大小的虛擬內(nèi)存特纤。 當(dāng)某個(gè)線程調(diào)用malloc的時(shí)候军俊,會(huì)先查看線程私有變量中是否已經(jīng)存在一個(gè)分配區(qū),如果存在則嘗試加鎖捧存,如果加鎖失敗則遍歷arena鏈表試圖獲取一個(gè)沒加鎖的arena粪躬, 如果依然獲取不到則創(chuàng)建一個(gè)新的非主分配區(qū);
- free()的時(shí)候也要獲取鎖昔穴。分配小塊內(nèi)存容易產(chǎn)生碎片镰官,ptmalloc在整理合并的時(shí)候也要對(duì)arena做加鎖操作。在線程多的時(shí)候吗货,鎖的開銷就會(huì)增大泳唠;
ptmalloc內(nèi)存管理:
用戶請(qǐng)求分配的內(nèi)存在ptmalloc中使用chunk表示, 每個(gè)chunk至少需要8個(gè)字節(jié)額外的開銷宙搬。 用戶free掉的內(nèi)存不會(huì)馬上歸還操作系統(tǒng)笨腥,ptmalloc會(huì)統(tǒng)一管理heap和mmap區(qū)域的空閑chunk拓哺,避免了頻繁的系統(tǒng)調(diào)用;
-
ptmalloc 將相似大小的 chunk 用雙向鏈表鏈接起來, 這樣的一個(gè)鏈表被稱為一個(gè) bin脖母。Ptmalloc 一共 維護(hù)了 128 個(gè) bin,并使用一個(gè)數(shù)組來存儲(chǔ)這些 bin士鸥。數(shù)組中的第一個(gè)為 unsorted bin, 數(shù)組中從 2 開始編號(hào)的前 64 個(gè) bin 稱為 small bins, 同一個(gè)small bin中的chunk具有相同的大小。small bins后面的bin被稱作large bins镶奉;
當(dāng)free一個(gè)chunk并放入bin的時(shí)候础淤, ptmalloc 還會(huì)檢查它前后的 chunk 是否也是空閑的,如果是的話哨苛,ptmalloc會(huì)首先把它們合并為一個(gè)大的 chunk鸽凶,然后將合并后的 chunk 放到 unstored bin 中。 另外ptmalloc 為了提高分配的速度建峭,會(huì)把一些小的(不大于64B) chunk先放到一個(gè)叫做 fast bins 的容器內(nèi)炎疆;
在fast bins和bins都不能滿足需求后裂明,ptmalloc會(huì)設(shè)法在一個(gè)叫做top chunk的空間分配內(nèi)存思杯。 對(duì)于非主分配區(qū)會(huì)預(yù)先通過mmap分配一大塊內(nèi)存作為top chunk寥茫, 當(dāng)bins和fast bins都不能滿足分配需要的時(shí)候, ptmalloc會(huì)設(shè)法在top chunk中分出一塊內(nèi)存給用戶,如果top chunk本身不夠大边锁,分配程序會(huì)重新mmap分配一塊內(nèi)存chunk姑食,并將 top chunk 遷移到新的chunk上,并用單鏈表鏈接起來茅坛。如果free()的chunk恰好 與 top chunk 相鄰音半,那么這兩個(gè) chunk 就會(huì)合并成新的 top chunk,如果top chunk大小大于某個(gè)閾值才還給操作系統(tǒng)贡蓖。主分配區(qū)類似曹鸠,不過通過sbrk()分配和調(diào)整top chunk的大小,只有heap頂部連續(xù)內(nèi)存空閑超過閾值的時(shí)候才能回收內(nèi)存斥铺;
需要分配的 chunk 足夠大彻桃,而且 fast bins 和 bins 都不能滿足要求,甚至 top chunk 本身也不能滿足分配需求時(shí)晾蜘,ptmalloc 會(huì)使用 mmap 來直接使用內(nèi)存映射來將頁(yè)映射到進(jìn)程空間邻眷;
ptmalloc分配流程
ptmalloc的缺陷
- 后分配的內(nèi)存先釋放,因?yàn)?ptmalloc 收縮內(nèi)存是從 top chunk 開始剔交,如果與 top chunk 相鄰的 chunk 不能釋放耗溜,top chunk 以下的 chunk 都無法釋放;
- 多線程鎖開銷大省容, 需要避免多線程頻繁分配釋放;
- 內(nèi)存從thread的areana中分配燎字, 內(nèi)存不能從一個(gè)arena移動(dòng)到另一個(gè)arena腥椒, 就是說如果多線程使用內(nèi)存不均衡阿宅,容易導(dǎo)致內(nèi)存的浪費(fèi)。 比如說線程1使用了300M內(nèi)存笼蛛,完成任務(wù)后glibc沒有釋放給操作系統(tǒng)洒放,線程2開始創(chuàng)建了一個(gè)新的arena, 但是線程1的300M卻不能用了滨砍;
- 每個(gè)chunk至少8字節(jié)的開銷很大往湿;
- 不定期分配長(zhǎng)生命周期的內(nèi)存容易造成內(nèi)存碎片,不利于回收惋戏。 64位系統(tǒng)最好分配32M以上內(nèi)存领追,這是使用mmap的閾值;
1.2 - tcmalloc
tcmalloc是Google開源的一個(gè)內(nèi)存管理庫(kù)响逢, 作為glibc malloc的替代品绒窑。目前已經(jīng)在chrome、safari等知名軟件中運(yùn)用舔亭。根據(jù)官方測(cè)試報(bào)告些膨,ptmalloc在一臺(tái)2.8GHz的P4機(jī)器上(對(duì)于小對(duì)象)執(zhí)行一次malloc及free大約需要300納秒。而TCMalloc的版本同樣的操作大約只需要50納秒钦铺。
小對(duì)象分配
- tcmalloc為每個(gè)線程分配了一個(gè)線程本地ThreadCache订雾,小內(nèi)存從ThreadCache分配,此外還有個(gè)中央堆(CentralCache)矛洞,ThreadCache不夠用的時(shí)候洼哎,會(huì)從CentralCache中獲取空間放到ThreadCache中;
- 小對(duì)象(<=32K)從ThreadCache分配缚甩,大對(duì)象從CentralCache分配谱净。大對(duì)象分配的空間都是4k頁(yè)面對(duì)齊的,多個(gè)pages也能切割成多個(gè)小對(duì)象劃分到ThreadCache中擅威;
小對(duì)象有將近170個(gè)不同的大小分類(class)壕探,每個(gè)class有個(gè)該大小內(nèi)存塊的FreeList單鏈表,分配的時(shí)候先找到best fit的class郊丛,然后無鎖的獲取該鏈表首元素返回李请。如果鏈表中無空間了,則到CentralCache中劃分幾個(gè)頁(yè)面并切割成該class的大小厉熟,放入鏈表中导盅。
CentralCache分配管理
- 大對(duì)象(>32K)先4k對(duì)齊后,從CentralCache中分配揍瑟。 CentralCache維護(hù)的PageHeap如下圖所示白翻, 數(shù)組中第256個(gè)元素是所有大于255個(gè)頁(yè)面都掛到該鏈表中;
- 當(dāng)best fit的頁(yè)面鏈表中沒有空閑空間時(shí),則一直往更大的頁(yè)面空間則滤馍,如果所有256個(gè)鏈表遍歷后依然沒有成功分配岛琼。 則使用sbrk, mmap, /dev/mem從系統(tǒng)中分配;
- tcmalloc PageHeap管理的連續(xù)的頁(yè)面被稱為span巢株。
- 如果span未分配槐瑞, 則span是PageHeap中的一個(gè)鏈表元素;
- 如果span已經(jīng)分配阁苞,它可能是返回給應(yīng)用程序的大對(duì)象困檩, 或者已經(jīng)被切割成多小對(duì)象,該小對(duì)象的size-class會(huì)被記錄在span中那槽;
- 在32位系統(tǒng)中悼沿,使用一個(gè)中央數(shù)組(central array)映射了頁(yè)面和span對(duì)應(yīng)關(guān)系, 數(shù)組索引號(hào)是頁(yè)面號(hào)倦炒,數(shù)組元素是頁(yè)面所在的span显沈。 在64位系統(tǒng)中,使用一個(gè)3-level radix tree記錄了該映射關(guān)系逢唤;
回收
- 當(dāng)一個(gè)object free的時(shí)候拉讯,會(huì)根據(jù)地址對(duì)齊計(jì)算所在的頁(yè)面號(hào),然后通過central array找到對(duì)應(yīng)的span鳖藕;
- 如果是小對(duì)象魔慷,span會(huì)告訴我們他的size class,然后把該對(duì)象插入當(dāng)前線程的ThreadCache中著恩。如果此時(shí)ThreadCache超過一個(gè)預(yù)算的值(默認(rèn)2MB)院尔,則會(huì)使用垃圾回收機(jī)制把未使用的object從ThreadCache移動(dòng)到CentralCache的central free lists中;
- 如果是大對(duì)象喉誊,span會(huì)告訴我們對(duì)象鎖在的頁(yè)面號(hào)范圍邀摆。 假設(shè)這個(gè)范圍是[p,q], 先查找頁(yè)面p-1和q+1所在的span伍茄,如果這些臨近的span也是free的栋盹,則合并到[p,q]所在的span, 然后把這個(gè)span回收到PageHeap中敷矫;
- CentralCache的central free lists類似ThreadCache的FreeList例获,不過它增加了一級(jí)結(jié)構(gòu),先根據(jù)size-class關(guān)聯(lián)到spans的集合曹仗, 然后是對(duì)應(yīng)span的object鏈表榨汤。如果span的鏈表中所有object已經(jīng)free, 則span回收到PageHeap中怎茫;
tcmalloc的改進(jìn)
- ThreadCache會(huì)階段性的回收內(nèi)存到CentralCache里收壕。 解決了ptmalloc2中arena之間不能遷移的問題;
- Tcmalloc占用更少的額外空間。例如啼器,分配N個(gè)8字節(jié)對(duì)象可能要使用大約8N * 1.01字節(jié)的空間旬渠。即,多用百分之一的空間端壳。Ptmalloc2使用最少8字節(jié)描述一個(gè)chunk;
- 更快枪蘑。小對(duì)象幾乎無鎖损谦, >32KB的對(duì)象從CentralCache中分配使用自旋鎖。 并且>32KB對(duì)象都是頁(yè)面對(duì)齊分配岳颇,多線程的時(shí)候應(yīng)盡量避免頻繁分配照捡,否則也會(huì)造成自旋鎖的競(jìng)爭(zhēng)和頁(yè)面對(duì)齊造成的浪費(fèi);
性能對(duì)比
官方測(cè)試環(huán)境是2.4GHz dual Xeon话侧,開啟超線程栗精,redhat9,glibc-2.3.2, 每個(gè)線程測(cè)試100萬個(gè)操作瞻鹏。
上圖中可以看到尤其是對(duì)于小內(nèi)存的分配悲立, tcmalloc有非常明顯性能優(yōu)勢(shì)。
上圖可以看到隨著線程數(shù)的增加新博,tcmalloc性能上也有明顯的優(yōu)勢(shì)薪夕,并且相對(duì)平穩(wěn)。
github使用tcmalloc后赫悄,mysql性能提升30%
1.3 - Jemalloc
jemalloc是facebook推出的原献, 最早的時(shí)候是freebsd的libc malloc實(shí)現(xiàn)。 目前在firefox埂淮、facebook服務(wù)器各種組件中大量使用姑隅。
jemalloc原理
- 與tcmalloc類似,每個(gè)線程同樣在<32KB的時(shí)候無鎖使用線程本地cache倔撞;
- Jemalloc在64bits系統(tǒng)上使用下面的size-class分類:
- Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
- Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
- Huge: [4 MiB, 8 MiB, 12 MiB, …]
- small/large對(duì)象查找metadata需要常量時(shí)間讲仰, huge對(duì)象通過全局紅黑樹在對(duì)數(shù)時(shí)間內(nèi)查找;
- 虛擬內(nèi)存被邏輯上分割成chunks(默認(rèn)是4MB误窖,1024個(gè)4k頁(yè))叮盘,應(yīng)用線程通過round-robin算法在第一次malloc的時(shí)候分配arena, 每個(gè)arena都是相互獨(dú)立的霹俺,維護(hù)自己的chunks柔吼, chunk切割pages到small/large對(duì)象。free()的內(nèi)存總是返回到所屬的arena中丙唧,而不管是哪個(gè)線程調(diào)用free()愈魏;
上圖可以看到每個(gè)arena管理的arena chunk結(jié)構(gòu), 開始的header主要是維護(hù)了一個(gè)page map(1024個(gè)頁(yè)面關(guān)聯(lián)的對(duì)象狀態(tài)), header下方就是它的頁(yè)面空間培漏。 Small對(duì)象被分到一起溪厘, metadata信息存放在起始位置。 large chunk相互獨(dú)立牌柄,它的metadata信息存放在chunk header map中畸悬。
- 通過arena分配的時(shí)候需要對(duì)arena bin(每個(gè)small size-class一個(gè),細(xì)粒度)加鎖珊佣,或arena本身加鎖蹋宦。并且線程cache對(duì)象也會(huì)通過垃圾回收指數(shù)退讓算法返回到arena中;
jemalloc的優(yōu)化
- Jmalloc小對(duì)象也根據(jù)size-class咒锻,但是它使用了低地址優(yōu)先的策略冷冗,來降低內(nèi)存碎片化;
- Jemalloc大概需要2%的額外開銷(tcmalloc 1%惑艇, ptmalloc最少8B)蒿辙;
- Jemalloc和tcmalloc類似的線程本地緩存,避免鎖的競(jìng)爭(zhēng)滨巴;
- 相對(duì)未使用的頁(yè)面思灌,優(yōu)先使用dirty page,提升緩存命中兢卵;
性能對(duì)比
官方測(cè)試
上圖是服務(wù)器吞吐量分別用6個(gè)malloc實(shí)現(xiàn)的對(duì)比數(shù)據(jù)习瑰,可以看到tcmalloc和jemalloc最好(facebook在2011年的測(cè)試結(jié)果,tcmalloc這里版本較舊)秽荤。
可以看到在多核心或者多線程的場(chǎng)景下甜奄, jemalloc和tcmalloc帶來的tps增加非常明顯。當(dāng)線程數(shù)量固定窃款,不會(huì)頻繁創(chuàng)建退出的時(shí)候课兄, 可以使用jemalloc;反之使用tcmalloc可能是更好的選擇晨继。
3 - MySQL替換malloc
下載對(duì)應(yīng)版本的Jemalloc烟阐,解壓、編譯紊扬。
./configure --prefix=/usr/local/jemalloc --libdir=/usr/lib
make && make install
echo /usr/lib >> /etc/ld.so.conf
ldconfig
修改MySQL配置:
[mysqld_safe]
malloc-lib=/usr/lib/libjemalloc.so
啟動(dòng)MySQL蜒茄,查看是否生效:
lsof -n | grep libjemalloc.so
4 - Jemalloc性能測(cè)試
通過一個(gè)簡(jiǎn)單的內(nèi)存分配、釋放程序進(jìn)行餐屎,共分為三種情況:
- 程序無修改檀葛,正常編譯,使用系統(tǒng)自帶的malloc和free腹缩,正常運(yùn)行屿聋;
- 程序有修改空扎,在程序中顯示使用jemalloc庫(kù);
- 程序無修改润讥,正常編譯转锈,運(yùn)行前通過LD_PRELOAD環(huán)境變量提前加載jemalloc庫(kù);
#include <stdio.h>
#include<time.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#ifdef USE_JEMALLOC
#include <jemalloc.h>
#endif
#define MSECOND 1000000
int main(void)
{
int i = 0;
char pad = 0;
char *ptr = NULL;
size_t size = 128;
float timeuse = 0;
struct timeval tpstart;
struct timeval tpend;
int loops = 100000000;
srand((int)time(0));
gettimeofday(&tpstart, NULL);
for (i = 0; i < loops; i++) {
size = rand() & 0x0000000000000fff;
ptr = malloc(size);
pad = rand() & 0xff;
memset(ptr, pad, size);
free(ptr);
}
gettimeofday(&tpend, NULL);
timeuse=MSECOND*(tpend.tv_sec-tpstart.tv_sec)+tpend.tv_usec-tpstart.tv_usec;
timeuse/=MSECOND;
printf("Used Time [%f] with [%d] loops\n", timeuse, loops);
return 0;
}
根據(jù)前文說的三種情況楚殿,共需要編譯出兩種二進(jìn)制文件:
gcc memory.c -o a1.out -DUSE_JEMALLOC -O2 -ljemalloc
gcc memory.c -o a2.out -O2
得到a1.out為顯示使用jemalloc的版本撮慨,a2.out為正常版本。
測(cè)試方法:
- 直接運(yùn)行a1.out勒魔,可以得出顯式使用jemalloc時(shí)的執(zhí)行時(shí)間;
- 直接運(yùn)行a2.out甫煞,可以得到使用普通malloc時(shí)的執(zhí)行時(shí)間;
- 通過LD_PRELOAD環(huán)境變量執(zhí)行a2.out,可以得到普通程序使用jemalloc時(shí)的執(zhí)行時(shí)間;
./a1.out
./a2.out
LD_PRELOAD="/PATH/TO/libjemalloc.so" ./a2.out
得到三組數(shù)據(jù)如下:
Used Time [8.849940] with [100000000] loops
Used Time [11.283691] with [100000000] loops
Used Time [8.637403] with [100000000] loops
可以看到冠绢,在申請(qǐng)和釋放隨機(jī)大小的內(nèi)存塊時(shí),使用jemalloc的性能明顯好于普通malloc常潮,性能提升約27.5%弟胀。
如果將程序源碼中的size = rand() & 0x0000000000000fff;
一行注釋掉,讓程序申請(qǐng)固定大小的內(nèi)存塊喊式,得到三組數(shù)據(jù):
Used Time [2.380698] with [100000000] loops
Used Time [4.532686] with [100000000] loops
Used Time [2.369266] with [100000000] loops
這種情況下使用jemalloc比普通malloc版本的性能提升將近一倍孵户,同時(shí)比各自的隨機(jī)大小內(nèi)存塊版本提升大約也有三四倍。
5 - 總結(jié)
無論是google的tcmalloc岔留,還是facebook的jemalloc夏哭,在高并發(fā)的場(chǎng)景下對(duì)MySQL的TPS提升均在30%左右。而MySQL默認(rèn)使用的glibc的malloc献联,不僅內(nèi)存操作處理的效率低竖配,也有OOM的風(fēng)險(xiǎn)。在條件允許的情況下里逆,可以替換进胯,推薦使用Jemalloc。
完原押。