论——memcached真的有工具可以dump出所有key嘛?

memcached.png

众所周知,memcached并没有像redis那样提供了类似 keys * 的命令来获取所有key,也没有数据持久化,因此memcached想要dump所有的key,需要使用额外的命令组合或工具来实现,但有时命令和工具……也不一定会满足需求。

下面是我最近对找到的几种方法进行的分析。

一、命令组合方式

我在git上发现如下工具:
https://github.com/gnomeby/memcached-itool

使用该工具的dumpkeys可以将memcached的key进行dump,查看其实现方式发现也是使用了 stats itemsstats cachedump 等命令组合实现的,下面介绍下这种方式的实现及原理。

1. 原理实现

首先需要明白memcached的内存管理方式:Slab Allocator

  • memcached内存分为多个chunk组,即多个slab;
  • 每个组的chunk有不同的大小规格,如slab 1中chunk大小均为96B,slab 2中chunk大小均为120B,以此类推;
  • key根据其大小分别分配到不同的slab组的chunk中存储;

具体的内存分配机制不再展开描述,有兴趣的可以参考如下链接:
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数量
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大小规格
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后的数字即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的数量,0表示所有key

3. 问题缺陷

cachedump每次返回的数据只有2M;
而且在memcached源码中是写死的数值。

这个问题很严重。

网上并没有找到相关源码,于是我在官网下载了memcached-1.5.3的源码并查找,发现确实如此:

##########
# 源码位置: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;
}

可以看到:

  • 函数第一句定义了 memlimit 参数来限制大小为2M;
  • 之后通过 malloc((size_t)memlimit) 申请了名为buffer的2M空间;
  • 循环获取slab中的item直到达到2M上限;
  • 最后copy到buffer中并return;

由此可以明白,上述命令组合的方式虽然可以批量获取key,但每个slab最大只能dump 2M,数据量超过2M则无法获得所有的key

当然,可以尝试将源码中的memlimit参数调大后重新编译,但是这样也没法从根本上解决问题,因为不可能每次数据量超过后都重新编译一次,而如果直接设置一个很大的值的话————cachedump会不会直接把memcached搞挂掉我也保不准啊!毕竟源码中可是直接 malloc((size_t)memlimit) 一次申请了整个内存的!

感兴趣的大兄弟可以试一下,然后告诉我结果。😄

二、libmemcached 工具

一提起libmemcached,我就一阵胃疼。不难搜到,网上很多文章介绍说它可以dump出memcached的所有key,因此我去官网看了下。

官网上介绍如下:

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”.

看上去是那么回事,由于没有翻墙,百度也搜不到有效的内容,于是我下载了libmemcached的源码决定研究下。

经过漫长的头疼后,终于搞懂了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循环遍历slab,在每个slab中进行操作;
  • 先定义合适大小的变量来存放命令,下面定义vector[]存放完整的cachedump命令;
  • 往下是一个while来循环接收返回结果,主要是在if (response_rc == MEMCACHED_ITEM)下的操作;
  • 将string_ptr指针放在slab的头部,然后跳过ITEM 字符(参考cachedump输出内容);
  • 接下来for循环将end_ptr指针指向key名称的尾端(参考百度:isgraph()函数);
  • 获取key起始位置*key以及key长度end_ptr-string_ptr,通过callback结构体获取key名称。

看到后面步骤的时候我是激动的,这是直接操作内存获取了所有key,厉害!但是我仔细一想,往上翻看了看——我去,这不还是用stats cachedump命令查的么!

为了避免因为我技术渣渣而误导大家,我终于想起来去stackoverflow搜了下,果然比百度强得多,一搜就搜到:

https://stackoverflow.com/questions/41458274/what-does-the-one-page-per-slab-class-limitation-mean-when-dumping-keys-with-m

呵呵,亏了源码开头注释还写着 We use this to dump all keys. 我开始胃疼了。

三、memcached-hack 补丁

memcached-hack是我无意中发现的一个补丁+工具的包,将源码按补丁修改重新编译后,可以实现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函数增加start参数,用来指定slab中的起始位置;
  • 添加while循环,将指针 it 从默认的slab头部指向用户设定的start位置,然后进行读取。

看起来这个方法确实可以真正dump到所有的key了,接下来我做了个简单的测试。

官网下载了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

很明显原生的版本把最后一个参数忽略了,还是按照 {slab_id} {limit_num} 的语法,而改动后的版本从这个例子上来看确实是实现了指定位置的功能。

四、结语

花了零零散散好几天的时间来研究这个问题,终于可以告一段落,也算是有不少收获,虽然没有找到原生的memcached中直接dump全部的方法,但总还有个改动后的可以实现。

当然我们线上正使用着的目前看来是dump不能了,但是业务上目前还是有这个需求,如果大家有知道原生的方法的,欢迎随时私信或评论中探讨。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容