生活随笔
收集整理的這篇文章主要介紹了
Memcached深度分析
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
Memcached是danga.com(運營LiveJournal的技術(shù)團(tuán)隊)開發(fā)的一套分布式內(nèi)存對象緩存系統(tǒng),用于在動態(tài)系統(tǒng)中減少數(shù)據(jù)庫負(fù)載, 提升性能。關(guān)于這個東西,相信很多人都用過,本文意在通過對memcached的實現(xiàn)及代碼分析,獲得對這個出色的開源軟件更深入的了解,并可以根據(jù)我們 的需要對其進(jìn)行更進(jìn)一步的優(yōu)化。末了將通過對BSM_Memcache擴(kuò)展的分析,加深對memcached的使用方式理解。 本文的部分內(nèi)容可能需要比較好的數(shù)學(xué)基礎(chǔ)作為輔助。
◎Memcached是什么 在闡述這個問題之前,我們首先要清楚它“不是什么”。很多人把它當(dāng)作和SharedMemory那種形式的存儲載體來使用,雖然memcached使用了 同樣的“Key=>Value”方式組織數(shù)據(jù),但是它和共享內(nèi)存、APC等本地緩存有非常大的區(qū)別。Memcached是分布式的,也就是說它不是 本地的。它基于網(wǎng)絡(luò)連接(當(dāng)然它也可以使用localhost)方式完成服務(wù),本身它是一個獨立于應(yīng)用的程序或守護(hù)進(jìn)程(Daemon方式)。 Memcached使用libevent庫實現(xiàn)網(wǎng)絡(luò)連接服務(wù),理論上可以處理無限多的連接,但是它和Apache不同,它更多的時候是面向穩(wěn)定的持續(xù)連接 的,所以它實際的并發(fā)能力是有限制的。在保守情況下memcached的最大同時連接數(shù)為200,這和Linux線程能力有關(guān)系,這個數(shù)值是可以調(diào)整的。 關(guān)于libevent可以參考相關(guān)文檔。 Memcached內(nèi)存使用方式也和APC不同。APC是基于共享內(nèi)存和MMAP的,memcachd有自己的內(nèi)存分配算法和管理方式,它和共享內(nèi)存沒有 關(guān)系,也沒有共享內(nèi)存的限制,通常情況下,每個memcached進(jìn)程可以管理2GB的內(nèi)存空間,如果需要更多的空間,可以增加進(jìn)程數(shù)。
◎Memcached適合什么場合 在很多時候,memcached都被濫用了,這當(dāng)然少不了對它的抱怨。我經(jīng)常在論壇上看見有人發(fā)貼,類似于“如何提高效率”,回復(fù)是“用memcached”,至于怎么用,用在哪里,用來干什么一句沒有。memcached不是萬能的,它也不是適用在所有場合。 Memcached是“分布式”的內(nèi)存對象緩存系統(tǒng),那么就是說,那些不需要“分布”的,不需要共享的,或者干脆規(guī)模小到只有一臺服務(wù)器的應(yīng)用, memcached不會帶來任何好處,相反還會拖慢系統(tǒng)效率,因為網(wǎng)絡(luò)連接同樣需要資源,即使是UNIX本地連接也一樣。 在我之前的測試數(shù)據(jù)中顯示,memcached本地讀寫速度要比直接PHP內(nèi)存數(shù)組慢幾十倍,而APC、共享內(nèi)存方式都和直接數(shù)組差不多??梢?#xff0c;如果只是 本地級緩存,使用memcached是非常不劃算的。 Memcached在很多時候都是作為數(shù)據(jù)庫前端cache使用的。因為它比數(shù)據(jù)庫少了很多SQL解析、磁盤操作等開銷,而且它是使用內(nèi)存來管理數(shù)據(jù)的, 所以它可以提供比直接讀取數(shù)據(jù)庫更好的性能,在大型系統(tǒng)中,訪問同樣的數(shù)據(jù)是很頻繁的,memcached可以大大降低數(shù)據(jù)庫壓力,使系統(tǒng)執(zhí)行效率提升。 另外,memcached也經(jīng)常作為服務(wù)器之間數(shù)據(jù)共享的存儲媒介,例如在SSO系統(tǒng)中保存系統(tǒng)單點登陸狀態(tài)的數(shù)據(jù)就可以保存在memcached中,被 多個應(yīng)用共享。 需要注意的是,memcached使用內(nèi)存管理數(shù)據(jù),所以它是易失的,當(dāng)服務(wù)器重啟,或者memcached進(jìn)程中止,數(shù)據(jù)便會丟失,所以 memcached不能用來持久保存數(shù)據(jù)。很多人的錯誤理解,memcached的性能非常好,好到了內(nèi)存和硬盤的對比程度,其實memcached使用 內(nèi)存并不會得到成百上千的讀寫速度提高,它的實際瓶頸在于網(wǎng)絡(luò)連接,它和使用磁盤的數(shù)據(jù)庫系統(tǒng)相比,好處在于它本身非?!拜p”,因為沒有過多的開銷和直接 的讀寫方式,它可以輕松應(yīng)付非常大的數(shù)據(jù)交換量,所以經(jīng)常會出現(xiàn)兩條千兆網(wǎng)絡(luò)帶寬都滿負(fù)荷了,memcached進(jìn)程本身并不占用多少CPU資源的情況。
◎Memcached的工作方式 以下的部分中,讀者最好能準(zhǔn)備一份memcached的源代碼。 Memcached是傳統(tǒng)的網(wǎng)絡(luò)服務(wù)程序,如果啟動的時候使用了-d參數(shù),它會以守護(hù)進(jìn)程的方式執(zhí)行。創(chuàng)建守護(hù)進(jìn)程由daemon.c完成,這個程序只有一個daemon函數(shù),這個函數(shù)很簡單(如無特殊說明,代碼以1.2.1為準(zhǔn)):
CODE: #include <fcntl.h> #include <stdlib.h> #include <unistd.h> int daemon(nochdir, noclose) int nochdir, noclose; { int fd; switch (fork()) { case -1: return (-1); case 0: break; default: _exit(0); } if (setsid() == -1) return (-1); if (!nochdir) (void)chdir("/"); if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) { (void)dup2(fd, STDIN_FILENO); (void)dup2(fd, STDOUT_FILENO); (void)dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void)close(fd); } return (0); }
這個函數(shù) fork 了整個進(jìn)程之后,父進(jìn)程就退出,接著重新定位 STDIN 、 STDOUT 、 STDERR 到空設(shè)備, daemon 就建立成功了。 Memcached 本身的啟動過程,在 memcached.c 的 main 函數(shù)中順序如下: 1 、調(diào)用 settings_init() 設(shè)定初始化參數(shù) 2 、從啟動命令中讀取參數(shù)來設(shè)置 setting 值 3 、設(shè)定 LIMIT 參數(shù) 4 、開始網(wǎng)絡(luò) socket 監(jiān)聽(如果非 socketpath 存在)( 1.2 之后支持 UDP 方式) 5 、檢查用戶身份( Memcached 不允許 root 身份啟動) 6 、如果有 socketpath 存在,開啟 UNIX 本地連接(Sock 管道) 7 、如果以 -d 方式啟動,創(chuàng)建守護(hù)進(jìn)程(如上調(diào)用 daemon 函數(shù)) 8 、初始化 item 、 event 、狀態(tài)信息、 hash 、連接、 slab 9 、如設(shè)置中 managed 生效,創(chuàng)建 bucket 數(shù)組 10 、檢查是否需要鎖定內(nèi)存頁 11 、初始化信號、連接、刪除隊列 12 、如果 daemon 方式,處理進(jìn)程 ID 13 、event 開始,啟動過程結(jié)束, main 函數(shù)進(jìn)入循環(huán)。 在 daemon 方式中,因為 stderr 已經(jīng)被定向到黑洞,所以不會反饋執(zhí)行中的可見錯誤信息。 memcached.c 的主循環(huán)函數(shù)是 drive_machine ,傳入?yún)?shù)是指向當(dāng)前的連接的結(jié)構(gòu)指針,根據(jù) state 成員的狀態(tài)來決定動作。 Memcached 使用一套自定義的協(xié)議完成數(shù)據(jù)交換,它的 protocol 文檔可以參考: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt 在API中,換行符號統(tǒng)一為/r/n
◎Memcached的內(nèi)存管理方式 Memcached有一個很有特色的內(nèi)存管理方式,為了提高效率,它使用預(yù)申請和分組的方式管理內(nèi)存空間,而并不是每次需要寫入數(shù)據(jù)的時候去malloc,刪除數(shù)據(jù)的時候free一個指針。Memcached使用slab->chunk的組織方式管理內(nèi)存。 1.1和1.2的slabs.c中的slab空間劃分算法有一些不同,后面會分別介紹。 Slab可以理解為一個內(nèi)存塊,一個slab是memcached一次申請內(nèi)存的最小單位,在memcached中,一個slab的大小默認(rèn)為 1048576字節(jié)(1MB),所以memcached都是整MB的使用內(nèi)存。每一個slab被劃分為若干個chunk,每個chunk里保存一個 item,每個item同時包含了item結(jié)構(gòu)體、key和value(注意在memcached中的value是只有字符串的)。slab按照自己的 id分別組成鏈表,這些鏈表又按id掛在一個slabclass數(shù)組上,整個結(jié)構(gòu)看起來有點像二維數(shù)組。slabclass的長度在1.1中是21,在 1.2中是200。 slab有一個初始chunk大小,1.1中是1字節(jié),1.2中是80字節(jié),1.2中有一個factor值,默認(rèn)為1.25 在1.1中,chunk大小表示為初始大小*2^n,n為classid,即:id為0的slab,每chunk大小1字節(jié),id為1的slab,每 chunk大小2字節(jié),id為2的slab,每chunk大小4字節(jié)……id為20的slab,每chunk大小為1MB,就是說id為20的slab里 只有一個chunk:
CODE: void slabs_init(size_t limit) { int i; int size=1; mem_limit = limit; for(i=0; i<=POWER_LARGEST; i++, size*=2) { slabclass[i].size = size; slabclass[i].perslab = POWER_BLOCK / size; slabclass[i].slots = 0; slabclass[i].sl_curr = slabclass[i].sl_total = slabclass[i].slabs = 0; slabclass[i].end_page_ptr = 0; slabclass[i].end_page_free = 0; slabclass[i].slab_list = 0; slabclass[i].list_size = 0; slabclass[i].killing = 0; } /* for the test suite: faking of how much we've already malloc'd */ { char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC"); if (t_initial_malloc) { mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC")); } } /* pre-allocate slabs by default, unless the environment variable for testing is set to something non-zero */ { char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC"); if (!pre_alloc || atoi(pre_alloc)) { slabs_preallocate(limit / POWER_BLOCK); } } }
在1.2 中,chunk大小表示為初始大小*f^n,f為factor,在memcached.c中定義,n為classid,同時,201個頭不是全部都要初始 化的,因為factor可變,初始化只循環(huán)到計算出的大小達(dá)到slab大小的一半為止,而且它是從id1開始的,即:id為1的slab,每chunk大 小80字節(jié),id為2的slab,每chunk大小80*f,id為3的slab,每chunk大小80*f^2,初始化大小有一個修正值 CHUNK_ALIGN_BYTES,用來保證n-byte排列 (保證結(jié)果是CHUNK_ALIGN_BYTES的整倍數(shù))。這樣,在標(biāo)準(zhǔn)情況下,memcached1.2會初始化到id40,這個slab中每個 chunk大小為504692,每個slab中有兩個chunk。最后,slab_init函數(shù)會在最后補(bǔ)足一個id41,它是整塊的,也就是這個 slab中只有一個1MB大的chunk:
CODE: void slabs_init(size_t limit, double factor) { int i = POWER_SMALLEST - 1; unsigned int size = sizeof(item) + settings.chunk_size; /* Factor of 2.0 means use the default memcached behavior */ if (factor == 2.0 && size < 128) size = 128; mem_limit = limit; memset(slabclass, 0, sizeof(slabclass)); while (++i < POWER_LARGEST && size <= POWER_BLOCK / 2) { /* Make sure items are always n-byte aligned */ if (size % CHUNK_ALIGN_BYTES) size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES); slabclass[i].size = size; slabclass[i].perslab = POWER_BLOCK / slabclass[i].size; size *= factor; if (settings.verbose > 1) { fprintf(stderr, "slab class %3d: chunk size %6d perslab %5d/n", i, slabclass[i].size, slabclass[i].perslab); } } power_largest = i; slabclass[power_largest].size = POWER_BLOCK; slabclass[power_largest].perslab = 1; /* for the test suite: faking of how much we've already malloc'd */ { char *t_initial_malloc = getenv("T_MEMD_INITIAL_MALLOC"); if (t_initial_malloc) { mem_malloced = atol(getenv("T_MEMD_INITIAL_MALLOC")); } } #ifndef DONT_PREALLOC_SLABS { char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC"); if (!pre_alloc || atoi(pre_alloc)) { slabs_preallocate(limit / POWER_BLOCK); } } #endif }
由上可以看出,memcached的內(nèi)存分配是有冗余的,當(dāng)一個slab不能被它所擁有的chunk大小整除時,slab尾部剩余的空間就被丟棄了,如id40中,兩個chunk占用了1009384字節(jié),這個slab一共有1MB,那么就有39192字節(jié)被浪費了。 Memcached使用這種方式來分配內(nèi)存,是為了可以快速的通過item長度定位出slab的classid,有一點類似hash,因為item的長度 是可以計算的,比如一個item的長度是300字節(jié),在1.2中就可以得到它應(yīng)該保存在id7的slab中,因為按照上面的計算方法,id6的chunk 大小是252字節(jié),id7的chunk大小是316字節(jié),id8的chunk大小是396字節(jié),表示所有252到316字節(jié)的item都應(yīng)該保存在id7 中。同理,在1.1中,也可以計算得到它出于256和512之間,應(yīng)該放在chunk_size為512的id9中(32位系統(tǒng))。 Memcached初始化的時候,會初始化slab(前面可以看到,在main函數(shù)中調(diào)用了slabs_init())。它會在slabs_init() 中檢查一個常量DONT_PREALLOC_SLABS,如果這個沒有被定義,說明使用預(yù)分配內(nèi)存方式初始化slab,這樣在所有已經(jīng)定義過的 slabclass中,每一個id創(chuàng)建一個slab。這樣就表示,1.2在默認(rèn)的環(huán)境中啟動進(jìn)程后要分配41MB的slab空間,在這個過程里, memcached的第二個內(nèi)存冗余發(fā)生了,因為有可能一個id根本沒有被使用過,但是它也默認(rèn)申請了一個slab,每個slab會用掉1MB內(nèi)存 當(dāng)一個slab用光后,又有新的item要插入這個id,那么它就會重新申請新的slab,申請新的slab時,對應(yīng)id的slab鏈表就要增長,這個鏈表是成倍增長的,在函數(shù)grow_slab_list函數(shù)中,這個鏈的長度從1變成2,從2變成4,從4變成8……:
CODE: static int grow_slab_list (unsigned int id) { slabclass_t *p = &slabclass[id]; if (p->slabs == p->list_size) { size_t new_size = p->list_size ? p->list_size * 2 : 16; void *new_list = realloc(p->slab_list, new_size*sizeof(void*)); if (new_list == 0) return 0; p->list_size = new_size; p->slab_list = new_list; } return 1; }
在 定位item時,都是使用slabs_clsid函數(shù),傳入?yún)?shù)為item大小,返回值為classid,由這個過程可以看出,memcached的第三 個內(nèi)存冗余發(fā)生在保存item的過程中,item總是小于或等于chunk大小的,當(dāng)item小于chunk大小時,就又發(fā)生了空間浪費。
◎Memcached的NewHash算法 Memcached的item保存基于一個大的hash表,它的實際地址就是slab中的chunk偏移,但是它的定位是依靠對key做hash的結(jié)果, 在primary_hashtable中找到的。在assoc.c和items.c中定義了所有的hash和item操作。 Memcached使用了一個叫做NewHash的算法,它的效果很好,效率也很高。1.1和1.2的NewHash有一些不同,主要的實現(xiàn)方式還是一樣的,1.2的hash函數(shù)是經(jīng)過整理優(yōu)化的,適應(yīng)性更好一些。 NewHash的原型參考:http://burtleburtle.net/bob/hash/evahash.html。數(shù)學(xué)家總是有點奇怪,呵呵~ 為了變換方便,定義了u4和u1兩種數(shù)據(jù)類型,u4就是無符號的長整形,u1就是無符號char(0-255)。 具體代碼可以參考1.1和1.2源碼包。 注意這里的hashtable長度,1.1和1.2也是有區(qū)別的,1.1中定義了HASHPOWER常量為20,hashtable表長為 hashsize(HASHPOWER),就是4MB(hashsize是一個宏,表示1右移n位),1.2中是變量16,即hashtable表長 65536:
CODE: typedef unsigned long int ub4; /* unsigned 4-byte quantities */ typedef unsigned char ub1; /* unsigned 1-byte quantities */ #define hashsize(n) ((ub4)1<<(n)) #define hashmask(n) (hashsize(n)-1)
在assoc_init ()中,會對primary_hashtable做初始化,對應(yīng)的hash操作包括:assoc_find()、assoc_expand()、 assoc_move_next_bucket()、assoc_insert()、assoc_delete(),對應(yīng)于item的讀寫操作。其中 assoc_find()是根據(jù)key和key長尋找對應(yīng)的item地址的函數(shù)(注意在C中,很多時候都是同時直接傳入字符串和字符串長度,而不是在函數(shù) 內(nèi)部做strlen),返回的是item結(jié)構(gòu)指針,它的數(shù)據(jù)地址在slab中的某個chunk上。 items.c是數(shù)據(jù)項的操作程序,每一個完整的item包括幾個部分,在item_make_header()中定義為: key:鍵 nkey:鍵長 flags:用戶定義的flag(其實這個flag在memcached中沒有啟用) nbytes:值長(包括換行符號/r/n) suffix:后綴Buffer nsuffix:后綴長 一個完整的item長度是鍵長+值長+后綴長+item結(jié)構(gòu)大小(32字節(jié)),item操作就是根據(jù)這個長度來計算slab的classid的。 hashtable中的每一個桶上掛著一個雙鏈表,item_init()的時候已經(jīng)初始化了heads、tails、sizes三個數(shù)組為0,這三個數(shù) 組的大小都為常量LARGEST_ID(默認(rèn)為255,這個值需要配合factor來修改),在每次item_assoc()的時候,它會首先嘗試從 slab中獲取一塊空閑的chunk,如果沒有可用的chunk,會在鏈表中掃描50次,以得到一個被LRU踢掉的item,將它unlink,然后將需 要插入的item插入鏈表中。 注意item的refcount成員。item被unlink之后只是從鏈表上摘掉,不是立刻就被free的,只是將它放到刪除隊列中(item_unlink_q()函數(shù))。 item對應(yīng)一些讀寫操作,包括remove、update、replace,當(dāng)然最重要的就是alloc操作。 item還有一個特性就是它有過期時間,這是memcached的一個很有用的特性,很多應(yīng)用都是依賴于memcached的item過期,比如 session存儲、操作鎖等。item_flush_expired()函數(shù)就是掃描表中的item,對過期的item執(zhí)行unlink操作,當(dāng)然這只 是一個回收動作,實際上在get的時候還要進(jìn)行時間判斷:
CODE: /* expires items that are more recent than the oldest_live setting. */ void item_flush_expired() { int i; item *iter, *next; if (! settings.oldest_live) return; for (i = 0; i < LARGEST_ID; i++) { /* The LRU is sorted in decreasing time order, and an item's timestamp * is never newer than its last access time, so we only need to walk * back until we hit an item older than the oldest_live time. * The oldest_live checking will auto-expire the remaining items. */ for (iter = heads[i]; iter != NULL; iter = next) { if (iter->time >= settings.oldest_live) { next = iter->next; if ((iter->it_flags & ITEM_SLABBED) == 0) { item_unlink(iter); } } else { /* We've hit the first old item. Continue to the next queue. */ break; } } } }
CODE: /* wrapper around assoc_find which does the lazy expiration/deletion logic */ item *get_item_notedeleted(char *key, size_t nkey, int *delete_locked) { item *it = assoc_find(key, nkey); if (delete_locked) *delete_locked = 0; if (it && (it->it_flags & ITEM_DELETED)) { /* it's flagged as delete-locked. let's see if that condition is past due, and the 5-second delete_timer just hasn't gotten to it yet... */ if (! item_delete_lock_over(it)) { if (delete_locked) *delete_locked = 1; it = 0; } } if (it && settings.oldest_live && settings.oldest_live <= current_time && it->time <= settings.oldest_live) { item_unlink(it); it = 0; } if (it && it->exptime && it->exptime <= current_time) { item_unlink(it); it = 0; } return it; }
Memcached的內(nèi)存管理方式是非常精巧和高效的,它很大程度上減少了直接alloc系統(tǒng)內(nèi)存的次數(shù),降低函數(shù)開銷和內(nèi)存碎片產(chǎn)生幾率,雖然這種方式會造成一些冗余浪費,但是這種浪費在大型系統(tǒng)應(yīng)用中是微不足道的。
總結(jié)
以上是生活随笔 為你收集整理的Memcached深度分析 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。