Posts tagged ‘php’

记录log,对于很多人而言是很简单或者低级的事情。但是,随着项目经验的增长,遇到生产环境中bug数的增多,至少对于我来说,日志的重要性日益增加。

这次,需要对项目中log类进行重构,主要希望实现4个目的:

  1. 建立日志监控机制,第一时间发现问题
  2. 协助定位问题,帮助快速解决问题
  3. 记录用户行为,协助解答客户疑问
  4. 记录用户行为,协助制定安全与个性化等策略

除了这些功能性的目的,由于log类在一次请求中的调用频率相对较高,且与基本业务无关,如果性能方面有问题的话,就太本末颠倒了,所以先从性能说起。

log一般记录在文件里,所以其本质上是写文件,使用php作为开发语言的话,主要考虑了3个方面:

  1. 选择fwrite还是file_put_contents?
  2. 是否使用debug_backtrace函数获取文件名和行号?
  3. 怎样保证并发写的原子性?

选择fwrite还是file_put_contents?

php有多种写文件的函数,fwrite需要先fopen获取到文件句柄,而file_put_contents直接使用文件名即可,且传入的data参数可以是字符串,也可以是数组,甚至stream,使用较简单。

zend框架的Zend_Log_Writer_Stream类,使用的是fwrite函数;而公司内部多个team的log封装都使用了file_put_contents函数。首先考虑,我们的log类,给谁用?内部使用,暂时没考虑开源。传入的参数,是否需要支持string,array or stream?记录log而已,支持string即可,而且log的基本样式是每次记录一行。所以,比较两者在写入多行数据之间的性能区别即可:

$str = "abc\n";
$n = 10000;
$filename = 'test_write.txt';

$start_time = mytime();
write_with_open($filename, $str, $n);
$used_time = mytime() - $start_time;
printf("%20s%s\n","fwrite","Time used: $used_time ");

$start_time = mytime();
write_without_open($filename, $str, $n);
$used_time = mytime() - $start_time;
printf("%20s%s\n","file_put_contents","Time used: $used_time ");

$start_time = mytime();
write_with_errorlog($filename, $str, $n);
$used_time = mytime() - $start_time;
printf("%20s%s\n","error_log","Time used: $used_time ");

function write_with_open($filename, $str, $n){
        $fp = fopen($filename, 'a') or die("can't open $filename for write");

        while($n--){
                fwrite($fp, $str);
        }

        fclose($fp);
}

function write_without_open($filename, $str, $n){
        while($n--){
                file_put_contents($filename, $str, FILE_APPEND);
        }
}

function write_with_errorlog($filename, $str, $n){
        while($n--){
                error_log($str, 3, $filename);
        }
}

执行该测试脚本的结果是:

fwriteTime used: 0.018175840377808
file_put_contentsTime used: 0.22816514968872
error_logTime used: 0.2338011264801

可见fwrite的性能要远远大于另外两者,直观上看,fwrite仅关注于写,而文件句柄的获取仅由fopen做一次,关闭操作也尽有一次。如果修改write_with_open函数,把fopen和fclose函数放置到while循环里,则3者的性能基本持平。

以上结论,也可以通过查看PHP源代码得知,fwrite和file_put_contents的源码都在php-5.3.10/ext/standard/file.c里,file_put_contents不但逻辑较为负载,还牵涉到open/锁/close操作,对于只做一次写操作的请求来说,file_put_contents可能较适合,因为其减少了函数调用,使用起来较为方便。而对于log操作来说,fwrite从性能角度来说,较为适合。

2012-06-22补充:

以上只是单纯从“速度”角度考虑,但是在web的生产环境里,如果并发数很高,导致系统的open files数目成为瓶颈的话,情况就不同了!fwrite胜在多次写操作只用打开一次文件,持有file handler的时间较长;而file_put_contents每次都在需要的时候打开文件,写完就立即关闭,持有file handler的时间较短。如果open files的值已无法调高,那么使用file_put_contents在这种情况下,就会是另外一种选择了。

是否使用debug_backtrace函数获取文件名和行号?

在gcc,标准php出错信息里,都会给出出错的文件名和行号,我们也希望在log里加上这两个信息,那么是否使用debug_backstrace函数呢?

class A1{
        public static $_use_debug=true;
        function run(){
                # without debug_backtrace
                if (!self::$_use_debug){
                        #echo "Quit\n";
                        return;
                }

                # with debug_backtrace
                $trace = debug_backtrace();
                $depth = count($trace) - 1;
                if ($depth > 1)
                        $depth = 1;
                $_file = $trace[$depth]['file'];
                $_line = $trace[$depth]['line'];

                #echo "file: $_file, line: $_line\n";
        }
}

class A2{
        function run(){
                $obj = new A1();
                $obj->run();
        }
}

class A3{
        function run(){
                $obj = new A2();
                $obj->run();
        }
}
class A4{
        function run(){
                $obj = new A3();
                $obj->run();
        }
}
class A5{
        function run(){
                $obj = new A4();
                $obj->run();
        }
}
class A6{
        function run(){
                $obj = new A5();
                $obj->run();
        }
}

