memcached源码分析-----item过期失效处理以及LRU爬虫

来源:互联网 发布:nginx 第三方模块开发 编辑:程序博客网 时间:2024/06/09 12:24

转载:http://blog.csdn.net/luotuo44/article/details/42963793

   温馨提示:本文用到了一些可以在启动memcached设置的全局变量。关于这些全局变量的含义可以参考《memcached启动参数详解》。对于这些全局变量,处理方式就像《如何阅读memcached源代码》所说的那样直接取其默认值。另外, 本文会提及LRU队列,关于LRU队列的介绍可以参考《LRU队列与item结构体》。



过期失效处理:

        一个item在两种情况下会过期失效:1.item的exptime时间戳到了。2.用户使用flush_all命令将全部item变成过期失效的。读者可能会说touch命令也可以使得一个item过期失效,其实这也属于前面说的第一种情况。


超时失效:

        对于第一种过期失效,memcached的使用懒惰处理:不主动检测一个item是否过期失效。当worker线程访问这个item时,才检测这个item的exptime时间戳是否到了。比较简单,这里就先不贴代码,后面会贴。


flush_all命令:

        第二种过期失效是用户flush_all命令设置的。flush_all会将所有item都变成过期失效。所有item是指哪些item?因为多个客户端会不断地往memcached插入item,所以必须要明白所有item是指哪些。是以worker线程接收到这个命令那一刻为界?还是以删除那一刻为界?

        当worker线程接收到flush_all命令后,会用全局变量settings的oldest_live成员存储接收到这个命令那一刻的时间(准确地说,是worker线程解析得知这是一个flush_all命令那一刻再减一),代码为settings.oldest_live =current_time - 1;然后调用item_flush_expired函数锁上cache_lock,然后调用do_item_flush_expired函数完成工作。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void do_item_flush_expired(void) {  
  2.     int i;  
  3.     item *iter, *next;  
  4.     if (settings.oldest_live == 0)  
  5.         return;  
  6.     for (i = 0; i < LARGEST_ID; i++) {  
  7.         for (iter = heads[i]; iter != NULL; iter = next) {  
  8.             if (iter->time != 0 && iter->time >= settings.oldest_live) {  
  9.                 next = iter->next;  
  10.                 if ((iter->it_flags & ITEM_SLABBED) == 0) {  
  11.                     do_item_unlink_nolock(iter, hash(ITEM_key(iter), iter->nkey));  
  12.                 }  
  13.             } else {  
  14.                 /* We've hit the first old item. Continue to the next queue. */  
  15.                 break;  
  16.             }  
  17.         }  
  18.     }  
  19. }  

        do_item_flush_expired函数内部会遍历所有LRU队列,检测每一个item的time成员。检测time成员是合理的。如果time成员小于settings.oldest_live就说明该item在worker线程接收到flush_all命令的时候就已经存在了(time成员表示该item的最后一次访问时间)。那么就该删除这个item。

        这样看来memcached是以worker线程接收到flush_all命令那一刻为界的。等等等等,看清楚一点!!在do_item_flush_expired函数里面,不是当item的time成员小于settings.oldest_live时删除这个item,而是大于的时候才删除。从time成员变量的意义来说,大于代表什么啊?有大于的吗?奇怪!@#@&¥


        实际上memcached是以删除那一刻为界的。那settings.oldest_live为什么要存储worker线程接收到flush_all命令的时间戳?为什么又要判断iter->time是否大于settings.oldest_live呢?

        按照一般的做法,在do_item_flush_expired函数中直接把哈希表和LRU上的所有item统统删除即可。这样确实是可以达到目标。但在本worker线程处理期间,其他worker线程完全不能工作(因为do_item_flush_expired的调用者已经锁上了cache_lock)。而LRU队列里面可能有大量的数据,这个过期处理过程可能会很长。其他worker线程完全不能工作是难于接受的。

        memcached的作者肯定也意识到这个问题,所以他就写了一个奇怪的do_item_flush_expired函数,用来加速。do_item_flush_expired只会删除少量特殊的item。如何特殊法,在后面代码注释中会解释。对于其他大量的item,memcached采用懒惰方式处理。只有当worker线程试图访问该item,才检测item是否已经被设置为过期的了。事实上,无需对item进行任何设置就能检测该item是否为过期的,通过settings.oldest_live变量即可。这种懒惰和前面第一种item过期失效的处理是一样的。


        现在再看一下do_item_flush_expired函数,看一下特殊的item。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void do_item_flush_expired(void) {  
  2.     int i;  
  3.     item *iter, *next;  
  4.     if (settings.oldest_live == 0)  
  5.         return;  
  6.     for (i = 0; i < LARGEST_ID; i++) {  
  7.         for (iter = heads[i]; iter != NULL; iter = next) {  
  8.             //iter->time == 0的是lru爬虫item,直接忽略  
  9.             //一般情况下iter->time是小于settings.oldest_live的。但在这种情况下  
  10.             //就有可能出现iter->time >= settings.oldest_live :  worker1接收到  
  11.             //flush_all命令,并给settings.oldest_live赋值为current_time-1。  
  12.             //worker1线程还没来得及调用item_flush_expired函数,就被worker2  
  13.             //抢占了cpu,然后worker2往lru队列插入了一个item。这个item的time  
  14.             //成员就会满足iter->time >= settings.oldest_live  
  15.             if (iter->time != 0 && iter->time >= settings.oldest_live) {  
  16.                 next = iter->next;  
  17.                 if ((iter->it_flags & ITEM_SLABBED) == 0) {  
  18.                     //虽然调用的是nolock,但本函数的调用者item_flush_expired  
  19.                     //已经锁上了cache_lock,才调用本函数的  
  20.                     do_item_unlink_nolock(iter, hash(ITEM_key(iter), iter->nkey));  
  21.                 }  
  22.             } else {  
  23.                 //因为lru队列里面的item是根据time降序排序的,所以当存在一个item的time成员  
  24.                 //小于settings.oldest_live,剩下的item都不需要再比较了  
  25.                 break;  
  26.             }  
  27.         }  
  28.     }  
  29. }  


