python-memcached源码小窥

以前经常使用python-memcached对内存进行操作,但应用都比较简单,最近需要有一个分布式缓存系统于是看了看关于twemproxy 以及 mcrouter 这两款分别由twitter和facebook开源的软件文档。这2个软件都能容易的扩展缓存节点以及自动删除问题节点,并且提供不同的算法把数据缓存到各个节点中。这时候我想起来使用python-memcached的时候,也可以使用多个节点,并且某个节点挂掉后并不影响整个缓存程序的使用,那么它是怎么将数据分配到不同的节点呢?以及怎么处理的故障节点呢? python-memcached的源码只有一个文件,不管是get或者set,取得服务节点Ip的函数如下:

_SERVER_RETRIES = 10  # how many times to try finding a free server.
def _get_server(self, key):
    print(key)
    if isinstance(key, tuple):
        serverhash, key = key
    else:
        serverhash = serverHashFunction(key)
    if not self.buckets:
        return None, None
    for i in range(Client._SERVER_RETRIES):
        print(self.buckets)
        print (serverhash)
        print(serverhash % len(self.buckets))
        server = self.buckets[serverhash % len(self.buckets)]
        if server.connect():
            print("(using server %s)" % server,)
            return server, key
        serverhash = str(serverhash) + str(i)
        if isinstance(serverhash, six.text_type):
            serverhash = serverhash.encode('ascii')
        serverhash = serverHashFunction(serverhash)
    return None, None

其中print语句是我添加的,为了更清晰的看出原因。首先我们创建一个对象并且设置一个值:

In [4]: mc = meme.Client(['127.0.0.1:11211','192.168.0.202:11211','192.168.0.203:11211'])
In [5]: mc.set("asdf","asdf")
asdf
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
20777
2
(using server inet:192.168.0.203:11211)
Out[5]: True

可以看到,这个"asdf"被放到了节点3中。再来多几个数据:

In [23]: mc.set("a","test")
a
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
26807
2
(using server inet:192.168.0.203:11211)
Out[23]: True
In [24]: mc.set("b","test")
b
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
29118
0
(using server inet:127.0.0.1:11211)
Out[24]: True
In [25]: mc.set("c","test")
c
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
1721
2
(using server inet:192.168.0.203:11211)
Out[25]: True
In [26]: mc.set("d","test")
d
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
6365
2
(using server inet:192.168.0.203:11211)
Out[26]: True
In [27]: mc.set("e","test")
e
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
28634
2
(using server inet:192.168.0.203:11211)
Out[27]: True
In [28]: mc.set("f","test")
f
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
30419
2
(using server inet:192.168.0.203:11211)
Out[28]: True
In [29]: mc.set("g","test")
g
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
468
0
(using server inet:127.0.0.1:11211)
Out[29]: True
In [30]: mc.set("h","test")
h
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
4459
1
(using server inet:192.168.0.202:11211)
Out[30]: True

好像并没有什么规律,但有心的读者肯定发现了,程序的流程就是根据key算出一个hash值,再根据这个hash值对总节点数进行取余数。换言之,数据被存储在哪个节点完全是由key来决定的!在某些极端情况下,可能出现所有的数据都存储在同一个节点的情况。

用于hash的函数如下:

def cmemcache_hash(key):
    return (
        (((binascii.crc32(key) & 0xffffffff) >> 16) & 0x7fff) or 1)
serverHashFunction = cmemcache_hash

首先对key进行crc32操作,这里注意python2.x版本中crc计算后得到的是有符号整数(- 2^31—-2^31-1),所以需要使用位操作& 0xffffffff将其转成无符号整数,然后在向右位移16位截取高16位,再与0x7fff进行位操作将值变成正数。(比较疑惑,不知道这么做后key转换出来的值真的不会重复吗?算法渣渣啊….)

那么,如果一台节点挂了,似乎并没有影响整个缓存的使用啊?比如上面key=h的数据应该存放在第二个节点上,现在关闭第二个节点(192.168.0.202),再取值:

In [35]: mc.get('h')
h
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
4459
1
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
26423
2
(using server inet:192.168.0.203:11211)

可以看出,就算某个节点挂了,对于mc来说,backets还是有3个节点的,它并不会把失效节点从“池子”中移除。不过节点2链接不上,于是程序把上次得到的hash值于循环次数进行拼接后再hash,看其能否得到一个可以链接的节点。这次结果是得到了节点3并且可以链接,于是程序去节点3寻找key=h的值,但第三个节点并没有,所以返回none。换言之,即使节点1有key=h的值,程序也不会理会的。 那么,我再设置一个key=h的数据试试:

In [36]: mc.set("h","test")
h
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
4459
1
[<meme._Host object at 0x7f2df825c390>, <meme._Host object at 0x7f2df825c3d0>, <meme._Host object at 0x7f2df825c410>]
26423
2
(using server inet:192.168.0.203:11211)

结果不出所料,程序还是先去找节点2,发现链接不上就继续hash,直到找到一个能链接的节点为止。极端情况下就循环10次都没碰到可用节点的话,程序就认为所有节点都挂了。

这种逻辑下,假设程序经过2次hash后决定把数据存在节点3后,节点2恢复正常,再进行get操作会发现数据还是取不到的。

结论:

python-memcached无法动态进行节点的扩展或者删除,简单的应用或者只有一个缓存节点时,python-memcached还是很给力的,但如果需要的是一个分布式缓存集群的话,还是使用上面提到的那2个程序更高效、灵活。