幸运的是,开源社区已经有非常成熟的分布式缓存系统,现如今,几乎没有人不知道 memcached,我们也曾经在前面使用过 memcached 来存储动态内容的页面缓存。可是,你真的能让它工作得愉快吗
Key-value
首先,我们要知道,为了实现高速缓存,我们不会将缓存内容放置在磁盘上,否则将毫无意义。基于这个原则,memcached 使用物理内存来作为缓存区,当我们启动 memcached 的时候,需要指定分配给缓存区的内存大小,比如我们分配了 4GB 的 内存来作为缓存区:
s-colin:~ # memcached -d -m 4086 -l 10.0.1.12 -p 11711
如果要说 memcached 最需要的是什么,毫无疑问,那就是内存。我使用的一台 memcached 服务器已经消耗掉了 2.8GB 的内 存空间,而我一共给它分配了 4GB
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3950 root 15 0 2913m 2.8g 384 S 1 35.9 1325:49 memcached
memcached 使用 key-value 的方式来存储数据,这是一种单索引的结构化数据组织形式,我们将每个key 以及对应的 value 合
起来称为数据项,所有数据项之间彼此独立,每个数据项都以 key 作为唯一索引,你可以通过 key 来读取或者更新这个数据项。
对于 key-value 这样的存储形式,你无法习惯性地像操作关系数据库那样来控制它,最大的不同在于,所有基于 SQL 语句的
条件查询方式在 key-value 形式的存储数据上都无法施展。所以,如何用 key-value 方式来存储数据,一部分取决于你将如何查询这些数据。
在稍具规模的应用中,缓存的数据项可能会非常多,足够达到天文数字,为了在内存中为如此之多的数据项提供高速的查找,
memcached 使用了高效的基于 key 的 hash 算法来设计存储数据结构,并且使用了精心设计的内存分配器,它们使得数据项查询的时间复杂度达到 O(1),这意味着不论你存储多少数据项,查询任何数据项所花费的时间都不变,几乎没有什么理由可以让你不用它。
数据项过期时间
由于缓存区空间是有限的,一旦缓存区没有足够的空间存储新的数据项时,memcached 便会想办法淘汰一些数据项来腾出空间,淘汰机制基于 LRU(Least Recently Used)算法,将最近不常访问的数据项淘汰掉。
当然,我们更愿意为数据项设置过期时间,这样它可以很有面子地离开缓存区。至于过期时间的取值,前面我们已经讨论过很多次,这需要你根据自己的站点来把握平衡,它们的道理都是相似的,你可以回顾一下之前介绍过期时间的内容。
如果你在使用 PHP 来编写动态内容,通过 memcached 的 PECL 扩展,你可以很容易地设置数据项过期时间,比如:
<?php $memcache_obj = memcache_connect('10.0.1.12', 11211); $memcache_obj->add('item_key', 'item_value', false, 30); ?>
这里我们将 item_key 这个数据项的过期时间设置为 30 秒。
网络并发模型
作为分布式缓存系统,memcached 可以运行在独立的服务器上,动态内容通过 TCP Socket 来访问它,这样一来,memcached本身的网络并发处理能力便显得尤为重要。memcached 使用 libevent 函数库来实现网络并发模型,其中包括我们前面详细介绍过的 epoll,所以你可以在较大并发用户数的环境下仍然放心使用 memcached。
这里,我们不妨做一个简单的性能测试,来看看 memcached 中 set 操作的性能,我们用 PHP 编写了以下的代码:
<?php $key = md5(uniqid()); $value = md5(uniqid()); $memcache = memcache_connect('10.0.1.12', 11711); $memcache->set($key, $value, false, 0); $memcache->close(); ?>
很简单,它首先随机生成两个 32 字节的字符串,分别赋值给 key 和 value,比如:
key -> 87e4f726c1837c98b4719488d9530532 value -> 64f0f23d115dca3b019bea8340cb552e
接下来连接到 memcached 服务器,然后通过 set 操作来写入数据,最后关闭连接。显然,我们将这一系列的操作打包为一个 动态程序,下面就对这个动态程序进行压力测试,结果如下所示:
Server Software: lighttpd/1.4.20 Server Hostname: www.liveinmap.com Server Port: 8001 Document Path: /book_memcache_perf3.php Document Length: 844 bytes Concurrency Level: 500 Time taken for tests: 2.138 seconds Complete requests: 5000 Failed requests: 0 Write errors: 0 Total transferred: 5028338 bytes HTML transferred: 4220000 bytes Requests per second: 2338.48 [#/sec] (mean) Time per request: 213.814 [ms] (mean Time per request: 0.428 [ms](mean, across all concurrent requests) Transfer rate: 2296.61 [Kbytes/sec] received
从结果中我们知道 memcached 服务器平均每秒处理了大约 2338 个 set 请求,当然,处理过程中不仅仅包括 set 操作,还包括了建立连接和释放连接,它们的开销不可忽视。
接下来,我们希望尽可能地了解 set 操作本身的性能,办法是有的,我们在一个连接中重复进行多次 set 操作就可以了,我们修改一下刚才的 PHP 代码,如下所示:
<?php $key = md5(uniqid()); $value = md5(uniqid()); $memcache = memcache_connect('10.0.1.12', 11711); for ($i = 0; $i < 10000; ++$i) { $memcache->set($key . $i, $value . $i, false, 0); } $memcache->close(); ?>
与修改之前相比,我们看到 set 操作被一个循环体重复执行了 10000 遍,并且为了让每次 set 操作可以更新不同的节点,更加接近实际运行情况,我们在循环体中对 key 的末尾追加了递增的数值。
我们再来进行压力测试,但这次只模拟 10 个并发用户数,同时总请求数为 50,也就是每个用户发送 5 个请求,为什么这么做呢?别忘了我已经让脚本中的 set 操作重复了 10000 遍,我可等不了那么长的时间。测试结果如下所示:
Server Software: Apache/2.2.11 Server Hostname: www.liveinmap.com Server Port: 80 Document Path: /book_memcache_perf3.php Document Length: 2 bytes Concurrency Level: 10 Time taken for tests: 12.248 seconds Complete requests: 50 Failed requests: 0 Write errors: 0 Total transferred: 10100 bytes HTML transferred: 100 bytes Requests per second: 4.08 [#/sec] (mean) Time per request: 2449.526 [ms] (mean Time per request: 2449.523 [ms](mean, across all concurrent requests) Transfer rate: 0.81 [Kbytes/sec] received
这次我们不看结果中的吞吐率,而是记录下总时间,为 12.248 秒。在这段时间内,memcached 一共处理了 50 个请求,而每个请求包括 10000 次 set 操作,那么我们可以算出每秒执行的 set 次数为:
10000 × 50 / 12.248 = 40823
当然,这是极端前提下的结果,实际情况中根据 TCP 连接复用程度的不同,memcached 的吞吐率会存在上下浮动,这是正常的,你需要根据站点的实际环境来评估它的处理能力
对象序列化
我们可以在 memcached 的数据项中存储什么格式的内容呢?这要考虑到网络传输,问题也就转换成了“我们可以在网络中传输什么内容”。自然是二进制数据。那么,对于数组或者对象这样的抽象数据类型,是否可以存入 memcached 中呢?
基于序列化(Serialize)机制,我们可以将更高层的抽象数据类型转化为二进制字符串,以便通过网络进入缓存服务器,同时,在读取这些数据的时候,二进制字符串又可以转换回原有的数据类型。
但是,有一点需要清楚的是,当我们试图将一个类的对象(或类的实例)进行序列化时,对象的成员函数是不被序列化的,而被序列化存储的只是对象的数据成员。当需要从持久化数据中恢复对象(反序列化)时,我们首先会实例化另一个新的对象,然后将之前持久化的数据成员依次赋值给新对象的相应数据成员。听起来比较复杂,不过幸运的是,在具有动态特性的脚本语言中,这些具体的过程往往不需要你去实现,你只需要或多或少了解序列化的本质即可。
顺便说一下,我们熟悉的 JSON 格式,便可以很好地应用在序列化中,任何数组和对象都可以很容易地与 JSON 格式的字符串互相转换,而且转换所需的计算量并不大。
对于对象序列化,常见的服务器端动态脚本语言都有相应的扩展支持,比如通过 PHP 的 memcached 扩展,你可以随时将对象写入缓存服务器,并在随后取出。下面是一个简单的例子:
<?php class Person { var $name; function setName($name) { $this->name = $name; } } $person = new Person(); $person->setName('colin'); $key = 'person.colin'; $memcache = memcache_connect('10.0.1.12', 11711); $memcache->add($key, $person, false, 0); $obj = $memcache->get($key); echo $obj->name; ?>
好,我们来运行这个 PHP 脚本,你可以通过浏览器请求它,也可以直接通过 PHP 的命令行方式来执行它,无论如何,它的结果都是一样的,如下所示
colin
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程