$n = 1000;
$start_time = mytime();
A1::$_use_debug = true;
$obj = new A6();
while($n--){
        $obj->run();
}
$used_time = mytime() - $start_time;
printf("%30s:%s\n", "With Debug Time used", $used_time);

$n = 1000;
$start_time = mytime();
A1::$_use_debug = false;
$obj = new A6();
while($n--){
        $obj->run();
}
$used_time = mytime() - $start_time;
printf("%30s:%s\n", "Without Debug Time used", $used_time);

function mytime(){
        list($utime, $time) = explode(' ', microtime());
        return $time+$utime;
}

运行的结果是:

flykobe@desktop test $ php test_debug_trace.php
With Debug Time used:0.005681037902832
Without Debug Time used:0.0021991729736328

但是若多次运行,少数情况下,with debug版本甚至与快于without版本。综合考虑,还是决定不用debug_backtrace,而由调用者传入__FILE__和__LINE__值。

怎样保证并发写的原子性?

写文件时,大致有这3种情况:

  1. 一次写一行,中间被插入其他内容
  2. 一次写多行,行与行之间被插入其他内容
  3. 多次写多行,行与行之间被插入其他内容

对于web访问这种高并发的写日志而言,一条日志一般就是一行,中间绝不允许被截断,覆盖或插入其他数据。但行与行之间,是否被插入其他内容,并不care。既然之前是决定采用fwrite,php手册上说的很清楚:

If handle was fopen()ed in append mode, fwrite()s are atomic (unless the size of string exceeds the filesystem’s block size, on some platforms, and as long as the file is on a local filesystem). That is, there is no need to flock() a resource before callingfwrite(); all of the data will be written without interruption.

即当fwrite使用的handler是由fopen在append模式下打开时,其写操作是原子性的。不过也有例外,如果一次写操作的长度超过了文件系统的块大小,或者使用到NFS之类的非local存储时,则可能出问题。其中文件系统的块大小(在我的centos5虚拟机上是4096 bytes)可以通过以下命令查看:

sudo /sbin/tune2fs -l /dev/mapper/VolGroup00-LogVol00 | grep -i block

这同样可以通过模拟多种不同情况的fwrite操作来验证,由于比较简单不再赘述代码。

————————-

2012-07-03添加

《unix环境高级编程》里说:Unix系统提供了一种方法使这种操作成为原子操作,即打开文件时,设置O_APPEND操作,就使内核每次对这种文件进行读写之前,都将进程的当前偏移量设置到该文件的尾端处。

而php手册中,指的某些platform应该不包含*nix实现。故可认为,在我们的环境下,php的fwrite函数是原子性的。

在以上几种比较的基础上,初步完成了我们的Log类封装,进行了千行和万行级别的log写入测试,性能提高了3倍,但应该仍然有优化余地。

使用php发送邮件很简单,实际利用的是linux自带的smtp服务。这里不加赘述。

邮件服务,我的理解是数据包与包的交互。发送邮件时,smtp服务接收client程序的邮件标题、正文、from、to、优先级等信息,拼装为smtp数据包,网络发送给to地址所在的邮件服务器。接收邮件时,smtp服务从网络上的邮件服务器接收数据包,解析出to的email地址,利用推或拉的方式传递给client。

php中的mail函数,作为一种client形式,在其help里写的很清楚:

reject note 返回值

Returns TRUE if the mail was successfully accepted for delivery, FALSE otherwise.

It is important to note that just because the mail was accepted for delivery, it does NOT mean the mail will actually reach the intended destination.

即mail的返回值仅代表php.ini里配置的smtp server是否接收发送请求成功,而并不代表最终的to email接收成功。

进而推测,smtp对mail请求应当是异步的,那么也许smtp的log中会有邮件接收的真正状态。进入centos系统的/var/log,果然有maillog!

Feb  7 09:21:45 v080106 sendmail[32612]: q171LiwL032612: to=abc@sohu.com, ctladdr=admin (500/500), delay=00:00:01, xdelay=00:00:01, mailer=relay, pri=32760, relay=[127.0.0.1] [127.0.0.1], dsn=2.0.0, stat=Sent (Ok: queued as AC58F1060001)

这是一条发送成功的邮件记录。各字段意义为:

Feb  7 09:21:45 发送时间
v080106 发送者机器名称
sendmail[32612]: 进程号
q171LiwL032612: 唯一Message-ID,可以根据该字段,找到log里与本次发送相关的所有记录
to=abc@sohu.com, 接收者邮箱
ctladdr=admin (500/500)
delay=00:00:01, xdelay=00:00:01, 延时
mailer=relay, 邮件服务器,是relay,则参考下面的relay字段可知具体信息
pri=32760, 优先级
relay=[127.0.0.1] [127.0.0.1], 中继
dsn=2.0.0, smtp code (200表示成功)
stat=Sent (Ok: queued as AC58F1060001) 状态说明,Sent代表发送成功