懒惰删除:

        现在阅读item的懒惰删除。注意代码中的注释。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {  
  2.     item *it = assoc_find(key, nkey, hv);  
  3.     ...  
  4.   
  5.     if (it != NULL) {  
  6.         //settings.oldest_live初始化值为0  
  7.         //检测用户是否使用过flush_all命令,删除所有item。  
  8.         //it->time <= settings.oldest_live就说明用户在使用flush_all命令的时候  
  9.         //就已经存在该item了。那么该item是要删除的。  
  10.         //flush_all命令可以有参数,用来设定在未来的某个时刻把所有的item都设置  
  11.         //为过期失效,此时settings.oldest_live是一个比worker接收到flush_all  
  12.         //命令的那一刻大的时间,所以要判断settings.oldest_live <= current_time  
  13.         if (settings.oldest_live != 0 && settings.oldest_live <= current_time &&  
  14.             it->time <= settings.oldest_live) {  
  15.             do_item_unlink(it, hv);  
  16.             do_item_remove(it);  
  17.             it = NULL;  
  18.    
  19.         } else if (it->exptime != 0 && it->exptime <= current_time) {//该item已经过期失效了  
  20.             do_item_unlink(it, hv);//引用数会减一  
  21.             do_item_remove(it);//引用数减一,如果引用数等于0,就删除  
  22.             it = NULL;  
  23.   
  24.         } else {  
  25.             it->it_flags |= ITEM_FETCHED;  
  26.         }  
  27.     }  
  28.   
  29.   
  30.     return it;  
  31. }  

        可以看到,在查找到一个item后就要检测它是否过期失效了。失效了就要删除之。

        除了do_item_get函数外,do_item_alloc函数也是会处理过期失效item的。do_item_alloc函数不是删除这个过期失效item,而是占为己用。因为这个函数的功能是申请一个item,如果一个item过期了那么就直接霸占这个item的那块内存。下面看一下代码。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. item *do_item_alloc(char *key, const size_t nkey, const int flags,  
  2.                     const rel_time_t exptime, const int nbytes,  
  3.                     const uint32_t cur_hv) {  
  4.     uint8_t nsuffix;  
  5.     item *it = NULL;  
  6.     char suffix[40];  
  7.     //要存储这个item需要的总空间  
  8.     size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);  
  9.     if (settings.use_cas) {  
  10.         ntotal += sizeof(uint64_t);  
  11.     }  
  12.   
  13.     //根据大小判断从属于哪个slab  
  14.     unsigned int id = slabs_clsid(ntotal);  
  15.   
  16.     /* do a quick check if we have any expired items in the tail.. */  
  17.     int tries = 5;  
  18.     item *search;  
  19.     item *next_it;  
  20.     rel_time_t oldest_live = settings.oldest_live;  
  21.   
  22.     search = tails[id];  
  23.     for (; tries > 0 && search != NULL; tries--, search=next_it) {  
  24.         next_it = search->prev;  
  25.         ...  
  26.   
  27.         if (refcount_incr(&search->refcount) != 2) {//引用数,还有其他线程在引用,不能霸占这个item  
  28.             //刷新这个item的访问时间以及在LRU队列中的位置  
  29.             do_item_update_nolock(search);  
  30.             tries++;  
  31.             refcount_decr(&search->refcount);  
  32.             //此时引用数>=2  
  33.       
  34.             continue;  
  35.         }  
  36.   
  37.         //search指向的item的refcount等于2,这说明此时这个item除了本worker  
  38.         //线程外,没有其他任何worker线程索引其。可以放心释放并重用这个item  
  39.           
  40.          //因为这个循环是从lru链表的后面开始遍历的。所以一开始search就指向  
  41.          //了最不常用的item,如果这个item都没有过期。那么其他的比其更常用  
  42.         //的item就不要删除了(即使它们过期了)。此时只能向slabs申请内存  
  43.         if ((search->exptime != 0 && search->exptime < current_time)  
  44.             || (search->time <= oldest_live && oldest_live <= current_time)) {  
  45.             //search指向的item是一个过期失效的item,可以使用之  
  46.             it = search;  
  47.             //重新计算一下这个slabclass_t分配出去的内存大小  
  48.             //直接霸占旧的item就需要重新计算  
  49.             slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);  
  50.             do_item_unlink_nolock(it, hv);//从哈希表和lru链表中删除  
  51.             /* Initialize the item block: */  
  52.             it->slabs_clsid = 0;  
  53.         }  
  54.           
  55.   
  56.         //引用计数减一。此时该item已经没有任何worker线程索引其,并且哈希表也  
  57.         //不再索引其  
  58.         refcount_decr(&search->refcount);  
  59.         break;  
  60.     }  
  61.   
  62.     ...  
  63.     return it;  
  64. }  
  65.   
  66.   
  67. //新的item直接霸占旧的item就会调用这个函数  
  68. void slabs_adjust_mem_requested(unsigned int id, size_t old, size_t ntotal)  
  69. {  
  70.     pthread_mutex_lock(&slabs_lock);  
  71.     slabclass_t *p;  
  72.     if (id < POWER_SMALLEST || id > power_largest) {  
  73.         fprintf(stderr, "Internal error! Invalid slab class\n");  
  74.         abort();  
  75.     }  
  76.   
  77.     p = &slabclass[id];  
  78.     //重新计算一下这个slabclass_t分配出去的内存大小  
  79.     p->requested = p->requested - old + ntotal;  
  80.     pthread_mutex_unlock(&slabs_lock);  
  81. }  

        flush_all命令是可以有时间参数的。这个时间和其他时间一样取值范围是 1到REALTIME_MAXDELTA(30天)。如果命令为flush_all 100,那么99秒后所有的item失效。此时settings.oldest_live的值为current_time+100-1,do_item_flush_expired函数也没有什么用了(总不会被抢占CPU99秒吧)。也正是这个原因,需要在do_item_get里面,加入settings.oldest_live<= current_time这个判断,防止过早删除了item。

        这里明显有一个bug。假如客户端A向服务器提交了flush_all10命令。过了5秒后,客户端B向服务器提交命令flush_all100。那么客户端A的命令将失效,没有起到任何作用。



