twemproxy是twitter开发的用于redis或memcache的proxy,其原理是对key计算hash,然后对节点数取模,就映射到了对应的redis/memcache节点上。当然如果要添加或者删除节点,需要手动去做数据迁移。

今天和前同事默罕默德•凯讨论twemproxy中的一段代码,颇有意思:

File: src/nc_conf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#define DEFINE_ACTION(_hash, _name) string(#_name),
static struct string hash_strings[] = {
    HASH_CODEC( DEFINE_ACTION )
    null_string
};
#undef DEFINE_ACTION

#define DEFINE_ACTION(_hash, _name) hash_##_name,
static hash_t hash_algos[] = {
    HASH_CODEC( DEFINE_ACTION )
    NULL
};
#undef DEFINE_ACTION

其中HASH_CODEC的定义在:src/hashkit/nc_hashkit.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#define HASH_CODEC(ACTION)                      \
    ACTION( HASH_ONE_AT_A_TIME, one_at_a_time ) \
    ACTION( HASH_MD5,           md5           ) \
    ACTION( HASH_CRC16,         crc16         ) \
    ACTION( HASH_CRC32,         crc32         ) \
    ACTION( HASH_CRC32A,        crc32a        ) \
    ACTION( HASH_FNV1_64,       fnv1_64       ) \
    ACTION( HASH_FNV1A_64,      fnv1a_64      ) \
    ACTION( HASH_FNV1_32,       fnv1_32       ) \
    ACTION( HASH_FNV1A_32,      fnv1a_32      ) \
    ACTION( HASH_HSIEH,         hsieh         ) \
    ACTION( HASH_MURMUR,        murmur        ) \
    ACTION( HASH_JENKINS,       jenkins       ) \

刚看到这段宏定义的时候会有点点纳闷,因为在源代码中检索不到HASH_CODEC里面定义的ACTION。仔细一看,原来ACTION是HASH_CODEC传进来的参数,而在前面nc_conf.c中,先定义了“函数” DEFINE_ACTION,然后把DEFINE_ACTION作为“参数”传到HASH_CODEC中。其实是实现了一个类似把函数当成参数传递的功能,其实把hash_strings扩展开来,就相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 这里的DEFINE_ACTION就是HASH_CODEC里面的ACTION,
   宏定义里面的#号表示把后面替换的符号转换成字符串
   (例如:string(#md5) ==> string("md5") )
 */
#define DEFINE_ACTION(_hash, _name) string(#_name),
static struct string hash_strings[] = {
    string("one_at_a_time") \
    string("md5"), \
    string("crc16"), \
    string("crc32"), \
    string("crc32a"), \
    string("fnv1_64"), \
    string("fnv1a_64"), \
    string("fnv1_32"), \
    string("fnv1a_32"), \
    string("hsieh"), \
    string("murmur"), \
    string("jenkins"), \
    null_string
};
#undef DEFINE_ACTION

再去src/nc_string.h中看string定义,就一目了然了:

1
2
3
4
5
6
7
struct string {
    uint32_t len;   /* string length */
    uint8_t  *data; /* string data */
};

#define string(_str)   { sizeof(_str) - 1, (uint8_t *)(_str) }
#define null_string    { 0, NULL }

相同的道理,再来看这一段:

1
2
3
4
5
6
7
8
9
10
/* 宏定义中的##号表示把前后两个符号连接成一个符号(注意不是字符串),
   这里的_name是宏参数,注意hash_不是_hash,就仅仅是个符号而已,
   因此, ACTION(HASH_MD5, md5) 就直接展开成为 hash_md5
 */
#define DEFINE_ACTION(_hash, _name) hash_##_name,
static hash_t hash_algos[] = {
    HASH_CODEC( DEFINE_ACTION )
    NULL
};
#undef DEFINE_ACTION

src/nc_server.h中,定义了hash_t:

1
typedef uint32_t (*hash_t)(const char *, size_t);

所以hash_algos是一个函数指针数组,这个宏定义展开就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static hash_t hash_algos[] = {
    hash_one_at_a_time \
    hash_md5, \
    hash_crc16, \
    hash_crc32, \
    hash_crc32a, \
    hash_fnv1_64, \
    hash_fnv1a_64, \
    hash_fnv1_32, \
    hash_fnv1a_32, \
    hash_hsieh, \
    hash_murmur, \
    hash_jenkins, \
    NULL
};

头文件中用先定义DEFINE_ACTION,把这个“函数”当成一个参数传入到另一个宏中,然后再undefine,就像是在用一个变量不停地赋值然后传入到另一个函数中去,只需要用一行代码去将DEFINE_ACTION定义成一个生成规则,再定义HASH_CODEC,通过这段让人眼花缭乱的代码,可以完成多个数组的赋值,不得不说这是一个C语言工程师提升逼格的必备技巧!