Memcached中item锁的粒度【转】


      在多线程环境中,对于memcached中的item操作,应该对它加锁,如果加所有的item都加一个全局锁,这样对于一把锁来控制所有hash桶中的item,粒度实在是太大了,一个桶的插入删除操作会阻塞另一个桶的操作。如果锁粒度很小的话,例如每个hash桶加锁,那么如果是在hash桶扩容的情况下,一次扩容操作可能涉及到多个桶数据的迁移,这样需要对多个桶的锁进行循环加锁,这样就有些复杂。Memcached的解决方法就是在按照情况进行锁粒度的转换。

      具体的解决办法是:memcached在扩容操作时,加的都是全局锁,就是所有item(所有hash桶中的)都是一把锁,在扩容操作中,item的操作,例如hash表的删除,插入,touch,查询都是去竞争那个全局锁,因为原来的元素在old_table中的元素需要rehashprimary_table,虽然可以在old_table中的每个桶上加锁,但是没法控制primary_table的多进程操作,小于expand_bucket的元素会直接进入primary_table,old_table的元素会按照新的hash值进入到primary中,不能确定rehash到primary_table的哪个桶中,所以这时侯只能获取全局锁。在扩容结束时,item锁被重新切换回hash桶上的锁,这里锁是分段加锁的(几个桶一个锁,这个具体数值取决与初始的worker的数量,worker数量越多,锁越细,越少hash桶公用一个锁)。默认hash桶是的1<<16hash

assoc.c

  1.  62 void assoc_init(const int hashtable_init) {
  2.  63    if (hashtable_init) {   //初始为空
  3.  64       hashpower = hashtable_init;
  4.  65    }
  5.  66 primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));

这里默认的hashtable_init为空,这里的hashpower=HASHPOWER_DEFAULT,等于16

thread.c

  1. 786 if (nthreads < 3) {
  2. 787 power = 10;
  3. 788 } else if (nthreads < 4) {
  4. 789 power = 11;
  5. 790 } else if (nthreads < 5) {
  6. 791 power = 12;
  7. 792 } else {
  8. 793 /* 8192 buckets, and central locks don't scale much past 5 threads */
  9. 794 power = 13;
  10. 795 }
  11. 796
  12. 797 item_lock_count = hashsize(power);
  13. 798
  14. 799 item_locks = calloc(item_lock_count, sizeof(pthread_mutex_t));

     可以看出在小于3worker的时候,在默认hash桶的数量下,相隔64个桶一个锁,在4的时候相32个桶一把锁,到了6个以上的worker的时候默认情况下相8个桶一个锁。

     在每次操作锁的时候会判断下锁的类型,如果全局锁,就对全局加锁,如果是细粒度()的锁,就通过hash值去取得hash桶的锁

  1. 124 void item_lock(uint32_t hv) {
  2. 125 uint8_t *lock_type = pthread_getspecific(item_lock_type_key);   
  3. 126 if (likely(*lock_type == ITEM_LOCK_GRANULAR)) {//细粒度的锁
  4. 127      mutex_lock(&item_locks[(hv & hashmask(hashpower)) % item_lock_count]);
  5.          //取得hash值在去取得相应的锁
  6. 128 } else {
  7. 129      mutex_lock(&item_global_lock);//全局锁
  8. 130 }
  9. 131 }

     每个线程的锁的类型存在了每个线程的私有空间中,用函数pthread_setspecificpthread_getspecific取得,每个线程默认锁的类型是分段锁(hash桶锁)


  1. 368 static void *worker_libevent(void *arg) {
  2. 369 LIBEVENT_THREAD *me = arg;
  3. 370
  4. 371 /* Any per-thread setup can happen here; thread_init() will block until
  5. 372 * all threads have finished initializing.
  6. 373 */
  7. 374
  8. 375 /* set an indexable thread-specific memory item for the lock type.
  9. 376 * this could be unnecessary if we pass the conn *c struct through
  10. 377 * all item_lock calls...
  11. 378 */
  12. 379 me->item_lock_type = ITEM_LOCK_GRANULAR;
  13. 380 pthread_setspecific(item_lock_type_key, &me->item_lock_type);
  14. 381
  15. 382 register_thread_initialized();
  16. 383
  17. 384 event_base_loop(me->base, 0);
  18. 385 return NULL;
  19. 386 }

       如果在hash空间不够的时候,需要对hash表进行扩容,这时候需要转换item的锁从分段锁转化成为全局锁,由于hash扩容操作是一个单独的线程在做,改变锁的类型需要改变所有workeritem的锁的类型,这时候memcached是通过循环每个worker对他们的pipe的一端写入锁转换的命令,在每个worker其实都有两套libeventloop,一套用于监听其他线程发给work的信息,例如主线程去监听网络,如果有连接来了,主线程通过这个pipe通知worker,还有就是这个锁的类型的变化,通过pipe通知,另一套loop就是去通过那个连接的fd去轮询网络中的读事件等。

  1. 174 void switch_item_lock_type(enum item_lock_types type) {
  2. 175 char buf[1];//通过pipe发给worker的char buf
  3. 176 int i;
  4. 177
  5. 178 switch (type) {
  6. 179    case ITEM_LOCK_GRANULAR:
  7. 180    buf[0] = 'l';//转换为分段锁
  8. 181    break;
  9. 182 case ITEM_LOCK_GLOBAL:
  10. 183    buf[0] = 'g';//转换为全局锁
  11. 184    break;
  12. 185 default:
  13. 186 fprintf(stderr, "Unknown lock type: %d\n", type);
  14. 187 assert(1 == 0);
  15. 188 break;
  16. 189 }
  17. 190
  18. 191 pthread_mutex_lock(&init_lock);
  19. 192 init_count = 0;
  20. 193 for (i = 0; i < settings.num_threads; i++) {
  21. 194 if (write(threads[i].notify_send_fd, buf, 1) != 1) {
  22.     //向每个worker的notify_send_fd (pipe的一端)写入buf
  23. 195 perror("Failed writing to notify pipe");
  24. 196 /* TODO: This is a fatal problem. Can it ever happen temporarily? */
  25. 197 }
  26. 198 }
  27. 199 wait_for_thread_registration(settings.num_threads);//等待每个线程处理完设置完锁的类型
  28. 200 pthread_mutex_unlock(&init_lock);
  29. 201 }

