追踪分布式Memcached默认的一致性hash算法

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">又到了一个能够沉思的夜晚。近期事情比較繁杂,大脑全然平静不下来。就想着研究点东西来平复一下。</span>
非常久前。曾被一个同事问到过关于PHP端Memcache分布式的细节问题,有一个业务有两台Memcached,举例:业务代码例如以下:
<?php
/**
*生成100个数据到memcache里。能够思考下,数据会怎么存储
*/
$memClient = new Memcached();
$memClient->addServers(array(’10.21.1.11’,’11233’),array(’10.21.1.12’,’11233’));
for($i = 0;$i < 100;$i++){
     $memClicent->set(“prefix_key_”.$i,$i,3600);
}

以下我要開始把这些值打印出来
<?php
$memClient = new Memcached();
$memClient->addServers(array(’10.21.1.11’,’11233’),array(’10.21.1.12’,’11233’));
for($i = 0;$i < 100;$i++){
     $memClicent->get(“prefix_key_”.$i);
}

这里的结果,是全部的值都被打印出来了,可是当我去掉当中一台server把连接变成,
$memClient->addServers(array(’10.21.1.11’,’11233’)。这时打印出来的值少了一将近半。看来serverset函数帮我们自己主动把数据hash到两台server上,可是set是怎么hash的,用的什么算法。我们全然不知道。以下就开启”疯狗模式”,准备深挖源码。

PS:Memcached能够手动设置hash算法,通过setOptions,而且Memcached自带了几种算法,比如Memcached::HASH_CRC,Memcached::HASH_FNV1_32,Memcached::MD5等

首先从memcached扩展入手。查看php_memcached.c,看下set做了什么,发现扩展里全部存储类的操作都调用了一个函数php_memc_store_impl。继续深挖。
PHP_METHOD(Memcached, set)
{
    php_memc_store_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, MEMC_OP_SET, 0);
}

在php_memc_store_imple找到例如以下:
status = memcached_set(m_obj->memc, key, key_len, payload, payload_len, expiration, flags);
他调用了libmemcached里提供的memcached_set函数。并把键值,有效时间传了过去。


在libmemcached/storage.cc里找到memcached_set。通过追踪发现终于存储数据是在memcached_send函里,在这里发现关键代码:
uint32_t server_key= memcached_generate_hash_with_redistribution(ptr, group_key, group_key_length);
memcached_instance_st* instance= memcached_instance_fetch(ptr, server_key);
他通过我们的key来选取某台server。

在libmemcached/hash.cc里找到memcached_generate_hash_with_redistribution函数,通过终于追踪在generate_hash函数里发现关键代码例如以下:
return hashkit_digest(&ptr->hashkit, key, key_length);
返回一个长整型,返回的这个值就是路由到哪个server的关键

在libhashkit/digest.cc里找到了hashkit_digest函数,关键代码例如以下:
return self->base_hash.function(key, key_length, self->base_hash.context);
可是,可是,self->base_hash是什么鬼。看下hashkit_digest函数的形參
uint32_t hashkit_digest(const hashkit_st *self, const char *key, size_t key_length)。这个self是来源于哪,于是開始回朔代码,终于回到了memcached扩展里最初的那个set
status = memcached_set(m_obj->memc, key, key_len, payload, payload_len, expiration, flags);
这里有个m_obj->memc。这是memcached_set传递的第一个參数,于是self->base_hash能够想像成
m_obj->memc->hashkit->base_hash.function(key, key_length, self->base_hash.context);
这个base_hash在哪里。


我们先来看下m_obj是什么东西。

在php_memcached.c的124行,看到m_obj = i_obj->obj;
i_obj的结构体例如以下:
typedef struct {
	zend_object zo;

	struct memc_obj {
		memcached_st *memc;
		zend_bool compression;
		enum memcached_serializer serializer;
		enum memcached_compression_type compression_type;
#if HAVE_MEMCACHED_SASL
		zend_bool has_sasl_data;
#endif
		long store_retry_count;
	} *obj;

	zend_bool is_persistent;
	zend_bool is_pristine;
	int rescode;
	int memc_errno;
} php_memc_t;


在i_obj里我们看到memc是一个memcached_st的指针,还记得hashkit_digest的函数的第一个參数么,里面的ptr就是如今的m_obj->memc,memcached_st在libmemcached-1.0/struct/memcached.h,所以我们从memcached_st里找到hashkit的结构体是hashkit_st。话说最终快找到头了。

hashkit_st结构体例如以下:
struct hashkit_st
{
  struct hashkit_function_st {
    hashkit_hash_fn function;
    void *context;
  } base_hash, distribution_hash;

  struct {
    bool is_base_same_distributed:1;
  } flags;

  struct {
    bool is_allocated:1;
  } options;

  void *_key;
};


看到base_hash在这里。这里有一个最关键的函数指针hashkit_hash_fn function,我们就是调用它指向的hash函数。可是这个指针是哪里被赋的值呢。

我们回到memcached扩展在php_memcached.c里看一下memcached在初始化的时候做了什么(new Memcached()),在static PHP_METHOD(Memcached, __construct)里我们看到m_obj->memc = memcached_create(NULL);这一句代码,这是调用了libmemcached来初始化一个memcached的实例。

我们在libmemcached/memcached.cc里追踪到了_memcached_init(Memcached *self)函数,关键代码例如以下:
  if (hashkit_create(&self->hashkit) == NULL)
  {
    return false;
  }

hashkit_create给了m_obj->memc->hashkit初始化。最终(快断气了)。在libhashkit/hashkit.cc里追踪到了_hashkit_init(hashkit_st *self)。关键代码例如以下:
static inline void _hashkit_init(hashkit_st *self)
{
  self->base_hash.function= hashkit_one_at_a_time;
  self->base_hash.context= NULL;

  self->distribution_hash.function= hashkit_one_at_a_time;
  self->distribution_hash.context= NULL;

  self->flags.is_base_same_distributed= true;
  self->_key= NULL;
}

这里hashkit_one_at_a_time就是我们在没加入默认hash的时候终于调用到的hash函数。
代码例如以下,很短,有兴趣的能够看下他的wiki:http://en.wikipedia.org/wiki/Jenkins_hash_function

#include <libhashkit/common.h>

uint32_t hashkit_one_at_a_time(const char *key, size_t key_length, void *context)
{
  const char *ptr= key;
  uint32_t value= 0;
  (void)context;

  while (key_length--)
  {
    uint32_t val= (uint32_t) *ptr++;
    value += val;
    value += (value << 10);
    value ^= (value >> 6);
  }
  value += (value << 3);
  value ^= (value >> 11);
  value += (value << 15);

  return value;
}



至此,我们就探索了这一内幕。结束了,关闭"疯狗模式”,说实话,整个过程是很享受的:)。难得的一晚,大脑清醒了不少,如有错误,欢迎批评指证。