关于“facebook的memcached实战”小记

上周挤到QCon的会场里,听了两场 —— Facebook的Memcached实战,以及Twitter 的可伸缩性数据架构。当时对facebook超大规模使用memcached印象很深刻,只可惜到现在也没见到这个的ppt。平时用php比较多,因此听闻同样使着php的facebook讲memcached,有些小小的感触,记录下来。

更高效的序列化函数

php有两个memcache扩展,默认都是使用php自带的序列化函数serialize来存储数组或对象。但是serialize最为人诟病的就是速度慢,序列化之后占用空间大。由于facebook已经在memcached里保存了200T字节的数据,因此序列化函数即便作出的百分之一的优化对它来说都是个不小的收益。他们发粪涂墙在thrift的binary协议基础上搞出了一个fb_serialize,据称这个序列化方法能快上3倍,快倒算了,还能节省30%空间, 200T字节的数据能节省出30%,简直就是传说中的银弹啊,这让php官方的开发人员们情何以堪?

facebook目前已经开源了thrift,其中自带了一个thrift协议的php扩展,但是这些代码里没有找到传说中的fb_serialize,我倒是从最近他们放出来hiphop-php里找到了这部分代码,哪位大侠去扒拉扒拉弄出来做成php扩展造福广大群众?

作为备选方案,我推荐igbinary,这也是一个binary的序列化方法。在上次的测试结果中,它甚至能节约50%的存储空间,速度也是稳超php原生的序列化方法,搞不好facebook换了这个序列化方法能省下更多的内存来?

节约每个item的存储空间有什么好处?我个人认为一个是省钱,另外一个就是能够带来速度上的提升。我们平常碰到稍大一点的item都得用gzip压的妥妥贴贴的才送到memcached里,网络传输的开销小了,这是实实在在的性能提升。何乐而不为?

mcproxy

mcproxy = memcached + proxy。facebook的机房遍布各洲,利用mcproxy来进行跨机房的同步或分发,全球制霸,指着太阳就能等到那天了。一般的互联网企业还真用不上这玩意,规模还没上去的时候,这些乱七八糟的只会拖后腿。facebook还没开源mcproxy,但是我找到两个替代品:

    • memagent is a simple but useful proxy program for memcached servers.
    • moxi = memcached + integrated proxy

从项目描述来看,moxi最接近facebook介绍的mcproxy,成熟度也比较高。

数据的一致性

Marc Kwiatkowski在会场上用大篇幅的ppt和大量的动画来阐述这个问题,他们用了很多额外的手段来解决在跨机房情况下因为延时问题造成的脏数据。这一段看着挺晕,但是我们联想到facebook用到的多级cache技术: 本地全局变量 + apc + memcache,不难理解这样做颇有些道理,这相当于用memcache实现了一个版本控制系统。

我还是很晕这段ppt。

igbinary vs serialize vs json_encode

最近看到memcached扩展支持额外的序列化方式 — igbinary,这是一个未收录到pecl的php扩展,它提供的两个主要方法:

    • igbinary_serialize
    • igbinary_unserialize

据称可以用它来代替php自带的序列化函数serialize,性能更好,而且占用的字节数也更少。下面我就 igbinary ,serialize ,json_encode三者的性能做了一个简单的测试。

测试

以一个包含1000000个元素的数组做为原始数据,分别以json, serialize, igbinary进行序列化和反向操作。


<?php
ini_set('memory_limit', '512m');
$array = array_fill(0, 1000000, rand(1, 9999));

$start = microtime(true);
$export = json_encode($array);
$end = microtime(true);
$duration = $end - $start;
print('JSON Encode: ' . $duration . PHP_EOL);

$start = microtime(true);
$import = json_decode($export);
$end = microtime(true);
$duration = $end - $start;
print('JSON Decode: ' . $duration . PHP_EOL);

$start = microtime(true);
$export = serialize($array);
$end = microtime(true);
$duration = $end - $start;
print('Serialize: ' . $duration . PHP_EOL);

