Memcached深度分析

來源:互聯網
上載者:User
Memcached是danga.com(運營LiveJournal的技術團隊)開發的一套分布式記憶體對象緩衝系統,用於在動態系統中減少資料庫負載, 提升效能。關於這個東西,相信很多人都用過,本文意在通過對memcached的實現及程式碼分析,獲得對這個出色的開源軟體更深入的瞭解,並可以根據我們 的需要對其進行更進一步的最佳化。末了將通過對BSM_Memcache擴充的分析,加深對memcached的使用方式理解。 本文的部分內容可能需要比較好的數學基礎作為輔助。 ◎Memcached是什麼 在闡述這個問題之前,我們首先要清楚它“不是什麼”。很多人把它當作和SharedMemory那種形式的儲存載體來使用,雖然memcached使用了 同樣的“Key=>Value”方式組織資料,但是它和共用記憶體、APC等本機快取有非常大的區別。Memcached是分布式的,也就是說它不是 本地的。它基於網路連接(當然它也可以使用localhost)方式完成服務,本身它是一個獨立於應用的程式或守護進程(Daemon方式)。 Memcached使用libevent庫實現網路連接服務,理論上可以處理無限多的串連,但是它和Apache不同,它更多的時候是面向穩定的持續串連 的,所以它實際的並發能力是有限制的。在保守情況下memcached的最大同時串連數為200,這和Linux線程能力有關係,這個數值是可以調整的。 關於libevent可以參考相關文檔。 Memcached記憶體使用量方式也和APC不同。APC是基於共用記憶體和MMAP的,memcachd有自己的記憶體配置演算法和管理方式,它和共用記憶體沒有 關係,也沒有共用記憶體的限制,通常情況下,每個memcached進程可以管理2GB的記憶體空間,如果需要更多的空間,可以增加進程數。 ◎Memcached適合什麼場合 在很多時候,memcached都被濫用了,這當然少不了對它的抱怨。我經常在論壇上看見有人發貼,類似於“如何提高效率”,回複是“用memcached”,至於怎麼用,用在哪裡,用來幹什麼一句沒有。memcached不是萬能的,它也不是適用在所有場合。 Memcached是“分布式”的記憶體對象緩衝系統,那麼就是說,那些不需要“分布”的,不需要共用的,或者乾脆規模小到只有一台伺服器的應用, memcached不會帶來任何好處,相反還會拖慢系統效率,因為網路連接同樣需要資源,即使是UNIX本地串連也一樣。 在我之前的測試資料中顯示,memcached本地讀寫速度要比直接PHP記憶體數組慢幾十倍,而APC、共用記憶體方式都和直接數組差不多。可見,如果只是 本地級緩衝,使用memcached是非常不划算的。 Memcached在很多時候都是作為資料庫前端cache使用的。因為它比資料庫少了很多SQL解析、磁碟操作等開銷,而且它是使用記憶體來管理資料的, 所以它可以提供比直接讀取資料庫更好的效能,在大型系統中,訪問同樣的資料是很頻繁的,memcached可以大大降低資料庫壓力,使系統執行效率提升。 另外,memcached也經常作為伺服器之間資料共用的儲存媒介,例如在SSO系統中儲存系統單點登陸狀態的資料就可以儲存在memcached中,被 多個應用共用。 需要注意的是,memcached使用記憶體管理資料,所以它是易失的,當伺服器重啟,或者memcached進程中止,資料便會丟失,所以 memcached不能用來持久儲存資料。很多人的錯誤理解,memcached的效能非常好,好到了記憶體和硬碟的對比程度,其實memcached使用 記憶體並不會得到成百上千的讀寫速度提高,它的實際瓶頸在於網路連接,它和使用磁碟的資料庫系統相比,好處在於它本身非常“輕”,因為沒有過多的開銷和直接 的讀寫方式,它可以輕鬆應付非常大的資料交換量,所以經常會出現兩條千兆網路頻寬都滿負荷了,memcached進程本身並不佔用多少CPU資源的情況。 ◎Memcached的工作方式 以下的部分中,讀者最好能準備一份memcached的原始碼。 Memcached是傳統的網路服務程式,如果啟動的時候使用了-d參數,它會以守護進程的方式執行。建立守護進程由daemon.c完成,這個程式只有一個daemon函數,這個函數很簡單(如無特殊說明,代碼以1.2.1為準):

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); }