Feb  8 01:38:48 v080106 postfix/smtpd[11264]: F1E521060007: client=localhost.localdomain[127.0.0.1]

Feb  8 01:38:49 v080106 postfix/cleanup[11267]: F1E521060007: message-id=<201202071738.q17HcmEB011273@v080106.isv>

Feb  8 01:38:49 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 01:38:49 v080106 sendmail[11273]: q17HcmEB011273: to=abc@vip.sohu.com, ctladdr=admin (500/500), delay=00:00:01, xdelay=00:00:01, mailer=relay, pri=32807, relay=[127.0.0.1] [127.0.0.1], dsn=2.0.0, stat=Sent (Ok: queued as F1E521060007)

Feb  8 01:38:49 v080106 postfix/smtp[11270]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=0.25, delays=0.05/0/0.15/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 02:04:59 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 02:05:00 v080106 postfix/smtp[22089]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=1572, delays=1571/0.85/0.13/0.04, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 02:38:26 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 02:38:26 v080106 postfix/smtp[28707]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=3577, delays=3577/0.01/0.14/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 03:45:54 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 03:45:54 v080106 postfix/smtp[10772]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=7625, delays=7625/0.03/0.08/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 05:08:20 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 05:08:30 v080106 postfix/smtp[19312]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=12581, delays=12572/9.5/0.11/0.04, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 06:31:47 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 06:31:47 v080106 postfix/smtp[21697]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=17578, delays=17578/0/0.08/0.05, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 07:55:00 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 07:55:00 v080106 postfix/smtp[22783]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=22572, delays=22571/0.02/0.11/0.37, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 09:01:41 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 09:01:41 v080106 postfix/smtp[23869]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=26572, delays=26572/0/0/0, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

Feb  8 10:08:21 v080106 postfix/qmgr[18249]: F1E521060007: from=<admin@v080106.isv>, size=3258, nrcpt=1 (queue active)

Feb  8 10:08:21 v080106 postfix/smtp[24922]: F1E521060007: to=<abc@vip.sohu.com>, relay=mx.vip.sohu.com[61.135.132.86]:25, delay=30572, delays=30572/0/0/0, dsn=4.1.8, status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))

以上是一组失败的记录,唯一号是F1E521060007。可以看到,当接收到失败之后,sendmail进行了多次重试。里面还有一些疑问,之后再解决,现在主要关心错误信息:

relay=mx.vip.sohu.com[61.135.132.86]:25, 中继
dsn=4.1.8, # 错误码
status=deferred (host mx.vip.sohu.com[61.135.132.86] said: 450 4.1.8 <admin@v080106.isv>: Sender address rejected: Domain not found (in reply to RCPT TO command))  # 具体描述,延迟发送,mx.vip.sohu.com认为无法找到发送者的domain

还有一类统计数据:

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: start interval Feb  8 10:41:42

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: domain lookup hits=1692 miss=209 success=89%

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: address lookup hits=947 miss=9428 success=9%

Feb  8 10:50:21 v080106 postfix/scache[25440]: statistics: max simultaneous domains=4 addresses=9 connection=30

有了以上这些信息,就可以准备的跟踪真实的邮件到达率了。当然,是否被认为是垃圾邮件之类,是无法从这里看出来的。

———————————————————

以gmail为例,可以点击邮件右上方的more按钮->show original查看到邮件头信息:

Delivered-To: abc@gmail.com
Received: by 10.182.176.71 with SMTP id cg7cs142014obc;
        Tue, 7 Feb 2012 15:44:47 -0800 (PST)
Received: by 10.68.218.167 with SMTP id ph7mr39734334pbc.110.1328658286305;
        Tue, 07 Feb 2012 15:44:46 -0800 (PST)
Return-Path: <201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com>
Received: from smtp-out-184-237.amazon.com (smtp-out-184-237.amazon.com. [207.171.184.237])
        by mx.google.com with ESMTP id e6si262857pbj.138.2012.02.07.15.44.45;
        Tue, 07 Feb 2012 15:44:46 -0800 (PST)
Received-SPF: pass (google.com: domain of 201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com designates 207.171.184.237 as permitted sender) client-ip=207.171.184.237;
Authentication-Results: mx.google.com; spf=pass (google.com: domain of 201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com designates 207.171.184.237 as permitted sender) smtp.mail=201202072344452bac1460e73a42f2b86762ab94c6272d-C29QW58DB99VUH@bounces.amazon.com
Date: Tue, 7 Feb 2012 23:44:45 +0000 (UTC)
From: "Amazon.cn" <store-news@amazon.cn>
To: "abc@gmail.com" <abc@gmail.com>
Message-ID: <677952103.17204211328658285803.JavaMail.em-build@massmail-sender-fe-9004.sea19.amazon.com>

SPF的状态我看到的有pass(可信)、neutral(中性),有些垃圾邮件的状态也是pass的。
大批量发送广告邮件时,目前收集到的需要注意:

  1. 控制发送频率,10个以上ip并行发送,一天控制在几十万的数量
  2. 更真实的header信息,通过配置发件域名等信息
  3. 如果可能,同邮件服务商沟通,争取进入白名单

