初次体验hiphop-php

昨天facebook在github上发布了hiphop-php的源代码。之前听说这玩意能把php代码翻译成c++代码,然后带来巨大的性能提升,所以第一时间编译了一份hiphop-php。

我的机器环境是

  • Centos 5.3 x86_64
  • 8G内存
  • Intel(R) Xeon(R) CPU E5420 @ 2.50GHz

安装注意事项

编译的时候碰到的问题很多,但是基本上都是按照wiki上的步骤进行的。我觉得比较重要的几点:

  • wiki上的Required Packages包包列表都要检查一遍,比如版本号,是否安装过,像binutils-dev这种就很容易忽略
  • 版本符合的话,直接用yum安装这些包就可以了
  • wiki上有类似Boost 1.37 is the minimum version字样,说明开发者可能就是在这个版本下开发的,我试了下最新版本的boost,编译到后来反而出错
  • 如果yum上没有符合版本的lib库,可以手动编译,但是编译时建议就放在自己的home下,比如:
    ./configure --prefix=/home/user
  • tbb Intel’s Thread Building Blocks这个包有些麻烦,记得按照wiki上说的步骤安装

测试hiphop-php

安装完成之后,时间也不是太多,所以我仅仅是简单的测试了一个php文件,代码如下:


<?php
$i = 0;
for($j = 0; $j < 1000000; $j++)
        $i += $j;

echo $i, "\n";
?>

用hphp进行编译:

hphp/hphp test.php --keep-tempdir=1 --log=3

提示生成新的可执行文件

/tmp/hphp_c9sbnG/program

做一下运行时间对比:


$ time php test.php
499999500000

real    0m0.307s
user    0m0.299s
sys     0m0.007s

$ time /tmp/hphp_c9sbnG/program
499999500000

real    0m0.259s
user    0m0.194s
sys     0m0.008s

没看出来编译成c++代码之后有太大的性能提升,估计是俺的使用手法问题?在邮件组里观察几天再说。

Update

facebook将优化之后的编译参数提交到了github,于是我重新编译并测试一遍这段相同的代码:


$ time /tmp/hphp_c9sbnG/program
499999500000

real    0m0.140s
user    0m0.076s
sys     0m0.006s

可以看到,经hiphop编译后的php,执行时间几乎快了一倍。

快速创建pear/pecl的rpm

目前使用的服务器为centos,使用yum以及rpm来维护系统好处多多:

    • 安装卸载,升级rpm软件包只需一条命令即可
    • 统一部署,便于维护
    • 比手工编译的方式要快

于是我需要把日常使用到的一些php扩展做成rpm包,但是手工写spec文件比较繁琐,因此pear的这个小工具PEAR_Command_Packaging帮了不少忙。它会给pear工具新增一个选项:

pear make-rpm-spec [options] 

这个命令行将会创建一个rpm的.spec文件,包含指定pear包的定义,而且也适用于pecl的php扩展。

创建pear rpm包的例子

$ cd /path/to/rpm-build-tree/SPECS
$ pear make-rpm-spec ../SOURCES/Net_Socket-1.0.tgz
Wrote RPM spec file PEAR::Net_Socket-1.0.spec
$ rpm -bb PEAR::Net_Socket-1.0.spec
...
Wrote: /path/to/rpm-build-tree/RPMS/noarch/PEAR::Net_Socket-1.0-1.noarch.rpm

关于make-rpm-spec的帮助

pear help make-rpm-spec

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开发,而且提供了丰富的接口方法,应该是更好的选择。

php的echo为什么这么慢

作为一个行走江湖多年的老中医,今天受命去解决一例前端页面展现缓慢的问题。问题页的情况如下:

    • apache + php
    • 使用smarty模板输出内容
    • 页面最终输出内容较大,80k+
    • 页面执行时间在500ms以上

祭出法宝xhprof对问题页面做了细致检查,发现页面的瓶颈竟然是模板(编译后的)中的一个echo语句,这个echo语句输出的字符串比较大,大概是50k+字节,花费时间为400多毫秒,占整个页面执行时间的80%。这样的echo输出在站点首页中其实是很常见的事情,没有数据库操作,按道理执行时间不应该这么长。

于是猛力使用搜索技能,最终在php手册的echo部分找到了一些蛛丝马迹,早在2003年就有前辈认为通过echo输出大字符串到客户端会引起服务器的性能问题,据我测试,在这个场景下使用print其实也是一样的慢。建议的解决办法是把字符串切割成更小的字符串输出,展现速度会有提升,输出函数如下:


<?php
function echobig($string, $bufferSize = 8192) {
    $splitString = str_split($string, $bufferSize);
    foreach($splitString as $chunk)
        echo $chunk;
}
?>

但是上面的处方不太对症,整个echobig的输出时间仍然在400毫秒左右,没有太大改善。

考虑到是输出大量内容到客户端比较慢,于是检查了apache的配置,原来还没打开deflate进行压缩,遂启用之。再次使用xhprof进行检查,这条echo的输出时间降低到5ms左右。

400ms到5ms,一个配置问题会产生80倍的差距,还真是省老钱了。这个故事告诉我们,压缩输出真的很重要。

Update

实际上这个问题也可以通过调整webserver的output buffer来解决一部分,参考:

在生产环境中使用php性能测试工具xhprof