這個函數 fork 了整個進程之後,父進程就退出,接著重新置放 STDIN 、 STDOUT 、 STDERR 到空裝置, daemon 就建立成功了。 Memcached 本身的啟動過程,在 memcached.c 的 main 函數中順序如下: 1 、調用 settings_init() 設定初始化參數 2 、從啟動命令中讀取參數來設定 setting 值 3 、設定 LIMIT 參數 4 、開始網路 socket 監聽(如果非 socketpath 存在)( 1.2 之後支援 UDP 方式) 5 、檢查使用者身份( Memcached 不允許 root 身份啟動) 6 、如果有 socketpath 存在,開啟 UNIX 本地串連(Sock 管道) 7 、如果以 -d 方式啟動,建立守護進程(如上調用 daemon 函數) 8 、初始化 item 、 event 、狀態資訊、 hash 、串連、 slab 9 、如設定中 managed 生效,建立 bucket 數組 10 、檢查是否需要鎖定記憶體頁 11 、初始化訊號、串連、刪除隊列 12 、如果 daemon 方式,處理進程 ID 13 、event 開始,啟動過程結束, main 函數進入迴圈。 在 daemon 方式中,因為 stderr 已經被定向到黑洞,所以不會反饋執行中的可見錯誤資訊。 memcached.c 的主迴圈函數是 drive_machine ,傳入參數是指向當前的串連的結構指標,根據 state 成員的狀態來決定動作。 Memcached 使用一套自訂的協議完成資料交換,它的 protocol 文檔可以參考: http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt 在API中,分行符號號統一為\r\n ◎Memcached的記憶體管理方式 Memcached有一個很有特色的記憶體管理方式,為了提高效率,它使用預申請和分組的方式管理記憶體空間,而並不是每次需要寫入資料的時候去malloc,刪除資料的時候free一個指標。Memcached使用slab->chunk的組織方式管理記憶體。 1.1和1.2的slabs.c中的slab空間劃分演算法有一些不同,後面會分別介紹。 Slab可以理解為一個記憶體塊,一個slab是memcached一次申請記憶體的最小單位,在memcached中,一個slab的大小預設為 1048576位元組(1MB),所以memcached都是整MB的使用記憶體。每一個slab被劃分為若干個chunk,每個chunk裡儲存一個 item,每個item同時包含了item結構體、key和value(注意在memcached中的value是只有字串的)。slab按照自己的 id分別組成鏈表,這些鏈表又按id掛在一個slabclass數組上,整個結構看起來有點像二維數組。slabclass的長度在1.1中是21,在 1.2中是200。 slab有一個初始chunk大小,1.1中是1位元組,1.2中是80位元組,1.2中有一個factor值,預設為1.25 在1.1中,chunk大小表示為初始大小*2^n,n為classid,即:id為0的slab,每chunk大小1位元組,id為1的slab,每 chunk大小2位元組,id為2的slab,每chunk大小4位元組……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可變,初始化只迴圈到計算出的大小達到slab大小的一半為止,而且它是從id1開始的,即:id為1的slab,每chunk大 小80位元組,id為2的slab,每chunk大小80*f,id為3的slab,每chunk大小80*f^2,初始化大小有一個修正值 CHUNK_ALIGN_BYTES,用來保證n-byte排列 (保證結果是CHUNK_ALIGN_BYTES的整倍數)。這樣,在標準情況下,memcached1.2會初始化到id40,這個slab中每個 chunk大小為504692,每個slab中有兩個chunk。最後,slab_init函數會在最後補足一個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的記憶體配置是有冗餘的,當一個slab不能被它所擁有的chunk大小整除時,slab尾部剩餘的空間就被丟棄了,如id40中,兩個chunk佔用了1009384位元組,這個slab一共有1MB,那麼就有39192位元組被浪費了。 Memcached使用這種方式來分配記憶體,是為了可以快速的通過item長度定位出slab的classid,有一點類似hash,因為item的長度 是可以計算的,比如一個item的長度是300位元組,在1.2中就可以得到它應該儲存在id7的slab中,因為按照上面的計算方法,id6的chunk 大小是252位元組,id7的chunk大小是316位元組,id8的chunk大小是396位元組,表示所有252到316位元組的item都應該儲存在id7 中。同理,在1.1中,也可以計算得到它出於256和512之間,應該放在chunk_size為512的id9中(32位系統)。 Memcached初始化的時候,會初始化slab(前面可以看到,在main函數中調用了slabs_init())。它會在slabs_init() 中檢查一個常量DONT_PREALLOC_SLABS,如果這個沒有被定義,說明使用預分配記憶體方式初始化slab,這樣在所有已經定義過的 slabclass中,每一個id建立一個slab。這樣就表示,1.2在預設的環境中啟動進程後要分配41MB的slab空間,在這個過程裡, memcached的第二個記憶體冗餘發生了,因為有可能一個id根本沒有被使用過,但是它也預設申請了一個slab,每個slab會用掉1MB記憶體 當一個slab用光後,又有新的item要插入這個id,那麼它就會重新申請新的slab,申請新的slab時,對應id的slab鏈表就要增長,這個鏈表是成倍增長的,在函數grow_slab_list函數中,這個鏈的長度從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函數,傳入參數為item大小,傳回值為classid,由這個過程可以看出,memcached的第三 個記憶體冗餘發生在儲存item的過程中,item總是小於或等於chunk大小的,當item小於chunk大小時,就又發生了空間浪費。 ◎Memcached的NewHash演算法 Memcached的item儲存基於一個大的hash表,它的實際地址就是slab中的chunk位移,但是它的定位是依靠對key做hash的結果, 在primary_hashtable中找到的。在assoc.c和items.c中定義了所有的hash和item操作。 Memcached使用了一個叫做NewHash的演算法,它的效果很好,效率也很高。1.1和1.2的NewHash有一些不同,主要的實現方式還是一樣的,1.2的hash函數是經過整理最佳化的,適應性更好一些。 NewHash的原型參考:http://burtleburtle.net/bob/hash/evahash.html。數學家總是有點奇怪,呵呵~ 為了變換方便,定義了u4和u1兩種資料類型,u4就是無符號的長整形,u1就是無符號char(0-255)。 具體代碼可以參考1.1和1.2源碼包。 注意這裡的hashtable長度,1.1和1.2也是有區別的,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做初始化,對應的hash操作包括:assoc_find()、assoc_expand()、 assoc_move_next_bucket()、assoc_insert()、assoc_delete(),對應於item的讀寫操作。其中 assoc_find()是根據key和key長尋找對應的item地址的函數(注意在C中,很多時候都是同時直接傳入字串和字串長度,而不是在函數 內部做strlen),返回的是item結構指標,它的資料地址在slab中的某個chunk上。 items.c是資料項目的操作程式,每一個完整的item包括幾個部分,在item_make_header()中定義為: key:鍵 nkey:鍵長 flags:使用者定義的flag(其實這個flag在memcached中沒有啟用) nbytes:值長(包括分行符號號\r\n) suffix:尾碼Buffer nsuffix:尾碼長 一個完整的item長度是鍵長+值長+尾碼長+item結構大小(32位元組),item操作就是根據這個長度來計算slab的classid的。 hashtable中的每一個桶上掛著一個雙鏈表,item_init()的時候已經初始化了heads、tails、sizes三個數組為0,這三個數 組的大小都為常量LARGEST_ID(預設為255,這個值需要配合factor來修改),在每次item_assoc()的時候,它會首先嘗試從 slab中擷取一塊閒置chunk,如果沒有可用的chunk,會在鏈表中掃描50次,以得到一個被LRU踢掉的item,將它unlink,然後將需 要插入的item插入鏈表中。 注意item的refcount成員。item被unlink之後只是從鏈表上摘掉,不是立刻就被free的,只是將它放到刪除隊列中(item_unlink_q()函數)。 item對應一些讀寫操作,包括remove、update、replace,當然最重要的就是alloc操作。 item還有一個特性就是它有到期時間,這是memcached的一個很有用的特性,很多應用都是依賴於memcached的item到期,比如 session儲存、操作鎖等。item_flush_expired()函數就是掃描表中的item,對到期的item執行unlink操作,當然這隻 是一個回收動作,實際上在get的時候還要進行時間判斷:

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的記憶體管理方式是非常精巧和高效的,它很大程度上減少了直接alloc系統記憶體的次數,降低函數開銷和記憶體片段產生幾率,雖然這種方式會造成一些冗餘浪費,但是這種浪費在大型系統應用中是微不足道的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.