memcached深度分析
Memcached深度分析
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為準):
#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:
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:
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……:
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:
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的時候還要進行時間判斷:
/* 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;
}
}
}
}
/* 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系統記憶體的次數,降低函式開銷和記憶體碎片產生機率,雖然這種方式會造成一些冗餘浪費,但是這種浪費在大型系統應用中是微不足道的。
◎Memcached的理論引數計算方式
影響 memcached 工作的幾個引數有:
常量REALTIME_MAXDELTA 60*60*24*30
最大30天的過期時間
conn_init()中的freetotal(=200)
最大同時連線數
常量KEY_MAX_LENGTH 250
最大鍵長
settings.factor(=1.25)
factor將影響chunk的步進大小
settings.maxconns(=1024)
最大軟連線
settings.chunk_size(=48)
一個保守估計的key+value長度,用來生成id1中的chunk長度(1.2)。id1的chunk長度等於這個數值加上item結構體的長度(32),即預設的80位元組。
常量POWER_SMALLEST 1
最小classid(1.2)
常量POWER_LARGEST 200
最大classid(1.2)
常量POWER_BLOCK 1048576
預設slab大小
常量CHUNK_ALIGN_BYTES (sizeof(void *))
保證chunk大小是這個數值的整數倍,防止越界(void *的長度在不同系統上不一樣,在標準32位系統上是4)
常量ITEM_UPDATE_INTERVAL 60
佇列重新整理間隔
常量LARGEST_ID 255
最大item連結串列數(這個值不能比最大的classid小)
變數hashpower(在1.1中是常量HASHPOWER)
決定hashtable的大小
根據上面介紹的內容及引數設定,可以計算出的一些結果:
1、在memcached中可以儲存的item個數是沒有軟體上限的,之前我的100萬的說法是錯誤的。
2、假設NewHash演算法碰撞均勻,查詢item的迴圈次數是item總數除以hashtable大小(由hashpower決定),是線性的。
3、Memcached限制了可以接受的最大item是1MB,大於1MB的資料不予理會。
4、Memcached的空間利用率和資料特性有很大的關係,又與DONT_PREALLOC_SLABS常量有關。 在最差情況下,有198個slab會被浪費(所有item都集中在一個slab中,199個id全部分配滿)。
◎Memcached的定長優化
根據上面幾節的描述,多少對memcached有了一個比較深入的認識。在深入認識的基礎上才好對它進行優化。
Memcached本身是為變長資料設計的,根據資料特性,可以說它是“面向大眾”的設計,但是很多時候,我們的資料並不是這樣的“普遍”,典型的情況中,一種是非均勻分佈,即資料長度集中在幾個區域內(如儲存使用者 Session);另一種更極端的狀態是等長資料(如定長鍵值,定長資料,多見於訪問、線上統計或執行鎖)。
這裡主要研究一下定長資料的優化方案(1.2),集中分佈的變長資料僅供參考,實現起來也很容易。
解決定長資料,首先需要解決的是slab的分配問題,第一個需要確認的是我們不需要那麼多不同chunk長度的slab,為了最大限度地利用資源,最好chunk和item等長,所以首先要計算item長度。
在之前已經有了計算item長度的演算法,需要注意的是,除了字串長度外,還要加上item結構的長度32位元組。
假設我們已經計算出需要儲存200位元組的等長資料。
接下來是要修改slab的classid和chunk長度的關係。在原始版本中,chunk長度和classid是有對應關係的,現在如果把所有的chunk都定為200個位元組,那麼這個關係就不存在了,我們需要重新確定這二者的關係。一種方法是,整個儲存結構只使用一個固定的id,即只使用199個槽中的1個,在這種條件下,就一定要定義DONT_PREALLOC_SLABS來避免另外的預分配浪費。另一種方法是建立一個hash關係,來從item確定classid,不能使用長度來做鍵,可以使用key的NewHash結果等不定資料,或者直接根據key來做hash(定長資料的key也一定等長)。這裡簡單起見,選擇第一種方法,這種方法的不足之處在於只使用一個id,在資料量非常大的情況下,slab鏈會很長(因為所有資料都擠在一條鏈上了),遍歷起來的代價比較高。
前面介紹了三種空間冗餘,設定chunk長度等於item長度,解決了第一種空間浪費問題,不預申請空間解決了第二種空間浪費問題,那麼對於第一種問題(slab內剩餘)如何解決呢,這就需要修改POWER_BLOCK常量,使得每一個slab大小正好等於chunk長度的整數倍,這樣一個slab就可以正好劃分成n個chunk。這個數值應該比較接近1MB,過大的話同樣會造成冗餘,過小的話會造成次數過多的alloc,根據chunk長度為200,選擇1000000作為POWER_BLOCK的值,這樣一個slab就是100萬位元組,不是1048576。三個冗餘問題都解決了,空間利用率會大大提升。
修改 slabs_clsid 函式,讓它直接返回一個定值(比如 1 ):
unsigned int slabs_clsid(size_t size) {
return 1;
}
修改slabs_init函式,去掉迴圈建立所有classid屬性的部分,直接新增slabclass[1]:
slabclass[1].size = 200; //每chunk200位元組
slabclass[1].perslab = 5000; //1000000/200
◎Memcached客戶端
Memcached是一個服務程式,使用的時候可以根據它的協議,連線到memcached伺服器上,傳送命令給服務程式,就可以操作上面的資料。為了方便使用,memcached有很多個客戶端程式可以使用,對應於各種語言,有各種語言的客戶端。基於C語言的有libmemcache、APR_Memcache;基於Perl的有Cache::Memcached;另外還有Python、Ruby、Java、C#等語言的支援。PHP的客戶端是最多的,不光有mcache和PECL memcache兩個擴充套件,還有大把的由PHP編寫的封裝類,下面介紹一下在PHP中使用memcached的方法:
mcache擴充套件是基於libmemcache再封裝的。libmemcache一直沒有釋出stable版本,目前版本是1.4.0-rc2,可以在這裡找到。libmemcache有一個很不好的特性,就是會向stderr寫很多錯誤資訊,一般的,作為lib使用的時候,stderr一般都會被定向到其它地方,比如Apache的錯誤日誌,而且libmemcache會自殺,可能會導致異常,不過它的效能還是很好的。
mcache擴充套件最後更新到1.2.0-beta10,作者大概是離職了,不光停止更新,連網站也打不開了(~_~),只能到其它地方去獲取這個不負責的擴充套件了。解壓後安裝方法如常:phpize & configure & make & make install,一定要先安裝libmemcache。使用這個擴充套件很簡單:
<?php
$mc = memcache(); // 建立一個memcache連線物件,注意這裡不是用new!
$mc->add_server('localhost', 11211); // 新增一個服務程式
$mc->add_server('localhost', 11212); // 新增第二個服務程式
$mc->set('key1', 'Hello'); // 寫入key1 => Hello
$mc->set('key2', 'World', 10); // 寫入key2 => World,10秒過期
$mc->set('arr1', array('Hello', 'World')); // 寫入一個陣列
$key1 = $mc->get('key1'); // 獲取'key1'的值,賦給$key1
$key2 = $mc->get('key2'); // 獲取'key2'的值,賦給$key2,如果超過10秒,就取不到了
$arr1 = $mc->get('arr1'); // 獲取'arr1'陣列
$mc->delete('arr1'); // 刪除'arr1'
$mc->flush_all(); // 刪掉所有資料
$stats = $mc->stats(); // 獲取伺服器資訊
var_dump($stats); // 伺服器資訊是一個陣列
?>
這個擴充套件的好處是可以很方便地實現分散式儲存和負載均衡,因為它可以新增多個服務地址,資料在儲存的時候是會根據hash結果定位到某臺伺服器上的,這也是libmemcache的特性。libmemcache支援集中hash方式,包括CRC32、ELF和Perl hash。
PECL memcache是PECL釋出的擴充套件,目前最新版本是2.1.0,可以在pecl網站得到。memcache擴充套件的使用方法可以在新一些的PHP手冊中找到,它和mcache很像,真的很像:
<?php
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");
$version = $memcache->getVersion();
echo "Server's version: ".$version."n";
$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;
$memcache->set('key', $tmp_object, false, 10) or die ("Failed to save data at the server");
echo "Store data in the cache (data will expire in 10 seconds)n";
$get_result = $memcache->get('key');
echo "Data from the cache:n";
var_dump($get_result);
?>
這個擴充套件是使用php的stream直接連線memcached伺服器並通過socket傳送命令的。它不像libmemcache那樣完善,也不支援add_server這種分佈操作,但是因為它不依賴其它的外界程式,相容性要好一些,也比較穩定。至於效率,差別不是很大。
另外,有很多的PHP class可以使用,比如MemcacheClient.inc.php,phpclasses.org上可以找到很多,一般都是對perl client API的再封裝,使用方式很像。
◎BSM_Memcache
從C client來說,APR_Memcache是一個很成熟很穩定的client程式,支援執行緒鎖和原子級操作,保證執行的穩定性。不過它是基於APR的(APR將在最後一節介紹),沒有libmemcache的應用範圍廣,目前也沒有很多基於它開發的程式,現有的多是一些Apache Module,因為它不能脫離APR環境執行。但是APR倒是可以脫離Apache單獨安裝的,在APR網站上可以下載APR和APR-util,不需要有Apache,可以直接安裝,而且它是跨平臺的。
BSM_Memcache是我在BS.Magic專案中開發的一個基於APR_Memcache的PHP擴充套件,說起來有點拗口,至少它把APR扯進了PHP擴充套件中。這個程式很簡單,也沒做太多的功能,只是一種形式的嘗試,它支援伺服器分組。
和mcache擴充套件支援多伺服器分佈儲存不同,BSM_Memcache支援多組伺服器,每一組內的伺服器還是按照hash方式來分佈儲存資料,但是兩個組中儲存的資料是一樣的,也就是實現了熱備,它不會因為一臺伺服器發生單點故障導致資料無法獲取,除非所有的伺服器組都損壞(例如機房停電)。當然實現這個功能的代價就是效能上的犧牲,在每次新增刪除資料的時候都要掃描所有的組,在get資料的時候會隨機選擇一組伺服器開始輪詢,一直到找到資料為止,正常情況下一次就可以獲取得到。
BSM_Memcache只支援這幾個函式:
zend_function_entry bsm_memcache_functions[] =
{
PHP_FE(mc_get, NULL)
PHP_FE(mc_set, NULL)
PHP_FE(mc_del, NULL)
PHP_FE(mc_add_group, NULL)
PHP_FE(mc_add_server, NULL)
PHP_FE(mc_shutdown, NULL)
{NULL, NULL, NULL}
};
mc_add_group函式返回一個整形(其實應該是一個object,我偷懶了~_~)作為組ID,mc_add_server的時候要提供兩個引數,一個是組ID,一個是伺服器地址(ADDR : PORT)。
/**
* Add a server group
*/
PHP_FUNCTION(mc_add_group)
{
apr_int32_t group_id;
apr_status_t rv;
if (0 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
RETURN_NULL();
}
group_id = free_group_id();
if (-1 == group_id)
{
RETURN_FALSE;
}
apr_memcache_t *mc;
rv = apr_memcache_create(p, MAX_G_SERVER, 0, &mc);
add_group(group_id, mc);
RETURN_DOUBLE(group_id);
}
/**
* Add a server into group
*/
PHP_FUNCTION(mc_add_server)
{
apr_status_t rv;
apr_int32_t group_id;
double g;
char *srv_str;
int srv_str_l;
if (2 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ds", &g, &srv_str, &srv_str_l) == FAILURE)
{
RETURN_FALSE;
}
group_id = (apr_int32_t) g;
if (-1 == is_validate_group(group_id))
{
RETURN_FALSE;
}
char *host, *scope;
apr_port_t port;
rv = apr_parse_addr_port(&host, &scope, &port, srv_str, p);
if (APR_SUCCESS == rv)
{
// Create this server object
apr_memcache_server_t *st;
rv = apr_memcache_server_create(p, host, port, 0, 64, 1024, 600, &st);
if (APR_SUCCESS == rv)
{
if (NULL == mc_groups[group_id])
{
RETURN_FALSE;
}
// Add server
rv = apr_memcache_add_server(mc_groups[group_id], st);
if (APR_SUCCESS == rv)
{
RETURN_TRUE;
}
}
}
RETURN_FALSE;
}
在set和del資料的時候,要迴圈所有的組:
/**
* Store item into all groups
*/
PHP_FUNCTION(mc_set)
{
char *key, *value;
int key_l, value_l;
double ttl = 0;
double set_ct = 0;
if (2 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss|d", &key, &key_l, &value, &value_l, ttl) == FAILURE)
{
RETURN_FALSE;
}
// Write data into every object
apr_int32_t i = 0;
if (ttl < 0)
{
ttl = 0;
}
apr_status_t rv;
for (i = 0; i < MAX_GROUP; i++)
{
if (0 == is_validate_group(i))
{
// Write it!
rv = apr_memcache_add(mc_groups[i], key, value, value_l, (apr_uint32_t) ttl, 0);
if (APR_SUCCESS == rv)
{
set_ct++;
}
}
}
RETURN_DOUBLE(set_ct);
}
在mc_get中,首先要隨機選擇一個組,然後從這個組開始輪詢:
/**
* Fetch a item from a random group
*/
PHP_FUNCTION(mc_get)
{
char *key, *value = NULL;
int key_l;
apr_size_t value_l;
if (1 != ZEND_NUM_ARGS())
{
WRONG_PARAM_COUNT;
}
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &key, &key_l) == FAILURE)
{
RETURN_MULL();
}
// I will try ...
// Random read
apr_int32_t curr_group_id = random_group();
apr_int32_t i = 0;
apr_int32_t try = 0;
apr_uint32_t flag;
apr_memcache_t *oper;
apr_status_t rv;
for (i = 0; i < MAX_GROUP; i++)
{
try = i + curr_group_id;
try = try % MAX_GROUP;
if (0 == is_validate_group(try))
{
// Get a value
oper = mc_groups[try];
rv = apr_memcache_getp(mc_groups[try], p, (const char *) key, &value, &value_l, 0);
if (APR_SUCCESS == rv)
{
RETURN_STRING(value, 1);
}
}
}
RETURN_FALSE;
}
/**
* Random group id
* For mc_get()
*/
apr_int32_t random_group()
{
struct timeval tv;
struct timezone tz;
int usec;
gettimeofday(&tv, &tz);
usec = tv.tv_usec;
int curr = usec % count_group();
return (apr_int32_t) curr;
}
BSM_Memcache的使用方式和其它的client類似:
<?php
$g1 = mc_add_group(); // 新增第一個組
$g2 = mc_add_group(); // 新增第二個組
mc_add_server($g1, 'localhost:11211'); // 在第一個組中新增第一臺伺服器
mc_add_server($g1, 'localhost:11212'); // 在第一個組中新增第二臺伺服器
mc_add_server($g2, '10.0.0.16:11211'); // 在第二個組中新增第一臺伺服器
mc_add_server($g2, '10.0.0.17:11211'); // 在第二個組中新增第二臺伺服器
mc_set('key', 'Hello'); // 寫入資料
$key = mc_get('key'); // 讀出資料
mc_del('key'); // 刪除資料
mc_shutdown(); // 關閉所有組
?>
APR_Memcache的相關資料可以在這裡找到,BSM_Memcache可以在網路上找下載。
◎APR環境介紹
APR的全稱:Apache Portable Runtime。它是Apache軟體基金會建立並維持的一套跨平臺的C語言庫。它從Apache httpd1.x中抽取出來並獨立於httpd之外,Apache httpd2.x就是建立在APR上。APR提供了很多方便的API介面可供使用,包括如記憶體池、字串操作、網路、陣列、hash表等實用的功能。開發Apache2 Module要接觸很多APR函式,當然APR可以獨立安裝獨立使用,可以用來寫自己的應用程式,不一定是Apache httpd的相關開發。
◎後記
這是我在農曆丙戌年(我的本命年)的最後一篇文章,由於Memcached的內涵很多,倉促整理一定有很多遺漏和錯誤。感謝新浪網提供的研究機會,感謝部門同事的幫助。
相關文章
- Memcached記憶體管理原始碼分析記憶體原始碼
- memcached+php環境配置和分析PHP
- Memcached 及 Redis 架構分析和比較Redis架構
- Memcached
- HashMap深度分析HashMap
- Memcached筆記——(三)Memcached使用總結筆記
- Toast原始碼深度分析AST原始碼
- Memcached 協議協議
- Memcached 原理剖析
- 安裝 Memcached
- memcached 安裝
- redis和memcachedRedis
- Memcached入門
- memcached安裝
- memcached原始碼分析-----雜湊表基本操作以及擴容過程原始碼
- Kong mesh深度分析報告
- 原神深度分析報告(上)
- 劉星:HBase效能深度分析
- 深度分析ORACLE中的塊Oracle
- php開發memcachedPHP
- PHP memcached,(set,get)PHP
- Ubuntu 安裝 MemcachedUbuntu
- ubuntu安裝memcachedUbuntu
- Memcached::cas 詳解
- memcached 安裝配置
- memcached全面剖析--2.理解memcached的記憶體儲存記憶體
- 實戰Memcached快取系統(4)Memcached的CAS協議快取協議
- Redis GEO & 實現原理深度分析Redis
- 深度: 資料分析3步走
- PHP memcached 利用遠端終端連線、操作memcached伺服器PHP伺服器
- 實戰Memcached快取系統(5)Memcached的CAS程式例項快取
- 實戰Memcached快取系統(3)Memcached配置引數初解快取
- memcached安裝及.NET中的Memcached.ClientLibrary使用詳解client
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- [轉]memcached學習系列
- 【Redis與Memcached比較】Redis
- check memcached process and restart if downREST
- Redis 和 Memcached 比較Redis