xhprof是facebook开源出来的一个php性能测试工具,也可以称之为profile工具,这个词不知道怎么翻译才比较达意。跟之前一直使用的xdebug相比,有很多类似之处。以前对xdebug有一些记录还可以供参考,但是它的缺点是对性能影响太大,即便是开启了profiler_enable_trigger参数,用在生产环境中也是惨不忍睹,cpu立刻就飙到high。

而xhprof就显得很轻量,是否记录profile可以由程序控制,因此,用在生产环境中也就成为一种可能。在它的文档上可以看到这样一种用法:

以万分之一的几率启用xhprof,平时悄悄的不打枪。


if (mt_rand(1, 10000) == 1) {
 xhprof_enable(XHPROF_FLAGS_MEMORY);
 $xhprof_on = true;
}

在程序结尾处调用方法保存profile


if ($xhprof_on) {
 // stop profiler
 $xhprof_data = xhprof_disable();

 // save $xhprof_data somewhere (say a central DB)
 ...
}

也可以用register_shutdown_function方法指定在程序结束时保存xhprof信息,这样就免去了结尾处判断,给个改写的不完整例子:


if (mt_rand(1, 10000) == 1) {
 xhprof_enable(XHPROF_FLAGS_MEMORY);
 register_shutdown_function(create_funcion('', "$xhprof_data = xhprof_disable(); save $xhprof_data;"));
}

至于日志,我暂时用的是最土的文件形式保存,定期清除即可。

BTW:xhprof生成的图形方式profile真是酷毙了,哪段代码成为瓶颈,一目了然。

启用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参数

apache的RewriteMap使用心得

在apache的环境下,rewrite还真是生活之友啊,时不时就得用上。前些日子有个需求,要将url重新转一转。

什么情况?

原来的url
http://www.xxx.com/demo/oldpage.php?param1=1&param2=2

转换后的url
http://www.xxx.com/newpage.php?url=%2Fdemo%2Fmypage.php%3Fparam1%3D1&param2%3D2

需要把粗体部分的url进行urlencode,能看出上面的字符”?&=”都分别转义过,作为参数发给另外一个url。那么这时候请出rewrite还真是最合适不过了。

坎坷的Rewrite经历

查查rewrite手册,俺这才知道,转义这活,非得派出RewriteMap的map function才能做的比较漂亮。现在只有四个内部map function可供差遣:

  • toupper: Converts the key to all upper case.
  • tolower: Converts the key to all lower case.
  • escape: Translates special characters in the key to hex-encodings.
  • unescape: Translates hex-encodings in the key back to special characters.

那么很快就有了第一个rewrite出现:


RewriteMap escape int:escape
RewriteRule ^/([^/]*)$ /newpage.php?mi_url_suffix=${escape:$1?%{QUERY_STRING}} [L,PT]

注:这里的int不是intger的意思,它是internal的缩写,表示调用内部函数。

看上去非常简单,跑起来貌似也正….常?且慢,俺打开RewriteLog一瞅,形式不容乐观啊,”&”字符通通没有转义。看来是失败了,爬到狗狗上翻了一下,貌似escape对”?=”之类的特殊字符是不做转义的,晕。

RewriteMap到底

接着细看apache的rewrite手册,发现RewriteMap还支持自定义脚本,那么还得使出俺的看家绝技——php了。首先弄一个能转义的php,必须非常简单,复杂了apache容易挂掉,写出来发现想复杂都挺难啊:

/usr/local/bin/escape.php

#!/usr/bin/php -f
<?php
while($in = trim(fgets(STDIN)))
        fputs(STDOUT, urlencode($in) . "\r\n");
?>

在这个脚本里可别使用php:://stdin之类的,具体原因查php手册。相应的,rewrite规则如下:


RewriteMap escape prg:/usr/local/bin/escape.php
RewriteRule ^/([^/]*)$ /newpage.php?mi_url_suffix=${escape:$1?%{QUERY_STRING}} [L,PT]

rewrite规则没有太大的改变,prg表示使用自定义脚本。现在这个版本总算正常运作了。

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在生产环境中运行的,经过实践检验过,稳定性应该不成问题。

131个字符的php framework

friendfeed上看到这个链接 —— The 140 Characters Webapp Challenge!,这个比赛要求用140个字符的代码造就一个web应用。

里头有36个程序可供投票,基本上全是脚本语言大杂烩:php,perl,ruby,javascript。实现的应用也是五花八门,有相册,类twitter,小游戏,甚至还有php代码框架?摘录如下:

require __DIR__.'/c.php';
if (!is_callable($c = @$_GET['c'] ?: function() { echo 'Woah!'; }))
        throw new Exception('Error');
$c();

这段代码利用了php5.3的一些新特性:

  • __DIR__
  • Anonymous functions
  • ?:运算符

代码只有131个字符,由于代码极为简陋,安全性也是没得保障的,只能算一个程序的统一入口罢了。

如果用php 5.2来写这段代码,大概就是:

require dirname(__FILE__).'/c.php';
if (!is_callable($c = @$_GET['c'] ? $_GET['c'] : create_function('', "echo 'Woah!';")))
        throw new Exception('Error');
$c();

如果要让这段代码变得实用点,可以在$c前面加上一个前缀,这样安全性会有进一步提升,代码也会相应的增加若干字节。

解决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连接过慢的问题果然已经解决。

结论

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