使用 memcached

2018年03月24日 09:20 | 1097次浏览 作者原创 版权保护

幸运的是,开源社区已经有非常成熟的分布式缓存系统,现如今,几乎没有人不知道 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


此文章本站原创,地址 https://www.vxzsk.com/693.html   转载请注明出处!谢谢!

感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程