LRU爬虫:

        前面说到,memcached是懒惰删除过期失效item的。所以即使用户在客户端使用了flush_all命令使得全部item都过期失效了,但这些item还是占据者哈希表和LRU队列并没有归还给slab分配器。


LRU爬虫线程:

        有没有办法强制清除这些过期失效的item,不再占据哈希表和LRU队列的空间并归还给slabs呢?当然是有的。memcached提供了LRU爬虫可以实现这个功能。

        要使用LRU爬虫就必须在客户端使用lru_crawler命令。memcached服务器根据具体的命令参数进行处理。

        memcached是用一个专门的线程负责清除这些过期失效item的,本文将称这个线程为LRU爬虫线程。默认情况下memcached是不启动这个线程的,但可以在启动memcached的时候添加参数-o lru_crawler启动这个线程。也可以通过客户端命令启动。即使启动了这个LRU爬虫线程,该线程还是不会工作。需要另外发送命令,指明要对哪个LRU队列进行清除处理。现在看一下lru_crawler有哪些参数。



LRU爬虫命令:


  • lru_crawler  <enable|disable>  启动或者停止一个LRU爬虫线程。任何时刻,最多只有一个LRU爬虫线程。该命令对settings.lru_crawler进行赋值为true或者false
  • lru_crawler crawl <classid,classid,classid|all>  可以使用2,3,6这样的列表指明要对哪个LRU队列进行清除处理。也可以使用all对所有的LRU队列进行处理
  • lru_crawler sleep <microseconds>  LRU爬虫线程在清除item的时候会占用锁,会妨碍worker线程的正常业务。所以LRU爬虫在处理的时候需要时不时休眠一下。默认休眠时间为100微秒。该命令对settings.lru_crawler_sleep进行赋值
  • lru_crawler tocrawl <32u>  一个LRU队列可能会有很多过期失效的item。如果一直检查和清除下去,势必会妨碍worker线程的正常业务。这个参数用来指明最多只检查每一条LRU队列的多少个item。默认值为0,所以如果不指定那么就不会工作。该命令对settings.lru_crawler_tocrawl进行赋值

        如果要启动LRU爬虫主动删除过期的item,需要这样做:首先使用lru_crawler enable命令启动一个LRU爬虫线程。然后使用lru_crawler tocrawl num命令确定每一个LRU队列最多检查num-1个item。最后使用命令lru_crawler crawl <classid,classid,classid|all> 指定要处理的LRU队列。lru_crawler sleep可以不设置,如果要设置那么可以在lru_crawler crawl命令之前设置即可。