邮件域名配置:

  1. 主机A记录abc.com
  2. 主机A记录mail.abc.com
  3. 主机A记录www.abc.com
  4. 主机A记录ftp.abc.com
  5. abc.com 到 mail.abc.com的MX记录

另外还有反向解析相关的配置。比如我们的发件地址是news@mail.flykobe.com,则需要在邮件服务器里将HELO地址配成mail.flykobe.com,并且按照上面的方法配置好各种相关域名到固定ip。同时,还需要将ip到mail.flykobe.com做一个映射(这个需要联系DNS提供商)。我们既无固定ip,就无法实现这个了!可以参考:http://hi.baidu.com/yangming/blog/item/323d034ff87a103fafc3ab8a.html

————————————————————-

记录一下利用postfix,即开源的邮件传输代理,搭建邮件服务器的过程。

网上资料可见,需要以下几个套件:

  • SMTP 認證套件: cyrus-sasl ( cyrus-sasl-1.5.27-5mdk 以及其他認證機制函式庫 )
  • Postfix 郵件伺服器: postfix ( postfix-1.1.11-4mdk )
  • POP3 伺服器: imap ( imap-2001a-9mdk, imap-devel-2001a-9mdk 兩個 ) # 最后没有用这个
  • 郵件分析軟體:procmail ( procmail-3.22-3mdk )

我们的项目需要往几个平台发微博,而由于各个平台接口稳定性和调用时间不同,而且有可能需要将用户的一条微博同步到多个平台去,所以不太适合做同步调用。考虑到有两种实现方式:1、利用php的socket接口,读取完POST、GET参数后即关闭socket,然后继续处理发送微博;2、队列方式。

正好当前手头上有个新浪SAE的账号,而它提供了TaskQueue队列,就试了下。

主要接口就两个:/recv.php 和 /worker.php。

recv.php接收用户请求,必须的参数有:app_key, app_key_secret,access_key,access_key_secret,platforms和微博内容。其中platforms用逗号分隔多个平台名称,如“sina,qq”。这里明文传递app_key, app_key_secret,access_key,access_key_secret其实不大安全,之后会对其进行加密。

        $task = array();
        $ret_uniq = array();
        foreach($platforms_arr as $p){
                if(($uniq_id = save_recv($app_key, $app_key_secret, $access_key, $access_key_secret, $p, $text))<=0){                         return_error("系统错误,请稍后再试");                 }                 // Make package                 $postdata = sprintf('uid=%d&%s=%s', $uniq_id, MY_KEY_NAME, MY_KEY_VAL);                 $task[] = array('url'=>DOMAIN."/worker.php", "postdata"=>$postdata);

                // prepare return values
                $ret_uniq[$p] = $uniq_id;
        }

        // save it to queue
        $queue = new SaeTaskQueue('packages');
        $queue->addTask($task);
        $ret = $queue->push();
        if ($ret === false){
                mylog("push fail: no.".$queue->errno().' ,'.$queue->errmsg());
                return_error("无法加入到队列中,请重试");
        }

        // return uniq id
        return_ok($ret_uniq);

由于SAE对于worker的执行时间有限制,所以将每个平台作为一个package存储,以减少每一次worker的运行时间。以上代码,将package的具体信息存储在数据库中,将唯一key作为worker的执行参数。

worker.php获取到唯一key,从数据库中读取到package的具体信息,通过package的status检查该package是否被处理过,然后调用send方法发送微博。

       function send(array $package){
                if (empty($package) || empty($package['secret']) || empty($package['platforms'])
                                || empty($package['content'])){
                        return false;
                }

                if(!($token = explode(',', $package['secret']))){
                        return false;
                }
                list($app_key, $app_key_secret, $access_key, $access_key_secret) = $token;

                if(!($platforms = explode(',', $package['platforms']))){
                        return false;
                }

                foreach($platforms as $p){
                        $wp = O_Weibo::create($p, $token);
                        $wp->send($package['content']);
                }

                return true;
        }

这里send方法仍然支持逗号分隔的多platforms。针对每一个platform名称,调用抽象工程类O_Weibo::create生成其对应的object。例如,新浪的微博类方法:
                protected function __construct(array $oauth){
                        if (empty($oauth)){
                                throw new O_WeiboException("We need app_key, app_key_secret, oauth_token and oauth_token_secret");
                        }
                        $this->app_key = trim($oauth[0]);
                        $this->app_key_secret = trim($oauth[1]);
                        $this->oauth_token = trim($oauth[2]);
                        $this->oauth_token_secret = trim($oauth[3]);

                        // Maybe we should check this oauth keys correct?

                        try{
                                $this->client = new WeiboClient( $this->app_key, $this->app_key_secret, $this->oauth_token , $this->oauth_token_secret);
                        } catch (Exception $e){
                                throw new O_WeiboException("Can't init WeiboClient: ".$e->get_message());
                        }
                }

                public function send($text){
                        return $this->client->update($text);
                }

