
眾所周知,memcached并沒有像redis那樣提供了類似 keys * 的命令來獲取所有key,也沒有數(shù)據(jù)持久化,因此memcached想要dump所有的key,需要使用額外的命令組合或工具來實現(xiàn),但有時命令和工具……也不一定會滿足需求。
下面是我最近對找到的幾種方法進行的分析。
一、命令組合方式
我在git上發(fā)現(xiàn)如下工具:
https://github.com/gnomeby/memcached-itool
使用該工具的dumpkeys可以將memcached的key進行dump,查看其實現(xiàn)方式發(fā)現(xiàn)也是使用了 stats items、stats cachedump 等命令組合實現(xiàn)的,下面介紹下這種方式的實現(xiàn)及原理。
1. 原理實現(xiàn)
首先需要明白memcached的內(nèi)存管理方式:Slab Allocator
- memcached內(nèi)存分為多個chunk組,即多個slab;
- 每個組的chunk有不同的大小規(guī)格,如slab 1中chunk大小均為96B,slab 2中chunk大小均為120B,以此類推;
- key根據(jù)其大小分別分配到不同的slab組的chunk中存儲;
具體的內(nèi)存分配機制不再展開描述,有興趣的可以參考如下鏈接:
https://www.cnblogs.com/zhoujinyi/p/5554083.html
2. 具體操作
1)stats items、stats slabs命令獲取各slabs id以及slab的具體信息
stats items
STAT items:1:number 39 # slab中的key數(shù)量
STAT items:1:age 693911
STAT items:1:evicted 0
STAT items:1:evicted_nonzero 0
STAT items:1:evicted_time 0
STAT items:1:outofmemory 0
STAT items:1:tailrepairs 0
STAT items:1:reclaimed 7
STAT items:1:expired_unfetched 4
STAT items:1:evicted_unfetched 0
STAT items:1:crawler_reclaimed 0
STAT items:1:crawler_items_checked 0
STAT items:1:lrutail_reflocked 0
...
stats slabs
STAT 1:chunk_size 96 # slab中的chunk大小規(guī)格
STAT 1:chunks_per_page 10922
STAT 1:total_pages 1
STAT 1:total_chunks 10922
STAT 1:used_chunks 39
STAT 1:free_chunks 10883
STAT 1:free_chunks_end 0
STAT 1:mem_requested 3656
STAT 1:get_hits 3666
STAT 1:cmd_set 569
STAT 1:delete_hits 1
STAT 1:incr_hits 0
STAT 1:decr_hits 0
STAT 1:cas_hits 0
STAT 1:cas_badval 0
STAT 1:touch_hits 0
...
- STAT后的數(shù)字即slab的標識id
2)stats cachedump {slab_id} {limit_num} 獲取slab下的key信息
stats cachedump 1 5
ITEM seller_shop_im_phone_122 [1 b; 1513935640 s] # key名稱
ITEM seller_shop_im_phone_11542 [1 b; 1513935346 s]
ITEM user_third_userid_35543020 [2 b; 1516523664 s]
ITEM seller_shop_im_phone_12331 [1 b; 1513933986 s]
ITEM user_third_userid_70086126 [4 b; 1516517439 s]
END
- slab_id 即各slab組的標識id
- limit_num 即key的數(shù)量,0表示所有key
3. 問題缺陷
cachedump每次返回的數(shù)據(jù)只有2M;
而且在memcached源碼中是寫死的數(shù)值。
這個問題很嚴重。
網(wǎng)上并沒有找到相關(guān)源碼,于是我在官網(wǎng)下載了memcached-1.5.3的源碼并查找,發(fā)現(xiàn)確實如此:
##########
# 源碼位置:memcached-1.5.3/items.c
##########
char *item_cachedump(const unsigned int slabs_clsid, const unsigned int limit, unsigned int *bytes) {
unsigned int memlimit = 2 * 1024 * 1024; /* 2MB max response size */
char *buffer;
unsigned int bufcurr;
item *it;
unsigned int len;
unsigned int shown = 0;
char key_temp[KEY_MAX_LENGTH + 1];
char temp[512];
unsigned int id = slabs_clsid;
id |= COLD_LRU;
pthread_mutex_lock(&lru_locks[id]);
it = heads[id];
buffer = malloc((size_t)memlimit);
if (buffer == 0) {
return NULL;
}
bufcurr = 0;
while (it != NULL && (limit == 0 || shown < limit)) {
assert(it->nkey <= KEY_MAX_LENGTH);
if (it->nbytes == 0 && it->nkey == 0) {
it = it->next;
continue;
}
/* Copy the key since it may not be null-terminated in the struct */
strncpy(key_temp, ITEM_key(it), it->nkey);
key_temp[it->nkey] = 0x00; /* terminate */
len = snprintf(temp, sizeof(temp), "ITEM %s [%d b; %llu s]\r\n",
key_temp, it->nbytes - 2,
it->exptime == 0 ? 0 :
(unsigned long long)it->exptime + process_started);
if (bufcurr + len + 6 > memlimit) /* 6 is END\r\n\0 */
break;
memcpy(buffer + bufcurr, temp, len);
bufcurr += len;
shown++;
it = it->next;
}
memcpy(buffer + bufcurr, "END\r\n", 6);
bufcurr += 5;
*bytes = bufcurr;
pthread_mutex_unlock(&lru_locks[id]);
return buffer;
}
可以看到:
- 函數(shù)第一句定義了 memlimit 參數(shù)來限制大小為2M;
- 之后通過 malloc((size_t)memlimit) 申請了名為buffer的2M空間;
- 循環(huán)獲取slab中的item直到達到2M上限;
- 最后copy到buffer中并return;
由此可以明白,上述命令組合的方式雖然可以批量獲取key,但每個slab最大只能dump 2M,數(shù)據(jù)量超過2M則無法獲得所有的key。
當然,可以嘗試將源碼中的memlimit參數(shù)調(diào)大后重新編譯,但是這樣也沒法從根本上解決問題,因為不可能每次數(shù)據(jù)量超過后都重新編譯一次,而如果直接設置一個很大的值的話————cachedump會不會直接把memcached搞掛掉我也保不準啊!畢竟源碼中可是直接 malloc((size_t)memlimit) 一次申請了整個內(nèi)存的!
感興趣的大兄弟可以試一下,然后告訴我結(jié)果。??
二、libmemcached 工具
一提起libmemcached,我就一陣胃疼。不難搜到,網(wǎng)上很多文章介紹說它可以dump出memcached的所有key,因此我去官網(wǎng)看了下。
官網(wǎng)上介紹如下:
memdump dumps a list of “keys” from all servers that it is told to fetch from. Because memcached does not guarentee to provide all keys it is not possible to get a complete “dump”.
看上去是那么回事,由于沒有翻墻,百度也搜不到有效的內(nèi)容,于是我下載了libmemcached的源碼決定研究下。
經(jīng)過漫長的頭疼后,終于搞懂了memdump的核心部分代碼:
/*
We use this to dump all keys.
At this point we only support a callback method. This could be optimized by first
calling items and finding active slabs. For the moment though we just loop through
all slabs on servers and "grab" the keys.
*/
#include <libmemcached/common.h>
static memcached_return_t ascii_dump(Memcached *memc, memcached_dump_fn *callback, void *context, uint32_t number_of_callbacks)
{
/* MAX_NUMBER_OF_SLAB_CLASSES is defined to 200 in Memcached 1.4.10 */
for (uint32_t x= 0; x < 200; x++)
{
char buffer[MEMCACHED_DEFAULT_COMMAND_SIZE];
int buffer_length= snprintf(buffer, sizeof(buffer), "%u", x);
if (size_t(buffer_length) >= sizeof(buffer) or buffer_length < 0)
{
return memcached_set_error(*memc, MEMCACHED_MEMORY_ALLOCATION_FAILURE, MEMCACHED_AT,
memcached_literal_param("snprintf(MEMCACHED_DEFAULT_COMMAND_SIZE)"));
}
// @NOTE the hard coded zero means "no limit"
libmemcached_io_vector_st vector[]=
{
{ memcached_literal_param("stats cachedump ") },
{ buffer, size_t(buffer_length) },
{ memcached_literal_param(" 0\r\n") }
};
// Send message to all servers
...
...
// Collect the returned items
memcached_instance_st* instance;
memcached_return_t read_ret= MEMCACHED_SUCCESS;
while ((instance= memcached_io_get_readable_server(memc, read_ret)))
{
memcached_return_t response_rc= memcached_response(instance, buffer, MEMCACHED_DEFAULT_COMMAND_SIZE, NULL);
if (response_rc == MEMCACHED_ITEM)
{
char *string_ptr, *end_ptr;
string_ptr= buffer;
string_ptr+= 5; /* Move past ITEM */
for (end_ptr= string_ptr; isgraph(*end_ptr); end_ptr++) {} ;
char *key= string_ptr;
key[(size_t)(end_ptr-string_ptr)]= 0;
for (uint32_t callback_counter= 0; callback_counter < number_of_callbacks; callback_counter++)
{
memcached_return_t callback_rc= (*callback[callback_counter])(memc, key, (size_t)(end_ptr-string_ptr), context);
if (callback_rc != MEMCACHED_SUCCESS)
{
// @todo build up a message for the error from the value
memcached_set_error(*instance, callback_rc, MEMCACHED_AT);
break;
}
}
}
else if (response_rc == MEMCACHED_END)
{
// All items have been returned
}
else if ...
...
...
return memcached_has_current_error(*memc) ? MEMCACHED_SOME_ERRORS : MEMCACHED_SUCCESS;
}
代碼可能比較多,但整體還是比較清晰的:
- 開頭一個大for循環(huán)遍歷slab,在每個slab中進行操作;
- 先定義合適大小的變量來存放命令,下面定義vector[]存放完整的cachedump命令;
- 往下是一個while來循環(huán)接收返回結(jié)果,主要是在if (response_rc == MEMCACHED_ITEM)下的操作;
- 將string_ptr指針放在slab的頭部,然后跳過ITEM 字符(參考cachedump輸出內(nèi)容);
- 接下來for循環(huán)將end_ptr指針指向key名稱的尾端(參考百度:isgraph()函數(shù));
- 獲取key起始位置*key以及key長度end_ptr-string_ptr,通過callback結(jié)構(gòu)體獲取key名稱。
看到后面步驟的時候我是激動的,這是直接操作內(nèi)存獲取了所有key,厲害!但是我仔細一想,往上翻看了看——我去,這不還是用stats cachedump命令查的么!
為了避免因為我技術(shù)渣渣而誤導大家,我終于想起來去stackoverflow搜了下,果然比百度強得多,一搜就搜到:
呵呵,虧了源碼開頭注釋還寫著 We use this to dump all keys. 我開始胃疼了。
三、memcached-hack 補丁
memcached-hack是我無意中發(fā)現(xiàn)的一個補丁+工具的包,將源碼按補丁修改重新編譯后,可以實現(xiàn)cachedump時指定slab中的起始位置。大家可以到codegoogle上搜索下載。
有兩個版本的補丁和一個python寫的工具:
zhangxueyan?? PileWorld memcached-hack $ ll
total 56
-rwxr-xr-x@ 1 zhangxueyan staff 499 8 22 2008 example.py
-rwxr-xr-x@ 1 zhangxueyan staff 2543 8 22 2008 memcached-1.2.2-cachedump-hack
-rwxr-xr-x@ 1 zhangxueyan staff 4949 8 22 2008 memcached-1.2.4-cachedump-hack
-rwxr-xr-x@ 1 zhangxueyan staff 8510 8 22 2008 memcachem.py
python工具就不說了,用的也是打完補丁后的cachedump,下面以1.2.4版本的補丁為例說下核心部分的改動:
Index: items.c
===================================================================
--- items.c (revision 793)
+++ items.c (working copy)
@@ -276,18 +276,23 @@
}
/*@null@*/
-char *do_item_cachedump(const unsigned int slabs_clsid, const unsigned int limit, unsigned int *bytes) {
+char *do_item_cachedump(const unsigned int slabs_clsid, const unsigned int start, const unsigned int limit, unsigned int *bytes) {
unsigned int memlimit = 2 * 1024 * 1024; /* 2MB max response size */
char *buffer;
unsigned int bufcurr;
item *it;
+ int i;
unsigned int len;
unsigned int shown = 0;
char temp[512];
if (slabs_clsid > LARGEST_ID) return NULL;
it = heads[slabs_clsid];
-
+ i = 0;
+ while (it != NULL && i < start) {
+ it = it->next;
+ i++;
+ }
buffer = malloc((size_t)memlimit);
if (buffer == 0) return NULL;
bufcurr = 0;
- do_item_cachedump函數(shù)增加start參數(shù),用來指定slab中的起始位置;
- 添加while循環(huán),將指針 it 從默認的slab頭部指向用戶設定的start位置,然后進行讀取。
看起來這個方法確實可以真正dump到所有的key了,接下來我做了個簡單的測試。
官網(wǎng)下載了1.5.3的版本,參照補丁進行修改后編譯啟動,與原版的進行了對比:
改動后的1.5.3:
# slab1 下的所有key
stats cachedump 1 0
ITEM xue [9 b; 1514291787 s]s s
ITEM zhang [9 b; 1514291587 s]
ITEM runoob [9 b; 1514291526 s]
END
# slab1 下,從第二個key開始的所有key
stats cachedump 1 1 0
ITEM zhang [9 b; 1514291587 s]
ITEM runoob [9 b; 1514291526 s]
END
# slab1 下,從第三個key開始的所有key
stats cachedump 1 2 0
ITEM runoob [9 b; 1514291526 s]
END
對比未改動的 1.5.3 版本:
stats cachedump 1 0
ITEM runoob [9 b; 1514292460 s]
ITEM zhang [9 b; 1514292440 s]
ITEM xue [9 b; 1514292430 s]
END
stats cachedump 1 1 0
ITEM runoob [9 b; 1514292460 s]
END
stats cachedump 1 2 0
ITEM runoob [9 b; 1514292460 s]
ITEM zhang [9 b; 1514292440 s]
END
很明顯原生的版本把最后一個參數(shù)忽略了,還是按照 {slab_id} {limit_num} 的語法,而改動后的版本從這個例子上來看確實是實現(xiàn)了指定位置的功能。
四、結(jié)語
花了零零散散好幾天的時間來研究這個問題,終于可以告一段落,也算是有不少收獲,雖然沒有找到原生的memcached中直接dump全部的方法,但總還有個改動后的可以實現(xiàn)。
當然我們線上正使用著的目前看來是dump不能了,但是業(yè)務上目前還是有這個需求,如果大家有知道原生的方法的,歡迎隨時私信或評論中探討。