启动LRU爬虫线程:

        现在来看一下LRU爬虫是怎么工作的。先来看一下memcached为LRU爬虫定义了哪些全局变量。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. static volatile int do_run_lru_crawler_thread = 0;  
  2. static int lru_crawler_initialized = 0;  
  3. static pthread_mutex_t lru_crawler_lock = PTHREAD_MUTEX_INITIALIZER;  
  4. static pthread_cond_t  lru_crawler_cond = PTHREAD_COND_INITIALIZER;  
  5.   
  6.   
  7. int init_lru_crawler(void) {//main函数会调用该函数  
  8.     if (lru_crawler_initialized == 0) {  
  9.         if (pthread_cond_init(&lru_crawler_cond, NULL) != 0) {  
  10.             fprintf(stderr, "Can't initialize lru crawler condition\n");  
  11.             return -1;  
  12.         }  
  13.         pthread_mutex_init(&lru_crawler_lock, NULL);  
  14.         lru_crawler_initialized = 1;  
  15.     }  
  16.     return 0;  
  17. }  

        代码比较简单,这里就不说了。下面看一下lru_crawler enable和disable命令。enable命令会启动一个LRU爬虫线程,而disable会停止这个LRU爬虫线程,当然不是直接调用pthread_exit停止线程。pthread_exit函数是一个危险函数,不应该在代码出现。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. static pthread_t item_crawler_tid;  
  2.   
  3. //worker线程接收到"lru_crawler enable"命令后会调用本函数  
  4. //启动memcached时如果有-o lru_crawler参数也是会调用本函数  
  5. int start_item_crawler_thread(void) {  
  6.     int ret;  
  7.   
  8.     //在stop_item_crawler_thread函数可以看到pthread_join函数  
  9.     //在pthread_join返回后,才会把settings.lru_crawler设置为false。  
  10.     //所以不会出现同时出现两个crawler线程  
  11.     if (settings.lru_crawler)  
  12.         return -1;  
  13.       
  14.     pthread_mutex_lock(&lru_crawler_lock);  
  15.     do_run_lru_crawler_thread = 1;  
  16.     settings.lru_crawler = true;  
  17.     //创建一个LRU爬虫线程,线程函数为item_crawler_thread。LRU爬虫线程在进入  
  18.     //item_crawler_thread函数后,会调用pthread_cond_wait,等待worker线程指定  
  19.     //要处理的LRU队列  
  20.     if ((ret = pthread_create(&item_crawler_tid, NULL,  
  21.         item_crawler_thread, NULL)) != 0) {  
  22.         fprintf(stderr, "Can't create LRU crawler thread: %s\n",  
  23.             strerror(ret));  
  24.         pthread_mutex_unlock(&lru_crawler_lock);  
  25.         return -1;  
  26.     }  
  27.     pthread_mutex_unlock(&lru_crawler_lock);  
  28.   
  29.     return 0;  
  30. }  
  31.   
  32.   
  33. //worker线程在接收到"lru_crawler disable"命令会执行这个函数  
  34. int stop_item_crawler_thread(void) {  
  35.     int ret;  
  36.     pthread_mutex_lock(&lru_crawler_lock);  
  37.     do_run_lru_crawler_thread = 0;//停止LRU线程  
  38.     //LRU爬虫线程可能休眠于等待条件变量,需要唤醒才能停止LRU爬虫线程  
  39.     pthread_cond_signal(&lru_crawler_cond);  
  40.     pthread_mutex_unlock(&lru_crawler_lock);  
  41.     if ((ret = pthread_join(item_crawler_tid, NULL)) != 0) {  
  42.         fprintf(stderr, "Failed to stop LRU crawler thread: %s\n", strerror(ret));  
  43.         return -1;  
  44.     }  
  45.     settings.lru_crawler = false;  
  46.     return 0;  
  47. }  

        可以看到worker线程在接收到” lru_crawler enable”命令后会启动一个LRU爬虫线程。这个LRU爬虫线程还没去执行任务,因为还没有指定任务。命令"lru_crawler tocrawl num"并不是启动一个任务。对于这个命令,worker线程只是简单地把settings.lru_crawler_tocrawl赋值为num。