thread.c中锁的类型转换:


  1. 427 case 'l':
  2. 428    me->item_lock_type = ITEM_LOCK_GRANULAR;
  3. 429    register_thread_initialized();
  4. 430    break;
  5. 431 case 'g':
  6. 432    me->item_lock_type = ITEM_LOCK_GLOBAL;
  7. 433    register_thread_initialized();
  8. 434    break;
  9. 435 }

通过连接每个worker的管道就设置了锁的类型。

hash扩容时会加全局锁


  1. 204 static void *assoc_maintenance_thread(void *arg) {
  2. 205
  3. 206 while (do_run_maintenance_thread) {
  4. 207 int ii = 0;
  5. 208
  6. 209 /* Lock the cache, and bulk move multiple buckets to the new
  7. 210 * hash table. */
  8. 211 item_lock_global();//加的是全局锁
  9. 212 mutex_lock(&cache_lock);

      在没有开始扩容时会把锁粒度调整到分段锁,同时打开slab_move,详见前一篇blog,扩容会使线程停留在等待maintenance_cond上,在hash容量操作了桶的1.5倍后slab_move被停止,同时锁的粒度转换为全局锁,开始hash扩容

  1. 244 if (!expanding) {
  2. 245 /* finished expanding. tell all threads to use fine-grained locks */
  3. 246 switch_item_lock_type(ITEM_LOCK_GRANULAR);//默认就是分段锁
  4. 247 slabs_rebalancer_resume(); //打开slab_move
  5. 248 /* We are done expanding.. just wait for next invocation */
  6. 249 mutex_lock(&cache_lock);
  7. 250 started_expanding = false;
  8. 251 pthread_cond_wait(&maintenance_cond, &cache_lock);//在不扩容时,线程会停留在这里
  9. 252 /* Before doing anything, tell threads to use a global lock */
  10. 253 mutex_unlock(&cache_lock); //后面开始扩容准备
  11. 254 slabs_rebalancer_pause();//停止slab_move
  12. 255 switch_item_lock_type(ITEM_LOCK_GLOBAL);//转换为全局锁
  13. 256 mutex_lock(&cache_lock);
  14. 257 assoc_expand();//开始扩容
  15. 258 mutex_unlock(&cache_lock);
  16. 259 }

在对item进行操作时都会加上调用item_lock,参数时桶的hash值,会根据锁的类型来决定是否阻塞对item的操作。

例如插入item操作:

  1. 517 int item_link(item *item) {
  2. 518 int ret;
  3. 519 uint32_t hv;
  4. 520
  5. 521 hv = hash(ITEM_key(item), item->nkey, 0);//取得hash值
  6. 522 item_lock(hv); //根据锁的类型加锁
  7. 523 ret = do_item_link(item, hv);//插入操作
  8. 524 item_unlock(hv);//释放锁
  9. 525 return ret;
  10. 526 }

再例如删除操作(释放itemslab中的空间):

  1. 532 void item_remove(item *item) {
  2. 533 uint32_t hv;
  3. 534 hv = hash(ITEM_key(item), item->nkey, 0);//取得hash值
  4. 535
  5. 536 item_lock(hv);//根据锁的类型加锁
  6. 537 do_item_remove(item);//删除操作
  7. 538 item_unlock(hv);//释放锁
  8. 539 }

thread.c中可以看到依靠这种锁的操作有item_getitem_touchitem_unlinkitem_updateadd_deltastore_item

memcached中锁粒度的转换大大提高了它的并发性,对于每个item的操作,还有refcount变量来表示一个item的状态,在多线程中如何不用对每个item加锁,而通过refcount来控制item的操作,这个在研究中,下次blog再写。


你可能感兴趣的:(Memcached中item锁的粒度【转】)