$start = microtime(true);
$import = unserialize($export);
$end = microtime(true);
$duration = $end - $start;
print('Serialize: ' . $duration . PHP_EOL);

$start = microtime(true);
$export = igbinary_serialize($array);
$end = microtime(true);
$duration = $end - $start;
print('Igbinary Serialize: ' . $duration . PHP_EOL);

$start = microtime(true);
$import = igbinary_unserialize($export);
$end = microtime(true);
$duration = $end - $start;
print('Igbinary Serialize: ' . $duration . PHP_EOL);
?>

测试结果

JSON Encode: 0.084825992584229
JSON Decode: 0.34976410865784
Serialize: 0.38241410255432
Serialize: 7.7904229164124
Igbinary Serialize: 0.046916007995605
Igbinary Serialize: 0.23396801948547

从测试结果来看,速度方面优先级排列为 igbinary > json > serialize。同时我们也可以看到,php原生的serialize在对大对象进行反向操作时,速度真是掉队一大截了。

占用字节数对比

    • json: 5000001
    • serialize: 15888902
    • igbinary: 7868681

在没有中文字符的情况下,json胜出,igbinary次之,serialize又被甩了几条街。

一图顶千言

柱状图越矮小的性能越好
igbinary vs serialize vs json_encode 速度比较

试着开源LiteCloud项目

所谓LiteCloud,无非就是前些天提到的LightCloud的php版本实现。这个和原来的python版本有一些区别,会造成不兼容,如下:

    1. 把Consistent Hashing算法换成了ketama,在pecl的memcached扩展里有简单方法可以实现,效率比单纯的php好很多,能快个10倍吧。没有重复造轮子,因此我很是得意。
    2. 静态方法调用时,不再支持原来的system参数,原版是用这个来支持多个LiteCloud集群。
    3. 第一个版本,利用memcached扩展来读取tokyo tyrant,所以目前仅支持简单的操作比如get, set, delete, increment。其实再努力一下,也可以支持更多的功能,比如redis。
    4. 去掉了原版的local_cache功能,我觉得这个功能完全可以放在外面,更灵活。

项目主页

目前托管在github上 —— LiteCloud ,使用git以及github的时间不太长,但是很喜欢,欢迎fork。

使用示例

用静态方法调用:

require 'LiteCloud.php';

$config = array(
    'lookup1_A' => '127.0.0.1:41201',
    'lookup1_B' => '127.0.0.1:51201',

    'storage1_A' => '127.0.0.1:44201',
    'storage1_B' => '127.0.0.1:54201',
);

list($lookupNodes, $storageNodes) = LiteCloud::generateNodes($config);
LiteCloud::init($lookupNodes, $storageNodes);

LiteCloud::set('hello', 'world');
print LiteCloud::get("hello"); # => world
LiteCloud::delete("hello");

print LiteCloud::get("hello"); # => nil

或者采用实例化的方式调用,这种方式能够支持多个LightCloud集群

 require 'LiteCloud.php';

$config = array(
    'lookup1_A' => '127.0.0.1:41201',
    'lookup1_B' => '127.0.0.1:51201',

    'storage1_A' => '127.0.0.1:44201',
    'storage1_B' => '127.0.0.1:54201',
);

$cloud = new LiteCloud($config);

$cloud->set('hello', 'world');
print $cloud->get("hello"); # => world
$cloud->delete("hello");

print $cloud->get("hello"); # => nil

看上去和python版本差不多,对吧?

性能测试

这个部分是个重点,之前找到的lightcloud的php版本,性能比原版要慢一个数量级,我可不想在这个地方丢了份。

暂时做了个最简单的性能测试,测试脚本在test目录下。测试条件如下:

    1. 底层采用tokyo tyrant作为存储。
    2. 单个进程重复操作一万次,比如写一万次hello => world,这个测试条件和python版本完全一致
    3. 关闭了local_cache

python版本的测试结果


Finished "Tyrant set" 10000 times in 2.50 sec [4002.0 operations pr.sec]
Finished "Tyrant get" 10000 times in 1.04 sec [9583.7 operations pr.sec]
Finished "Tyrant delete" 10000 times in 7.39 sec [1352.4 operations pr.sec]