清除失效item:

        命令”lru_crawler crawl<classid,classid,classid|all>”才是指定任务的。该命令指明了要对哪个LRU队列进行清理。如果使用all那么就是对所有的LRU队列进行清理。

        在看memcached的清理代码之前,先考虑一个问题:怎么对一条LRU队列进行清理?

        最直观的做法是先加锁(锁上cache_lock),然后遍历一整条LRU队列。直接判断LRU队列里面的每一个item即可。明显这种方法有问题。如果memcached有大量的item,那么遍历一个LRU队列耗时将太久。这样会妨碍worker线程的正常业务。当然我们可以考虑使用分而治之的方法,每次只处理几个item,多次进行,最终达到处理整个LRU队列的目标。但LRU队列是一个链表,不支持随机访问。处理队列中间的某个item,需要从链表头或者尾依次访问,时间复杂度还是O(n)。


伪item:

        memcached为了实现随机访问,使用了一个很巧妙的方法。它在LRU队列尾部插入一个伪item,然后驱动这个伪item向队列头部前进,每次前进一位。

        这个伪item是全局变量,LRU爬虫线程无须从LRU队列头部或者尾部遍历就可以直接访问这个伪item。通过这个伪item的next和prev指针,就可以访问真正的item。于是,LRU爬虫线程无需遍历就可以直接访问LRU队列中间的某一个item。

        下面看一下lru_crawler_crawl函数,memcached会在这个函数会把伪item插入到LRU队列尾部的。当worker线程接收到lru_crawler crawl<classid,classid,classid|all>命令时就会调用这个函数。因为用户可能要求LRU爬虫线程清理多个LRU队列的过期失效item,所以需要一个伪item数组。伪item数组的大小等于LRU队列的个数,它们是一一对应的。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //这个结构体和item结构体长得很像,是伪item结构体,用于LRU爬虫  
  2. typedef struct {  
  3.     struct _stritem *next;  
  4.     struct _stritem *prev;  
  5.     struct _stritem *h_next;    /* hash chain next */  
  6.     rel_time_t      time;       /* least recent access */  
  7.     rel_time_t      exptime;    /* expire time */  
  8.     int             nbytes;     /* size of data */  
  9.     unsigned short  refcount;  
  10.     uint8_t         nsuffix;    /* length of flags-and-length string */  
  11.     uint8_t         it_flags;   /* ITEM_* above */  
  12.     uint8_t         slabs_clsid;/* which slab class we're in */  
  13.     uint8_t         nkey;       /* key length, w/terminating null and padding */  
  14.     uint32_t        remaining;  /* Max keys to crawl per slab per invocation */  
  15. } crawler;  
  16.   
  17.   
  18.   
  19. static crawler crawlers[LARGEST_ID];  
  20. static int crawler_count = 0;//本次任务要处理多少个LRU队列  
  21.   
  22.   
  23. //当客户端使用命令lru_crawler crawl <classid,classid,classid|all>时,  
  24. //worker线程就会调用本函数,并将命令的第二个参数作为本函数的参数  
  25. enum crawler_result_type lru_crawler_crawl(char *slabs) {  
  26.     char *b = NULL;  
  27.     uint32_t sid = 0;  
  28.     uint8_t tocrawl[POWER_LARGEST];  
  29.   
  30.     //LRU爬虫线程进行清理的时候,会锁上lru_crawler_lock。直到完成所有  
  31.     //的清理任务才会解锁。所以客户端的前一个清理任务还没结束前,不能  
  32.     //再提交另外一个清理任务     
  33.     if (pthread_mutex_trylock(&lru_crawler_lock) != 0) {  
  34.         return CRAWLER_RUNNING;  
  35.     }  
  36.     pthread_mutex_lock(&cache_lock);  
  37.   
  38.     //解析命令,如果命令要求对某一个LRU队列进行清理,那么就在tocrawl数组  
  39.     //对应元素赋值1作为标志  
  40.     if (strcmp(slabs, "all") == 0) {//处理全部lru队列  
  41.         for (sid = 0; sid < LARGEST_ID; sid++) {  
  42.             tocrawl[sid] = 1;  
  43.         }  
  44.     } else {  
  45.         for (char *p = strtok_r(slabs, ",", &b);  
  46.              p != NULL;  
  47.              p = strtok_r(NULL, ",", &b)) {  
  48.   
  49.             //解析出一个个的sid  
  50.             if (!safe_strtoul(p, &sid) || sid < POWER_SMALLEST  
  51.                     || sid > POWER_LARGEST) {//sid越界  
  52.                 pthread_mutex_unlock(&cache_lock);  
  53.                 pthread_mutex_unlock(&lru_crawler_lock);  
  54.                 return CRAWLER_BADCLASS;  
  55.             }  
  56.             tocrawl[sid] = 1;  
  57.         }  
  58.     }  
  59.   
  60.     //crawlers是一个伪item类型数组。如果用户要清理某一个LRU队列,那么  
  61.     //就在这个LRU队列中插入一个伪item  
  62.     for (sid = 0; sid < LARGEST_ID; sid++) {  
  63.         if (tocrawl[sid] != 0 && tails[sid] != NULL) {  
  64.   
  65.             //对于伪item和真正的item,可以用nkey、time这两个成员的值区别  
  66.             crawlers[sid].nbytes = 0;  
  67.             crawlers[sid].nkey = 0;  
  68.             crawlers[sid].it_flags = 1; /* For a crawler, this means enabled. */  
  69.             crawlers[sid].next = 0;  
  70.             crawlers[sid].prev = 0;  
  71.             crawlers[sid].time = 0;  
  72.             crawlers[sid].remaining = settings.lru_crawler_tocrawl;  
  73.             crawlers[sid].slabs_clsid = sid;  
  74.             //将这个伪item插入到对应的lru队列的尾部  
  75.             crawler_link_q((item *)&crawlers[sid]);  
  76.             crawler_count++;//要处理的LRU队列数加一  
  77.         }  
  78.     }  
  79.     pthread_mutex_unlock(&cache_lock);  
  80.     //有任务了,唤醒LRU爬虫线程,让其执行清理任务  
  81.     pthread_cond_signal(&lru_crawler_cond);  
  82.     STATS_LOCK();  
  83.     stats.lru_crawler_running = true;  
  84.     STATS_UNLOCK();  
  85.     pthread_mutex_unlock(&lru_crawler_lock);  
  86.     return CRAWLER_OK;  
  87. }  


        现在再来看一下伪item是怎么在LRU队列中前进的。先看一个伪item前进图。

        


        从上面的图可以看到,伪item通过与前驱节点交换位置实现前进。如果伪item是LRU队列的头节点,那么就将这个伪item移出LRU队列。函数crawler_crawl_q完成这个交换操作,并且返回交换前伪item的前驱节点(当然在交换后就变成伪item的后驱节点了)。如果伪item处于LRU队列的头部,那么就返回NULL(此时没有前驱节点了)。crawler_crawl_q函数里面那些指针满天飞,这里就不贴出代码了。

        上面的图,虽然伪item遍历了LRU队列,但并没有删除某个item。这样画,一来是为了好看,二来遍历LRU队列不一定会删除item的(item不过期失效就不会删除)。