另外,由于SAE禁止了curl系列函数,改为提供SaeFetchurl类,所以需要对新浪微博的SDK中的http函数进行修改:
                        $this->http_info = array(); 

                        $f = new SaeFetchurl();

                        $f->setHeader(CURLOPT_USERAGENT, $this->useragent);
                        $f->setHeader(CURLOPT_RETURNTRANSFER, TRUE);
                        $f->setHeader(CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer); 
                        $f->setHeader(CURLOPT_HEADERFUNCTION, array($this, 'getHeader')); 
                        $f->setHeader(CURLOPT_HEADER, FALSE); 

                        $f->setMethod($method);

                        switch ($method) { 
                        case 'POST': 
                                if (!empty($postfields)) { 
                                        $f->setPostData($postfields);
                                } 
                                break; 
                        case 'DELETE': 
                                if (!empty($postfields)) { 
                                        $url = "{$url}?{$postfields}"; 
                                } 
                        } 

                        $header_array = array(); 
                        
                        $header_array2=array(); 
                        if( $multi ) 
                                } 
                        } 

                        $header_array = array(); 
                        
                        $header_array2=array(); 
                        if( $multi ) 
                                $header_array2 = array("Content-Type: multipart/form-data; boundary=" . OAuthUtil::$boundary , "Expect: ");
                        foreach($header_array as $k => $v) 
                                array_push($header_array2,$k.': '.$v); 

                        $f->setHeader(CURLOPT_HTTPHEADER, $header_array2 ); 
                        $f->setHeader(CURLINFO_HEADER_OUT, TRUE ); 

                        //$f->debug(true);

                        $response = $f->fetch($url);
                        
                        // TODO: process error

                        $this->http_code = $f->httpCode();
                        //$this->http_info = array_merge($this->http_info, curl_getinfo($ci)); 
                        $this->url = $url; 

                        return $response; 

我们的登录系统,使用了php的session机制,与cookie相比,无疑它是很安全的。同时,为了提升用户体验,页面上大量的使用了ajax延迟加载,而这些加载的数据,多是要求登录的,也就是说加载时需要进行session的验证。

这样造成的结果是,用户打开一个页面,先看到页面框架load完毕,然后浏览器发出一堆的ajax请求,这些ajax请求变成了串行的!在网络或者服务器繁忙的时候,用户体验可想而知!