LiteCloud的测试结果


Using 1.8229308128357 time to set 10000 values. QPS:5485.67
Using 0.71097207069397 time to get 10000 values. QPS:14065.25
Using 2.1550960540771 time to delete 10000 values. QPS:4640.16

看上去还比较乐观,尤其是delete的性能翻了好几番,几乎要怀疑我在代码实现部分出了差错,在其它方面的表现也是全面超出,因为python版在hash_ring的实现上拖了后腿。

LightCloud的设计原理

LightCloud是最近看到的一个比较轻巧的分布式key-value数据库,尽管这类软件已经让人觉得审美疲劳,但我仍然觉得它的设计思路值得一提。

特色

除开其项目主页上列出来的特点不提,我觉得还能数得上的特色有:

    1. 理论上可以用任意key-value数据库做为底层存储,现在支持以tokyo tyrant或者redis作为底层的存储,如果使用redis可以获得更好的性能(大概提升30%~50%)
    2. 没有定制服务器端,基本上靠客户端语言来实现键值查找。优点是部署起来比较简单,缺点也是显而易见的,效率会有损失。
    3. 可以很方便的移植到其它语言上,我已经在github上找到一个ruby版本,甚至还有个php版本的实现。
    4. 可以方便的增加节点。
    5. 结构简单,方便hack

LightCloud的设计原理

Hash ring

LightCloud不能免俗的使用了一致性hash算法(Consistent Hashing),这是为了避免新增数据节点时发生集体拆迁事件。Consistent Hashing算法的原理请参考这里

last.fm的工作人员写的ketama算法算是比较常见的一致性算法,在libmemcached里大量使用。而LightCloud的作者当时还没发现合适的ketama python版,所以干脆自己捋起袖子写了个python版本的hash_ring,不到50行。这个是量身定制的,所以效率也还过得去,但是兼容ketama就别想了。

献上hash圈圈一个以明志:

LightCloud的hash环有什么与众不同?

其它分布式key-value数据库采用的办法是复制数据到多个节点上,例如Amazon Dynamo的复制策略图:

Dynamo用了许多办法解决consistent问题,系统相当复杂。而LightCloud直接使用tokyo tyrant的master-master复制功能,大幅简化了这部分的逻辑。所以在它的hash环上,单个节点其实是一对master-master的tokyo tyrant,焦不离孟不离焦。

在新增数据节点时,如果没有路由服务找到正确的服务器,可能会损失数据。那么LightCloud继续采用流氓手段解决这个问题,他又给上了个环,保证不会发生意外。这两个hash环里的节点仍然是之前提到的tokyo tyrant双人组,一个环叫lookup,记录了每一个key保存在哪个storage节点上;另外一个环叫storage,这是真正存放数据的地方。于是它的结构图变成了下面这个样子:

这部分比较难以理解,试着就上图阐述一下:

    1. 一个名叫A的家伙,住在storage_SB地区(storage ring)。同时,我们还告诉记性好的lookup_B(lookup ring),A君的地址为storage_SB。
    2. 很不幸或很幸运,咱们的数据膨胀到需要扩容了,于是新增了一个违章建筑storge_X,这个该死的建筑正好影响了我们找到A君。这时候,我们还可以问起记性好的lookup_B,A君住在哪个角落里啊? —— lookup_B日道:他就住在sotrage_SB一带~
    3. lookup这群家伙记性虽然好,但是也架不住人多,再也记不住这么多人的住址,所以又新来了几个记性好的lookup。这个会影响咱们找到storage住哪吗?答案是不会,因为没有新增别的违章storage建筑,咱们不需要问路也能找着人。

按照以上推论,一定要避免同时增加lookup和storage节点,这很可能会损失数据。

参考网页

memcache连接慢又一例

上次解决memcache连接慢问题以来,好长一段时间没在这个问题上翻过跟头。这一次我又在生产环境观察到php和memcache的连接时间经常会在50ms以上。