清理item:

        前面说到,可以用命令lru_crawler tocrawl num指定每个LRU队列最多只检查num-1个item。看清楚点,是检查数,不是删除数,而且是num-1。首先要调用item_crawler_evaluate函数检查一个item是否过期,是的话就删除。如果检查完num-1个,伪item都还没有到达LRU队列的头部,那么就直接将这个伪item从LRU队列中删除。下面看一下item_crawler_thread函数吧。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. static void *item_crawler_thread(void *arg) {  
  2.     int i;  
  3.   
  4.     pthread_mutex_lock(&lru_crawler_lock);  
  5.     while (do_run_lru_crawler_thread) {  
  6.     //lru_crawler_crawl函数和stop_item_crawler_thread函数会signal这个条件变量  
  7.     pthread_cond_wait(&lru_crawler_cond, &lru_crawler_lock);  
  8.   
  9.     while (crawler_count) {//crawler_count表明要处理多少个LRU队列  
  10.         item *search = NULL;  
  11.         void *hold_lock = NULL;  
  12.   
  13.         for (i = 0; i < LARGEST_ID; i++) {  
  14.             if (crawlers[i].it_flags != 1) {  
  15.                 continue;  
  16.             }  
  17.             pthread_mutex_lock(&cache_lock);  
  18.             //返回crawlers[i]的前驱节点,并交换crawlers[i]和前驱节点的位置  
  19.             search = crawler_crawl_q((item *)&crawlers[i]);  
  20.             if (search == NULL || //crawlers[i]是头节点,没有前驱节点  
  21.                 //remaining的值为settings.lru_crawler_tocrawl。每次启动lru  
  22.                 //爬虫线程,检查每一个lru队列的多少个item。  
  23.                 (crawlers[i].remaining && --crawlers[i].remaining < 1)) {  
  24.   
  25.                 //检查了足够多次,退出检查这个lru队列  
  26.                 crawlers[i].it_flags = 0;  
  27.                 crawler_count--;//清理完一个LRU队列,任务数减一  
  28.                 crawler_unlink_q((item *)&crawlers[i]);//将这个伪item从LRU队列中删除  
  29.                 pthread_mutex_unlock(&cache_lock);  
  30.                 continue;  
  31.             }  
  32.             uint32_t hv = hash(ITEM_key(search), search->nkey);  
  33.             //尝试锁住控制这个item的哈希表段级别锁  
  34.             if ((hold_lock = item_trylock(hv)) == NULL) {  
  35.                 pthread_mutex_unlock(&cache_lock);  
  36.                 continue;  
  37.             }  
  38.   
  39.   
  40.             //此时有其他worker线程在引用这个item  
  41.             if (refcount_incr(&search->refcount) != 2) {  
  42.                 refcount_decr(&search->refcount);//lru爬虫线程放弃引用该item  
  43.                 if (hold_lock)  
  44.                     item_trylock_unlock(hold_lock);  
  45.                 pthread_mutex_unlock(&cache_lock);  
  46.                 continue;  
  47.             }  
  48.   
  49.             //如果这个item过期失效了,那么就删除这个item  
  50.             item_crawler_evaluate(search, hv, i);  
  51.   
  52.             if (hold_lock)  
  53.                 item_trylock_unlock(hold_lock);  
  54.             pthread_mutex_unlock(&cache_lock);  
  55.   
  56.             //lru爬虫不能不间断地爬lru队列,这样会妨碍worker线程的正常业务  
  57.             //所以需要挂起lru爬虫线程一段时间。在默认设置中,会休眠100微秒  
  58.             if (settings.lru_crawler_sleep)  
  59.                 usleep(settings.lru_crawler_sleep);//微秒级  
  60.         }  
  61.     }  
  62.     STATS_LOCK();  
  63.     stats.lru_crawler_running = false;  
  64.     STATS_UNLOCK();  
  65.     }  
  66.     pthread_mutex_unlock(&lru_crawler_lock);  
  67.   
  68.     return NULL;  
  69. }  
  70.   
  71.   
  72.  //如果这个item过期失效了,那么就删除其  
  73. static void item_crawler_evaluate(item *search, uint32_t hv, int i) {  
  74.     rel_time_t oldest_live = settings.oldest_live;  
  75.   
  76.     //这个item的exptime时间戳到了,已经过期失效了  
  77.     if ((search->exptime != 0 && search->exptime < current_time)  
  78.         //因为客户端发送flush_all命令,导致这个item失效了  
  79.         || (search->time <= oldest_live && oldest_live <= current_time)) {  
  80.         itemstats[i].crawler_reclaimed++;  
  81.   
  82.         if ((search->it_flags & ITEM_FETCHED) == 0) {  
  83.             itemstats[i].expired_unfetched++;  
  84.         }  
  85.   
  86.         //将item从LRU队列中删除  
  87.         do_item_unlink_nolock(search, hv);  
  88.         do_item_remove(search);  
  89.         assert(search->slabs_clsid == 0);  
  90.     } else {  
  91.         refcount_decr(&search->refcount);  
  92.     }  
  93. }  