那为什么session会造成这样的后果呢?PHP手册上有这样的句子(http://www.php.net/manual/en/function.session-write-close.php

session data is locked to prevent concurrent writes only one script may operate on a session at any time

也就是说session_start函数调用后,会对session加锁,以避免其他web请求修改了session的内容,而且从这里可以看到,该锁是彻底的互斥锁。针对该问题,网上也有人给出了解决方案,即在session打开并使用完毕之后,立即session_write_close掉,以允许其他web请求正常进行。另外,既然函数名称为write close,经测试,调用之后,仍然可以读取$_SESSION中的值,但是写入则失败。

session_start();
$_SESSION[‘test’] = ‘abcd';
session_write_close();
$_SESSION[‘a’] = ‘aaaa';
var_dump($_SESSION);

这里,立即var_dump session可以看到a=>’aaaa’,但是它并没有真的写入,在其他页面进行请求,是请求不到的。

当然,另一方面,我们也可以使用cookie来避免这种竞争的存在。由于cookie对于用户是可见的,所以一些敏感内容不可以直接存储,需要进行加密。自己实现加密算法未免有些重复造轮子,所以我从Cake框架中摘取出了一段加密代码:

下载地址:http://flykobetest-xfiles.stor.sinaapp.com/2cfc19a8c680453cf374317c946bddad.txt

最近在做基于OAUTH的一些事情,其所需的oauth_token,oauth_token_secret,user_id原先都是以数组的形式存储在session中的。而cookie不支持array,而我又不想在一个域名下建立多个cookie的key,所以就可以将需要存储的数组转化为GET参数的形式,以&和=连接。这里需要注意的是,key和value中都可能出现我们的连接符号&和=,所以在编码前,请对key和value分别urlencode。

最近项目中,出于内存效率考虑,对函数的返回值采取了值引用的方式。在control层的函数中,声明某变量为global的,然后调用该值引用函数进行初始化。然后在view层中,试图使用该global的变量失败,其值为NULL。

而如果函数直接返回值,而非引用方式,则正常。

简化代码如下:

function &fun(){
        $arr = array(
                        'a' => 1,
                        'b' => 2
                        );
        return $arr;
}

function child(){
        global $product;
        $product =& fun();
}

function child2(){
        $GLOBALS['product'] =& fun();
}

child();
global $product;
var_dump($product);

child2();
global $product;
var_dump($product);

调用child函数后,product为NULL,而调用child2函数后,product有值。

在两个global $product之后,分别调用xdebug_debug_zval函数,可以看到以下输出:

product: (refcount=1, is_ref=1)=NULL
product: (refcount=1, is_ref=1)=array (‘a’ => (refcount=1, is_ref=0)=1, ‘b’ => (refcount=1, is_ref=0)=2)

即child之后,product变量存在,但是其值为NULL;而child2之后,product变量和其值都存在。

在php手册里看到这么一句话:

在一个函数域内部用 global 语句导入的一个真正的全局变量实际上是建立了一个到全局变量的引用

这意味着,在child函数中,声明了一个指向global product”指针”的引用,并且改变了改引用的值。翻译成C语言即(注意:PHP的引用不是指针地址的引用,是符号表别名,后面会介绍。所以这里翻译成C代码,仅仅是表述其意。):

char * product = null;
void init(char*product){
    product = (char*)malloc(10);
}

那么很明显该product不会被初始化。

进一步查看php的引用原理,发现如下解释:

如果在一个函数内部给一个声明为 global 的变量赋于一个引用,该引用只在函数内部可见。可以通过使用 $GLOBALS 数组避免这一点

把 global $var; 当成是 $var =& $GLOBALS[‘var’]; 的简写。从而将其它引用赋给 $var 只改变了本地变量的引用。

<?php
$var1
= “Example variable”;
$var2 = “”;

function global_references($use_globals)
{
global
$var1, $var2;
if (!
$use_globals) {
$var2 =& $var1; // visible only inside the function
} else {
$GLOBALS[“var2″] =& $var1; // visible also in global context
}
}

global_references(false);
echo
“var2 is set to ‘$var2‘\n”; // var2 is set to ”
global_references(true);
echo
“var2 is set to ‘$var2‘\n”; // var2 is set to ‘Example variable’
?>

从php手册上可以看到,php的引用区别于C等语言,它不是指针,是“符号表别名”。

php引用与C引用的区别

php引用与C引用的区别

图画的简陋了点 :( ,在php中如果$q=&$p,则它们指向同一块地址,但是p和q之间没有直接关系。而c中,如果q=&p,则q是指向指针的指针,改变**q才会改变*p的值。

参考:

引用计数基本知识  http://php.net/manual/zh/features.gc.refcounting-basics.php

回收周期(Collecting Cycles) http://php.net/manual/zh/features.gc.collecting-cycles.php

变量范围 http://php.net/manual/zh/language.variables.scope.php

代码上传到了google code:http://code.google.com/p/crawl-tianya-php/

在天涯论坛看小说,水太多了

只想看楼主,想存为文本,想放在ipad、手机里看?

所以写了这个php脚本,利用 http://phpdom.comsing.com 解析html代码,根据第一篇帖子的url,抓取后续所有页面的楼主内容。

下载之后,解压,查看read.php里的php路径是否正确,执行方式:

./read.php “http://www.tianya.cn/publicforum/content/funinfo/1/2309593.shtml” tmp.txt

最近在做的项目,需要为其他产品提供一些接口,如果由QA测试的话,就还需要一个图形化的页面,而直接看接口的返回值,都是json字符串,就感觉比较麻烦和不靠谱。

所以,使用了PHP自带的run-tests.php,实现一些自动化测试的功能。刚开始玩这个,理解较浅。仅给一个具体的例子。

比如,针对某个接口http://interface.a.com/ri.php?act=test&id=23&cnt=1,返回值是:

{“Result”:1,”Msg”:[{“ID”:12345,”BlogId”:890,”Title”:”\u4e00\u5f00\u59cb\u7684\u672a\u6765\u3002\uff09″,”Description”:”\u4e00\u5929\u6bd4\u4e00\u5929\u7737\u604b\u3002\uff09 ……- -\u3001\u7761\u7720\u3002 . \u81ea\u52a9\u9910\u2014\u2014\u5168\u5e26\u58f3\u7684\u98df\u7269\u3002 \u72fc\u7c4d\u4e00\u7247\u3002 \u2014\u2014\u2014\u2014\u751f\u6d3b…”,”DateAdded”:”Sep 14 2010 10:38AM”}]}

其中Result是用来标示接口处理是否成功的,如果成功则具体信息是在Msg里。在这种情况下,Msg是一个数组,数组中每个元素可以decode为一个stdClass的对象。利用反射,我们可以取得该对象的属性名称。(其实decode为数组,直接获取其keys也可以)

那么如果直接写代码,为:

$jsonStr = get_http_result(“http://interface.a.com/ri.php?act=test&id=23&cnt=1″);
$obj = json_decode($jsonStr);
$reflect = new ReflectionObject($obj->Msg[0]);
$props = $reflect->getProperties();
sort($props); # 排序,以保证输出唯一
foreach ($props as $prop) {
print $prop->getName() . “\n”;
}
function get_http_result($url) {
assert(strlen($url));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, “$url”);
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if( $info[‘http_code’] != 200 )
return ”;
return $result;
}

在这里,我只想进行简单的测试,确保接口返回的keys是一致的就可以了,所以以上代码满足我的要求。那么写入到自动化测试脚本为001.phpt:

–TEST–

interface test

–SKIPIF–

–FILE–

<?php

$jsonStr = get_http_result(“http://interface.a.com/ri.php?act=test&id=23&cnt=1″);
$obj = json_decode($jsonStr);
$reflect = new ReflectionObject($obj->Msg[0]);
$props = $reflect->getProperties();
sort($props); # 排序,以保证输出唯一
foreach ($props as $prop) {
print $prop->getName() . “\n”;
}
function get_http_result($url) {
assert(strlen($url));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, “$url”);
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if( $info[‘http_code’] != 200 )
return ”;
return $result;
}

?>

–EXPECTF–

BlogId

DateAdded

Description

ID

Title

在保证当前目录可写的情况下,从php源代码复制run-tests.php到当前目录,并设置export TEST_PHP_EXECUTABLE=”/path/to/php/bin/php”,执行:

php run-tests.php 001.phpt,看到的结果应该类似:

=====================================================================

CWD         : /curwork/web/autotest

PHP         : /usr/local/php/bin/php

PHP_SAPI    : cli

PHP_VERSION : 5.2.5

ZEND_VERSION: 2.2.0

PHP_OS      : Linux

INI actual  : /etc/php.ini

More .INIs  :

Extra dirs  :

=====================================================================

Running selected tests.

PASS rec fashion/feeling/travel blogs [001.phpt]

=====================================================================

Number of tests :    1                 1

Tests skipped   :    0 (  0.0%) ——–

Tests warned    :    0 (  0.0%) (  0.0%)

Tests failed    :    0 (  0.0%) (  0.0%)

Tests passed    :    1 (100.0%) (100.0%)

———————————————————————

Time taken      :    0 seconds

=====================================================================

IBM的文档很清楚:http://www.ibm.com/developerworks/cn/opensource/os-cn-php-autotest/index.html

经常的需求是,由其他地方获取到一个类的名称,需要生成该类的对象。
直接使用这样的语法是肯定行不通的:

$classname='Test';
$obj = new $classname();

可以给目标类定义一个static的方法生成该对象的实例,然后通过call_user_func或call_user_func_array调用该方法。当然,也可以通过工厂类等方法来实现。

class Test{
var $val;
function __construct(){
$this->val = 'test';
}
static function create(){
static $instance ;
if (!$instance){
$instance = new Test();
}
return $instance;
}
}
$className = 'Test';
$obj = call_user_func(array($className, 'create'));
var_dump($obj);

Hadoop的分布式文件系统HDFS为java提供了原生的接口,可以像访问本地文件一样的,对HDFS中的文件进行增删改查等操作。

对于其他非java语言的支持,hadoop使用了Thrift

对于该方式,hadoop里针对thrift的hadoop-0.20.2/src/contrib/thriftfs/readme是这样说的:

Thrift is a software framework for scalable cross-language services
development. It combines a powerful software stack with a code generation
engine to build services that work efficiently and seamlessly
between C++, Java, Python, PHP, and Ruby.
This project exposes HDFS APIs using the Thrift software stack. This
allows applciations written in a myriad of languages to access
HDFS elegantly.
The Application Programming Interface (API)
===========================================
The HDFS API that is exposed through Thrift can be found in if/hadoopfs.thrift.
Compilation
===========
The compilation process creates a server org.apache.hadoop.thriftfs.HadooopThriftServer
that implements the Thrift interface defined in if/hadoopfs.thrift.
Th thrift compiler is used to generate API stubs in python, php, ruby,
cocoa, etc. The generated code is checked into the directories gen-*.
The generated java API is checked into lib/hadoopthriftapi.jar.
There is a sample python script hdfs.py in the scripts directory. This python
script, when invoked, creates a HadoopThriftServer in the background, and then
communicates wth HDFS using the API. This script is for demonstration purposes
only.

由于它说的过于简单,而我又对java世界了解甚少,导致走了不少弯路,现记录如下:

1、下载thrift源码,安装(./bootstrap.sh ;./configure –prefix=/usr/local/thrift; make;sudo make install)

2、将一些必须的文件cp到thrift安装目录下:

cp /path/to/thrift-0.2.0/lib/php/ /usr/local/thrift/lib/ -r

mkdir /usr/local/thrift/lib/php/src/packages/

cp /path/to/hadoop-0.20.2/src/contrib/thriftfs/gen-php/ /usr/local/thrift/lib/php/src/packages/hadoopfs/ -r

3、安装thrift的php扩展(针对php而言)

cd /path/to/thrift-0.2.0/lib/php/src/ext/thrift_protocol;phpize; ./configure;make ;make install;

修改php.ini,添加extension=thrift_protocol.so

4、编译hadoop

cd /path/to/hadoop-0.20.2; ant compile (ant -projecthelp可以查看项目信息;compile 是编译core和contrib目录)

5、启动hadoop的thrift代理

cd /path/to/hadoop-0.20.2/src/contrib/thriftfs/scripts/; ./start_thrift_server.sh [your-port] (如果不输入port,则随机一个port)

6、执行php测试代码

<?php

error_reporting(E_ALL);

ini_set(‘display_errors’, ‘on’);

$GLOBALS[‘THRIFT_ROOT’] = ‘/usr/local/thrift/lib/php/src‘;

define(‘ETCC_THRIFT_ROOT’, $GLOBALS[‘THRIFT_ROOT’]);

require_once(ETCC_THRIFT_ROOT.’/Thrift.php’ ); require_once(ETCC_THRIFT_ROOT.’/transport/TSocket.php’ ); require_once(ETCC_THRIFT_  ROOT.’/transport/TBufferedTransport.php’ ); require_once(ETCC_THRIFT_ROOT.’/protocol/TBinaryProtocol.php’ );

$socket = new TSocket(your-host, your-port);

$socket->setSendTimeout(10000);

$socket->setRecvTimeout(20000);

$transport = new TBufferedTransport($socket);

$protocol = new TBinaryProtocol($transport);

require_once(ETCC_THRIFT_ROOT.’/packages/hadoopfs/ThriftHadoopFileSystem.php’);

$client = new ThriftHadoopFileSystemClient($protocol);

$transport->open();

try

{

$pathname = new Pathname(array(‘pathname’ => ‘your-hdfs-file-name‘));

$fp = $client->open($pathname);

var_dump($client->stat($pathname));

var_dump($client->read($fp, 0, 1024));

exit;

} catch(Exception $e)

{

print_r($e);

}

$transport->close();

?>

可能出现的问题:

1、可以创建目录或者文件,但是读取不到文件内容

这时可以打开hadoop thrift的log4j配置(如果你用的是log4j记录日志的话),在/path/to/hadoop/conf/log4j.properties 里,修改:

hadoop.root.logger=ALL,console

这时,没执行一条访问HDFS的操作,都会把debug信息打印出来。

我这里看到的是file的id很奇怪,怀疑是在32位机上溢出了,尝试修改未果。之后迁移到64位机,运行正常!

问题代码在:/usr/local/thrift/lib/php/src/protocol/TBinaryProtocol.php (由于我代码里用的TBinaryProtocol类)的readI64函数里。

2、启动start_thrift_server.s失败

Exception in thread “main” java.lang.NoClassDefFoundError: org/apache/hadoop/thriftfs/HadoopThriftServer

Caused by: java.lang.ClassNotFoundException: org.apache.hadoop.thriftfs.HadoopThriftServer

at java.net.URLClassLoader$1.run(URLClassLoader.java:217)

at java.security.AccessController.doPrivileged(Native Method)

at java.net.URLClassLoader.findClass(URLClassLoader.java:205)

at java.lang.ClassLoader.loadClass(ClassLoader.java:323)

at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)

at java.lang.ClassLoader.loadClass(ClassLoader.java:268)

at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:336)

