有很多理由让我们不得不扩展 memcached 的规模,包括并发处理能力和缓存空间容量等,不论是哪个方面达到极限,扩展都 在所难免。 对于缓存空间的容量,扩容意味着增加服务器物理内存,这显得不切合实际,而对于并发处理能力,我们知道,memcached已经在这方面做了很大的努力,这也是它成名的前提。所以,我们只能通过增加新的缓存服务器来达到扩展的目的。
当存在多台缓存服务器后,我们面临的问题是,如何将缓存数据均衡地分布在多台缓存服务器上。现假设我们拥有以下两台 缓存服务器,它们的内部 IP 地址如下所示,它们都运行着 memcached。
10.0.1.12 10.0.1.13
看起来这没什么难的,因为 key-value 类型的数据项本来就相互独立,你可以在连接 memcached 服务器的时候,随便从以上 两台服务器中挑选出一台即可。但是,关键在于如何做到“均衡”呢?这个问题归结于如何对数据项进行分区。
相比于关系型数据库的分区设计,key-value 型数据缓存的分区要容易得多,你可以根据数据项的商业逻辑进行分区设计,拿 前面的例子来说,我们可以将用户登录状态的缓存部署在独立的一台缓存服务器上,而将访问量统计的缓存部署在另一台独 立的服务器上。这样一来,这两台缓存服务器的用途如下:
10.0.1.12 -> 用户登录状态缓存 10.0.1.13 -> 访问量统计缓存
但是,它们仍然存在以下两个问题:
1.两台缓存服务器的工作量真的均衡吗?
2.如果两台缓存服务器仍然不能满足需要,如何继续扩展呢?
第一个问题的答案并不确定,但我想在大多数情况下都是不均衡的,由于以上两种缓存的职责截然不同,所以它们的开销和 访问频率也会存在很大的差异,我们当然希望两台缓存服务器的工作量比较对称,达到物尽其用。
对于第二个问题,假如访问量统计缓存需要进一步扩展,我们准备了新的缓存服务器:
10.0.1.14
接下来我们将访问量统计缓存再次划分,存储在两台缓存服务器上,如何划分呢?我们可以将被统计的所有子站点划分为两 部分,让它们分别存储在不同的缓存服务器上,为此,我们需要为每个缓存服务器维护一个一对多的映射表(One To Many Mapping List) ,里边记录子站点的域名,比如
Group 1 -> www.highperfweb.com, app1.highperfweb.com, app2.highperfweb. com Group 2 -> app3.highperfweb.com, app4.highperfweb.com, app5.highperfweb. com
如此一来,我们重新调整缓存划分方式,三台缓存服务器上的缓存内容如下所示:
10.0.1.12 -> 用户登录状态缓存 10.0.1.13 -> 访问量统计缓存 Group 1 10.0.1.14 -> 访问量统计缓存 Group 2
但是,假如再次需要扩展,我们还要调整映射表吗?问题是也许你也不知道该怎么划分,实际情况中的映射表可能不像例子 中的如此简洁,人工维护这个列表无疑是自找麻烦。另一方面,一个老问题再次浮现出来,你能保证它们的工作量均衡吗?
好吧,我们决定打破这种基于数据项商业逻辑的划分思维,来考虑一种基于 key 的划分方式,这有些类似于后面介绍的数据 库水平分区(Sharding)。我们需要设计一种不依赖数据项内容的散列算法,将所有数据项的 key 均衡分配在这三台缓存服务 器上。
一个简单而有效的方法是“取余”运算,这就像打扑克时的发牌,让所有数据项按照一个顺序在不同的缓存服务器上轮询, 这可以达到较好的相对平衡,想想发牌的动机也正是为了让大家彼此公平。这种方法是一种比较常用的基本散列算法,事实 上在很多时候都用得到,而且你可以根据实际情况对它进行改造,达到更好的散列效果。下面我们举个例子。
在“取余”之前,我们先要做一些准备工作,目的是让 key 变成整数,而且尽量唯一。比如对于以下这个 key:
article_090222.htm
我们先对它进行 md5 运算,这里直接使用 PHP 命令行方式
s-colin:~ # php -r "echo md5('article_090222.htm');" e6e87fc9f9c2914339a9b7cc4db6055c
得到的是一个 32 字节的字符串,同时它也是一个十六进制的长整数,为了减少计算开销,我们取这个字符串的前 5 个字节, 然后将它转换为十进制数
s-mat:~ # php -r "echo hexdec('e6e87');" 945799
然后将结果进行“模 3”运算
然后将结果进行“模 3”运算 1
得到的余数便是缓存服务器的编号,我们的三台缓存服务器应该从 0 开始编号,那么 1 代表了第二台服务器。
看起来真复杂,我们不得不需要一个“缓存连接器”了,我们希望将上面这些运算都放在连接器里,而只需要告诉它 key, 接下来选择缓存服务器的事情就拜托给它了。看看连接器的一个例子:
<?php function memcache_connector($key) { $hosts = array( '10.0.1.12', '10.0.1.13', '10.0.1.14' ); $host_index = hexdec(substr(md5($key), 0, 5)) % 3; $host = $hosts[$host_index]; return memcache_connect($host, 11711); } ?>
现在,我们访问缓存的时候只需要这样连接缓存服务器即可:
<?php $memcache = memcache_connector('article_090222.htm'); ?>
那么,如果还需要继续扩展,一定难不倒你了,将“模 3”的运算变成“模 4”、“模 5”……然后其余的工作就放心地交给连 接器去做吧。
这里有一个问题也许你一直在思考,那就是当我们扩展缓存系统后,由于分区算法的改变,会涉及缓存数据需要从一台缓存 服务器迁移到另一台缓存服务器的问题,如何迁移呢?事实上,根本不需要考虑分区之间的迁移,因为这是缓存,它应该具 备在必要时刻牺牲自己的勇气,当然这是你赋予它的,你必须明白缓存不是持久存储,并且从引入分布式缓存开始就不断地 提醒自己。
没错,当调整缓存分区算法后,我们需要时间来等待缓存重建和预热,但这往往并不影响站点的正常运转,前提是你按照前 面读缓存和写缓存的理念来进行设计。顺便一提的是,与此相比,数据库规模扩展引发分区(Shard)之间的数据迁移就要复 杂得多,后面我们会有专门的章节探讨它
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程