真正的LRU淘汰:

        虽然本文前面多次使用LRU这个词,并且memcached代码里面的函数命名也用了lru前缀,特别是lru_crawler命令。但实际上这和LRU淘汰没有半毛钱关系!!

        上当受骗了吧,骂吧:¥&@#¥&*@%##……%#%……#¥%¥@#%……


        读者可以回忆一下操作系统里面的LRU算法。本文里面删除的那些item都是过期失效的,删除了活该。过期了还霸着位置,有点像霸着茅坑不拉屎。操作系统里面LRU算法是因为资源不够,迫于无奈而被踢的,被踢者也是挺无奈的。不一样吧,所以说本文前面说的不是LRU。

        那memcached的LRU在哪里体现了呢?do_item_alloc函数!!前面的博文一直都有提到这个神一般的函数,但从没有给出完整的版本。当然这里也不会给出完整的版本。因为这个函数里面还是有一些东西暂时无法解释给读者们听。现在估计读者都能体会到《如何阅读memcached源码》中写到的:memcached模块间的关联性太多了。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. item *do_item_alloc(char *key, const size_t nkey, const int flags,  
  2.                     const rel_time_t exptime, const int nbytes,  
  3.                     const uint32_t cur_hv) {  
  4.     uint8_t nsuffix;  
  5.     item *it = NULL;  
  6.     char suffix[40];  
  7.     //要存储这个item需要的总空间  
  8.     size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);  
  9.     if (settings.use_cas) {  
  10.         ntotal += sizeof(uint64_t);  
  11.     }  
  12.   
  13.     //根据大小判断从属于哪个slab  
  14.     unsigned int id = slabs_clsid(ntotal);  
  15.   
  16.     int tries = 5;  
  17.     item *search;  
  18.     item *next_it;  
  19.     rel_time_t oldest_live = settings.oldest_live;  
  20.     search = tails[id];  
  21.   
  22.     for (; tries > 0 && search != NULL; tries--, search=next_it) {  
  23.         next_it = search->prev;  
  24.        
  25.         uint32_t hv = hash(ITEM_key(search), search->nkey);  
  26.           
  27.         /* Now see if the item is refcount locked */  
  28.         if (refcount_incr(&search->refcount) != 2) {//引用数>=3  
  29.             refcount_decr(&search->refcount);  
  30.             continue;  
  31.         }  
  32.   
  33.         //search指向的item的refcount等于2,这说明此时这个item除了本worker  
  34.         //线程外,没有其他任何worker线程索引其。可以放心释放并重用这个item  
  35.           
  36.          //因为这个循环是从lru链表的后面开始遍历的。所以一开始search就指向  
  37.          //了最不常用的item,如果这个item都没有过期。那么其他的比其更常用  
  38.         //的item就不要删除了(即使它们过期了)。此时只能向slabs申请内存  
  39.         if ((search->exptime != 0 && search->exptime < current_time)  
  40.             || (search->time <= oldest_live && oldest_live <= current_time)) {  
  41.   
  42.             ..  
  43.         } else if ((it = slabs_alloc(ntotal, id)) == NULL) {//申请内存失败  
  44.             //此刻,过期失效的item没有找到,申请内存又失败了。看来只能使用  
  45.             //LRU淘汰一个item(即使这个item并没有过期失效)  
  46.               
  47.             if (settings.evict_to_free == 0) {//设置了不进行LRU淘汰item  
  48.                 //此时只能向客户端回复错误了  
  49.                 itemstats[id].outofmemory++;  
  50.             } else {  
  51.                 //即使一个item的exptime成员设置为永不超时(0),还是会被踢的  
  52.       
  53.                 it = search;  
  54.                 //重新计算一下这个slabclass_t分配出去的内存大小  
  55.                 //直接霸占旧的item就需要重新计算  
  56.                 slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);  
  57.                 do_item_unlink_nolock(it, hv);//从哈希表和lru链表中删除  
  58.                 /* Initialize the item block: */  
  59.                 it->slabs_clsid = 0;  
  60.   
  61.             }  
  62.         }  
  63.   
  64.         //引用计数减一。此时该item已经没有任何worker线程索引其,并且哈希表也  
  65.         //不再索引其  
  66.         refcount_decr(&search->refcount);  
  67.         break;  
  68.     }  
  69.   
  70.     ...  
  71.   
  72.     return it;  
  73. }  

0 0
原创粉丝点击