Could not find the main class: org.apache.hadoop.thriftfs.HadoopThriftServer. Program will exit.

可以查看classpath,是否正确。我是添加了以下语句才正常的:

CLASSPATH=$CLASSPATH:$TOP/build/contrib/thriftfs/classes/:$TOP/build/classes/:$TOP/conf/

3、安装thrift出现问题

运行./bootstrap.sh出错: 查看boost是否安装,或者版本过旧

make出错:需要jdk 1.6以上

make出错:ImportError: No module named java_config_2 可能因为python升级,导致java-config不可用,重新安装java-config即可

line 832: X–tag=CXX: command not found : 把thrif/libtool文件内所有的“$echo”改为”$ECHO”即可(应该是libtool版本问题)

如果过于频繁的出现问题,且现象说明是大部分软件版本过旧,那可以考虑emerge –sync更新全部软件。

比较惨的时候,emerge总是被masked,那就手动安装一些依赖库吧,比如boost:

首先,下载boost包 http://www.boost.org/users/download/ ;并解压到/usr/local下(想安装的地方)

然后,建软连接到/usr/include下:ln -s /usr/local/boost-version/boost /usr/include/boost,这样就安装完了不需要编译的部分

如果还要继续安装需要编译的部分,那么进入到boost目录,运行bootstrap.sh脚本,生成bjam,运行./bjam install即可

4、ant安装报找不到jdk

但是我已经在/etc/profile,每个用户的.bash_profile里都把JAVA_HOME指向jdk目录了,而且echo $JAVA_HOME的结果也是jdk目录。

查看ant脚本,在第一行echo $JAVA_HOME,发现为空。无奈,只能手工把javahome添加到ant脚本里。

5、hadoop版本统一以后,可以正常启动datanode了,但是tasktracker还是起不来,在log里找到:

2010-04-30 18:07:31,975 ERROR org.apache.hadoop.mapred.TaskTracker: Shutting down. Incompatible buildVersion.

JobTracker’s: 0.20.3-dev from  by ms on Thu Apr 29 17:44:22 CST 2010

TaskTracker’s: 0.20.3-dev from  by root on Fri Apr 30 17:48:14 CST 2010

解决方法:

把master的hadoop目录,整个覆盖过去!

(尝试过用ms账号重新ant hadoop,无效;即版本一致了,但是安装时间不一致也不行。。。。)