作为一个cache,占用了这么长的执行时间,天理何在?

实际的运行环境如下:

    • apache + mod_php
    • php-memcache扩展版本为2.2.5
    • memcache的并发连接数在400左右,相当少

这次memcache扩展用的是最新的稳定版,无可挑剔。所以刚开始我认为是网络环境的问题,于是直接采用telnet工具直连memcache进行测试,发现速度飞快!一点便秘感都没有!所以把目光仍然放回到memcache扩展上来,集中对比较慢的addServer方法各项参数进行排查。

Memcache::addServer方法

bool Memcache::addServer  ( string $host  [, int $port = 11211  [, bool $persistent  [, int $weight  [, int $timeout  [, int $retry_interval  [, bool $status  [, callback $failure_callback  [, int $timeoutms  ]]]]]]]] )

比对结果表明,$weight参数对memcache的连接时间有显著的影响,$weight的默认值为1,一旦设置为别的数值,连接时间便会由毫秒级变成50ms左右,立竿见影。

鉴于php-memcache扩展一贯恶劣的表现,俺不得不痛下决心迁移到新的memcached扩展上。memcached扩展基于libmemcached开发,而且提供了丰富的接口方法,应该是更好的选择。

启用memcached压缩注意事项

在php开发中,开启memcache的数据压缩存储是一件很简单的事情。在多数情况下,压缩数据不仅不会降低程序的执行效率,反倒会因为网络传输的开销降低,带来速度提升。看看最常用的Memcache::set方法:

bool Memcache::set  ( string $key  , mixed $var  [, int $flag  [, int $expire  ]] )

在这个方法中,将$flag设置为MEMCACHE_COMPRESSED即可启用memcache压缩存储。

这样做有什么弊端?

如果没有做额外判断,每一次写入memcache都会启用压缩,不管数据的大小。对应的,每次获得数据都需要做一次解压缩的操作,这是典型的一刀切手法。实际上在数据很小的情况下,不需要压缩,在这个基础上压缩省不了多少空间。

更好的压缩策略?

好了,我的想法是在数据超过一定大小(比如2k)的情况下,才开启压缩。这个好办,捋起袖子就干,在调用Memcache::set方法之前,首先判断一下数据的大小,一个strlen就搞定了,再简单不过了。

$memcache = new Memcache;
$memcache->connect('localhost', 11211);
$flag = strlen($data) > 2048 ? MEMCACHE_COMPRESSED : 0;
$memcache->set('mykey', $data, $flag);

有人可能会问了,array和object怎么办,这玩意可不能用strlen判断长度。

这还真能难住我一阵子,要知道把array/object写入memcache的时候,php会自动做serialize,再把它当作字符串插入memcache。

$flag = strlen(serialize($data)) > 2048 ? MEMCACHE_COMPRESSED : 0;

谁会采用这段代码?看起来非常山寨,而且serialize也不快,赔本买卖。

更好的办法!

上面的文字都是废话,直接看这段就好。Memcache::setCompressThreshold方法可以包办之前所有的逻辑。

Memcache::setCompressThreshold — Enable automatic compression of large values

bool Memcache::setCompressThreshold  ( int $threshold  [, float $min_savings  ] )

举个例子,下面这段会自动启用压缩策略,当数据大于2k时,以0.2的压缩比进行zlib。

$memcache->setCompressThreshold(2000, 0.2);

根据我的测试结果,setCompressThreshold方法会忽略Memcache::set的flag参数

starling试用手记

twitter最近将ruby实现的消息队列服务器starling开源了,这是一个支持memcache协议的轻量级持久化服务器,因此使用php/perl/ruby/java等多种客户端都没问题,可以将较慢的处理逻辑通过消息队列放在后台处理,同时也支持多点分布式处理。周末找了个闲置的centos 5机器搭了一套starling试用,配合php的memcache扩展测试一番,有点意思。

starling安装步骤

centos默认不带ruby,得重新装,以下安装步骤都是以root身份跑的。


yum install ruby ruby-devel rubygems
gem install memcache-client starling

如果你使用yum找不到ruby的相关包,请记得添加epel repository。安装之后新增的程序:

  • /usr/bin/starling #一个ruby脚本,starling的服务程序
  • /usr/bin/starling_top #starling的状态检查程序,可以查看starling的运行状态,类似memcache的状态显示,不同的是能够显示每个key的状态

starling启动后默认会在22122端口蹲点守候。

为了使用方便,我修改了一个starling在centos下的启动脚本,放在/etc/init.d/starling,下载地址:http://customcode.googlecode.com/files/starling。使用方法:

/etc/init.d/starling start|stop|restart

测试程序

以下是在测试中用的php脚本,说实话php在循环比较大的时候没啥优势,但是关键是简单,几行就搞定了。

写入的测试程序

#!/usr/bin/php
<?php
$m = new Memcache;
$m->addServer('127.0.0.1', '22122');

$start = microtime(true);

for($i = 0; $i < 10000; ++$i) {
    //echo $i, "\n";
    $m->set('testtesttesttest', '中文测试中文测试中文测试中文测试中文测试中文测试中中文测试中中文中a');
}
echo "time:" . (microtime(true) - $start), "\n";

读出来的测试程序


#!/usr/bin/php
<?php
$m = new Memcache;
$m->addServer('127.0.0.1', '22122');

while(1) {
    echo $m->get('test'), "\n";
    usleep(100); // 休息一下,否则容易cpu 100%
}

性能测试

测试条件:

  • key的长度16B
  • value的长度100B,
  • 8个并发写入进程
  • 每个进程插入10,000条记录

平均每个进程花了7秒完成写入操作,那么照这样计算:

10000 * 8 / 7 = 每秒写入11428次

以上测试进行的比较随意,而且我懒得插入大量数据来测试了,这个比较花时间,所以测试结果仅供参考。由于starling是目前twitter在生产环境中运行的,经过实践检验过,稳定性应该不成问题。

解决memcache连接奇慢问题一例

最近用xdebug观察线上程序的运行时间统计,发现往日里跑起来像飞的memcache居然是系统中拖后腿的耗时大户,连接时间特长。

运行环境

  • webserver是apache + php
  • php memcache extension版本是3.0.2,当时是最新的beta版…
  • 有4个memcache server可供使用
  • 代码中会利用php的Memcache::addServer依次连接四个memcache,长连接方式

现象

完成四次addServer一共需要300ms以上,但是一旦连接上,获取单个item飞快,时间在3ms以下。
更可恶的问题在于,虽然执行了四次Memcache::addServer,但是实际使用的始终是最后一个memcache,这实在让人崩溃。

问题解决

使用了一点搜索技巧,在pecl.php.net上找到了类似的bug: First get slow when using multiple memcached servers

这个bug的描述如下:

We are monitoring memcached performance and noticed that when we added a second memcached via Memcache::addServer the first get request is always slower than the subsequent ones although we are using persitent memcached connections. Switching from crc32 to fnv hashing didn’t help either. Is that delay explainable

看起来是最新的memcache extension有一些问题,尝试将这个扩展降级成最新的稳定版2.2.6,然后重启apache看看,memcache连接过慢的问题果然已经解决。

结论

吃螃蟹果然是要付出代价的。。

memcached Binary协议的ppt

memcached 1.3将开始支持Binary Protocol,下面是一篇介绍的ppt。

大概看了一遍,可以认为memcache的binary协议相对原来基于文本的协议,略快一些。key的长度可以到65536(2 bytes)。而memcache 1.3将仍然保持向后兼容,同时支持文本协议和binary协议。

从memcache中dump所有key的patch

在邮件组里看到这个补丁,能够将memcache中所有的key dump出来。

I have just finished a patch to dump all keys from memcached.
And I am glad to share this patch to anyone who wants to use it.

In the attachment, there are two python scripts which are used for dump all keys from a memcached server,
you can find the usage in the example.py script.

Any questions or advice can mail to we_2002 at 163.com

用法见example.py

下载: memcached-hack.zip