PHP

PHP 函数调用的开销

2

处理大量数据,每个关键词有5000条数据,一共有50万个关键词。

要对每个关键词的每条数据进行加权处理。

写了一个加权函数,作为一个类的静态方法。

遍历这50万个关键词的数据,结果非常慢。

考虑问题原因,尝试把加权函数的逻辑拆出来,放到大循环中。

写了测试代码,结果性能提升非常明显。

调用类的静态方法,程序性能是 156 次/秒,而拆出逻辑,直接运行,性能是 625 次/秒!

速度是原来的 4  倍多!

addslashes() Versus mysql_real_escape_string()

0

关于 addslashes() 和 mysql_real_escape_string() 两个函数,已经有过很多争论了。

在 PHP 过滤 SQL 注入时,通常都会使用  addslashes() 函数,但这并不保险,尤其是数据库编码是 GBK 时,类似于 %5c(\) 和 %27(‘) 等字符时,不能得到预期的正确过滤。

我在使用  mysql_real_escape_string() ,数据库是 UTF-8 编码时,也是不能正常过滤,以 %5c 结尾的汉字,在做完转义后,效果仍然和 addslashes() 的结果一样。但同样的代码,拿到使用 GBK 编码编译的 MySQL 环境下就没问题。

我还没有搞明白到底问题出在哪里,最近会一直 focus 在这上面。

今天发现一个老外写的 Blog,分析得比较深入,尽管已经是 2 年前发表的了。

http://shiflett.org/blog/2006/jan/addslashes-versus-mysql-real-escape-string

分析得比较透彻,有很多人写了回复,仔细看看每个人的反馈,也能受益匪浅。

PHP 性能优化 二进制转换 pack()

0

今天搞一个数据存储程序,需要把数据转换成二进制存储。

在转换过程中,发现效率很低,不能满足需要。

经过反复测试和修改,总结了一些经验。

写在我的 Wiki 里了,Wiki 里贴代码比较方便,也顺便在这里推广一下我的Wiki

<<Leakon’s Wiki>>

性能优化的Wiki地址:http://wiki.leakon.com/PerformancePack

编码检查 UTF-8 浏览器编码

0

编码问题一直是做网站开发的工程师们很头疼的事,如果你希望自己的网站能够被更多不同语言环境的人浏览和使用,那就一定要解决好编码的问题。

我的经验是,从HTML页面的编码,到PHP程序文件的编码,到数据库的设计以及与PHP之间连接的编码,全部使用UTF-8,这样就能保证你的页面不会出现乱码。

不要总是觉得,你的网站是给中国人用的,不给其他国家的人使用。如果你的中国用户希望保存一些日文、韩文等亚洲文字, 你也没有理由拒绝吧?

还是在一开始就解决好编码的问题为好,做网站,免不了这一步,今天不做,以后迟早要还的。

如果你认为我说的毫无道理,那我给大家引用一篇W3C组织的官方说明吧:

http://www.w3.org/International/questions/qa-forms-utf-8.en.php

注意回答部分的第一句:The best way to deal with encoding issues in (X)HTML forms is to serve all your pages in UTF-8.

用UTF-8吧,肯定没错的,你可以避免很多很多问题。

不过,就算页面使用了UTF-8,也还是会遇到一个问题:

浏览器接收编码的转换问题。

就这个问题的说明,我做了一个测试页面,可以在我的Google SVN下载:

http://leakon.googlecode.com/svn/trunk/leakon/php/detect_utf8/

在浏览器里打开test.php页面即可。

这个页面是UTF-8编码的,如果你不是用SVN软件CheckOut而是直接复制的源码,请记得要把test.php这个文件保存成UTF-8格式。

这个页面有一个Form表单,你可以在输入框内输入中文,然后看地址栏里word字段的值,一个汉字对应3个%,因为UTF-8是变长编码的,针对不同的文字,%的数量是1-4个。

如果你熟悉GBK编码的页面,应该注意到每个汉字对应2个%,因为汉字都是双字节编码的。

这个时候,如果你足够细心,应该可以发现一个冲突的问题。

如果,我在浏览器的地址栏的word字段后面直接输入汉字,得到的是什么结果呢?给大家一个提示,这里最好使用Firefox浏览器,如果你在地址栏输入中文,Firefox会按照GBK的编码方式,按双字节编码,也就是一个汉字对应2个%。

可是你的页面默认是接收UTF-8编码的字符的,给你一个GBK编码,你会解析成乱码。

很多网站存在这个问题,包括Google和Yahoo。 他们只对各自的中文网站就解决了这个问题,英文的和日文的都没有处理。下面我们逐一测试一下:

我们给一个测试用例,中文:百度;GBK:%B0%D9%B6%C8;UTF-8:%E7%99%BE%E5%BA%A6。

看到了吧,GBK是4个%,UTF-8是6个%。

  1. http://www.google.cn/search?hl=zh-CN&q=%E7%99%BE%E5%BA%A6&btnG=Google+%E6%90%9C%E7%B4%A2&meta=
  2. http://www.google.cn/search?hl=zh-CN&q=%B0%D9%B6%C8&btnG=Google+%E6%90%9C%E7%B4%A2&meta=
  3. http://yahoo.cn/s?p=%E7%99%BE%E5%BA%A6&v=web
  4. http://yahoo.cn/s?p=%B0%D9%B6%C8&v=web
  5. http://www.google.com/search?hl=en&q=%E7%99%BE%E5%BA%A6&btnG=Google+Search
  6. http://www.google.com/search?hl=en&q=%B0%D9%B6%C8&btnG=Google+Search
  7. http://search.yahoo.com/search?p=%E7%99%BE%E5%BA%A6&fr=yfp-t-501&toggle=1&cop=mss&ei=UTF-8&vc=&fp_ip=CN
  8. http://search.yahoo.com/search?p=%B0%D9%B6%C8&fr=yfp-t-501&toggle=1&cop=mss&ei=UTF-8&vc=&fp_ip=CN
  9. http://www.google.co.jp/search?hl=ja&newwindow=1&q=%E7%99%BE%E5%BA%A6&btnG=%E6%A4%9C%E7%B4%A2&lr=
  10. http://www.google.co.jp/search?hl=ja&newwindow=1&q=%B0%D9%B6%C8&btnG=%E6%A4%9C%E7%B4%A2&lr=
  11. http://search.yahoo.co.jp/search?p=%E7%99%BE%E5%BA%A6&ei=UTF-8&fr=sfp_as&x=wrt
  12. http://search.yahoo.co.jp/search?ei=UTF-8&fr=sfp_as&p=%B0%D9%B6%C8&meta=vc%3D

以上是对Google和Yahoo两大搜索引擎的中文、英文、日文网站进行UTF-8、GBK编码的访问。

可以看到,2家公司只对中文网站做了编码检查,发现不是UTF-8就对关键词进行编码转换,得到了正确的结果,英文和日文都没有处理,给GBK编码时得到的是乱码。

他们就忽略了英文和日文用户搜索中文的需求。

其实,这个问题不是不可以解决的,中文网站都做到了,其他语言的怎么就不行呢?

这个不讨论了,还是说说怎么检查UTF-8编码吧。

其实就是一段正则表达式:

$regex = ‘/^(‘
. ‘[\x09\x0A\x0D\x20-\x7E]|’ # ASCII
. ‘[\xC2-\xDF][\x80-\xBF]|’ # non-overlong 2-byte
. ‘\xE0[\xA0-\xBF][\x80-\xBF]|’ # excluding overlongs
. ‘[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|’ # straight 3-byte
. ‘\xED[\x80-\x9F][\x80-\xBF]|’ # excluding surrogates
. ‘\xF0[\x90-\xBF][\x80-\xBF]{2}|’ # planes 1-3
. ‘[\xF1-\xF3][\x80-\xBF]{3}|’ # planes 4-15
. ‘\xF4[\x80-\x8F][\x80-\xBF]{2}’ # plane 16
. ‘)*\z/x’;

大家从SVN里可以下载源码。

我封装了一个类,DetectUT8,用于检测和转换编码。

过程是这样的:如果检查到不是UTF-8编码,就进行GBK -> UTF-8的转换。因为在地址栏输入的非ASCII字符都会按照GBK编码。

转换函数会检查系统是否启用了 mbstring 函数库,如果没有,则改用 iconv 转换。

这里要注意一点,不要把UTF-8写成utf8,也不要写成其他格式,iconv 的检查比较严格。

如果内置了 mbstring 函数,还是用 mb_detect_encoding 比较保险,但经测试表明,用上面这段正则,编码检测的成功率和 mb_detect_encoding 没有差别。

还要重申一个问题,编码检测的可靠性不是 100%,但应用这种方法已经极大地改善了用户体验,建议大家都采用这种方式。

PHP 文件锁 flock 负载均衡

1

最近有个项目,采用单台前端服务器提供Web服务,程序需要实时访问后端服务器。后端一共有几十台服务器,但有压力限制,单台负载不能过高,必须做负载均衡。

最简单的方式是用随机数,前端来请求的时候,随机挑选一台后端服务器,但这并不能保证压力平均分布,很有可能在某一段时间内请求都落到同一台服务器上,很容易导致这台服务器停止服务。

后来想到用文件锁的方式,来标记访问计数,顺序访问后端的每一台服务器,让每一台服务器一个周期只被访问一次。

在进行了多次功能测试和压力测试后,验证了这种想法的可行性,然后写了一个IDService类,封装了整个过程。

我在Google提供的SVN服务器上保存了源码,大家可以在

http://leakon.googlecode.com/svn/trunk/leakon/php/flock/flock.php

这个地址看到源码,或者用SVN工具CheckOut到本地。

核心过程,就是初始化的时候给一个ID范围,默认是从0开始,如果你的server_count是32,那么调用getId()方法的时候,我会顺序给你31至0这32个ID,采用文件锁就是考虑到并发请求之间彼此独立,一个进程读数据文件的时候要加独占锁,解锁前,其他进程无法读取数据文件。

ID分配给你了,每个ID对应哪个服务器,就是你自己做映射的事了,保证了这个模块的无关性和独立性,和其他所有模块保持无耦合。

这是在PHP5的环境下写的,const 定义了3个类常量:

LINE_FEED 是换行符,Windows 下是 \r\n,Linux 下是 \n,只是为了方便测试的时候实时查看数据,可以是任意字符,只要不是数字就OK;

MAX_LOAD 是计数器的最大值,计数器都是从0开始,如果有任何一个ID达到了最大值,则所有ID计数器全部归零,开始新的一轮计数,其实这个设置只要大于0就可以,最好不要太大,因为存储数字也是要占用存储空间的,越小,id_data_file的尺寸就越小,硬盘读取就越快;

DATA_BLOCK 是设置一次读文件的数据大小,硬盘的一个文件至少要占一个簇,一般文件系统一个簇是4K,这个取值要跟ID的总量有关系,如果你的LINE_FEED是\n,MAX_LOAD是99(采用文本方式存储,2字节),那么一个ID占用3个字节,如果你有100个ID,那么数据文件占用空间就是300Byue,因此DATA_BLOCK设置为300是最佳值,需要注意,如果ID范围变大,需要同步更改此值,因此我默认设置了2048字节,小于硬盘的一个簇,相对于300字节来说没有性能损失,因为都在一个簇内,数据存储是连续的,硬盘只需一次寻道和一次读写。

源码里有使用说明,很简单,在实例化对象的时候指定ID范围和数据文件位置即可。

已经经过测试,给一些压力测试数据吧:

在AMD3000+和7200转80G硬盘的台式机环境,可以提供到 1300+次/秒 的速度,此时磁盘IO是瓶颈;
换上Linux服务器,具体配置不太清楚,反正是SCSI硬盘,100多G,只是开发用机,性能并不高,但可以提供 5000+次/秒 的速度。

综合2中环境的测试数据,以目前前端服务器的最高负载(最高也就 200+次/秒),以及项目的实际负载,此代码性能足够满足需要。

PHP require 绝对路径 autoload

4

在写PHP程序时,如果文件比较多,目录也比较多,有很多require_once的情况。

如果程序的入口不唯一,并且分布在不同的目录,必须require绝对路径,如果是相对路径,PHP会把入口文件的路径作为基本路径,require里的相对路径都是相对于入口文件的。

这样带来最大的问题就是自己部署的程序,给别人提供接口时,别人根本无法使用!

所以要用绝对路径。

做法就是选一个require的入口文件,大家都要包含这个文件,这个文件的开头加上这么一句:

这个时候PROJECT_BASE_PATH就是从根开始的绝对路径了。

后续的所有require文件,都用这个宏定义去拼接,例如:

就绝对保证不会再出错!

当然,如果你是PHP5的环境,推荐使用__autoload函数,把所有功能都封装到类中,然后在需要的时候自动调用,避免到处require。

PHP 性能 安全 缺点

0

推荐资源一:a howto on optimizing php
http://phplens.com/lens/php-book/optimizing-debugging-php.php
总揽全局方能运筹帷幄决胜千里之外。这是一篇非常全面的php性能优化指南,高屋建瓴,教你全面均衡的优化你的应用。系统的介绍了LAMP架构下系统优化的各个层次。虽然两年半没有更新了,仍不失为经典的php优化扛鼎之作。

推荐资源二:php benckmark tests
http://www.php.lt/benchmark/phpbench.php
细节决定成败。这个简洁却不失细致的基准测试结果在“代码行”级别上教你如何编写高性能的php程序。尤其值得注意的是,和“同样的任务,面向过程的实现方式比面向对象快数倍”这个论调一样,php社区长期流传单引号速度远远快于双引号的言论,如今,时过境迁,这些经验是否还有效呢?我的建议是,相信你自己的判断,而不是道听途说。作决定之前,对你不了解的技术和架构做个垂直切片,而不是等到业务逻辑全部实现了才发现严重的性能问题。avoid surprises.

推荐资源三:PHP有什么缺点
http://www.nirvanastudio.org/php/php-in-contrast-to-perl.html
知己知彼,百战不殆。衡量一个人是否足够熟悉php的标准之一就是看他了解多少php的缺点,这篇文章罗列了PHP的很多不足之处,其中有很多地方都说的很中肯。了解了php的缺点,相信你能更好的驾驭它,用其可用之处。不过,也别走极端,任何技术都不是完美的,严谨的必然罗嗦(比如ADA),灵活的必然晦涩(比如Haskell),强大的必然难以驾驭(比如汇编),与其把时髦的技术挂在嘴边,不如把过时的技术放在心里。用好自己最熟悉的就是成功。

推荐资源四:《Essential PHP Security》http://project.5acity.com.cn/documents/essential_php_security.chm
一本PHP安全的电子书。软件工程有个很重要的原则就是防御式编程,遵守这个原则能让你生产安全健壮的产品。哦,顺便说个放之四海而皆准的道理:不要相信任何来自外部的数据。2004年的时候,我在linux下面用lumaqq把我的qq昵称修改为空了(不是空格,而是空白,什么都没有)。原因就是qq只在客户端验证了昵称是否为空,服务端却没有验证。

PHP Perl 关联数组 哈希表 Hash Table

2

关联数组,又称为哈希表(hash table),是一种非常好用的数据结构。

在程序中,我们可能会遇到需要消重的问题,举一个最简单的模型:

有一份用户名列表,存储了 10000 个用户名,没有重复项;
还有一份黑名单列表,存储了 2000 个用户名,格式与用户名列表相同;
现在需要从用户名列表中删除处在黑名单里的用户名,要求用尽量快的时间处理。

这个问题是一个小规模的处理量,如果实际一点,2 个表都可能很大,比如有 2 亿条记录。

我最开始想到的方法,就是做一个嵌套的循环,设用户名表有 M 条记录,黑名单列表有 N 条记录,那么,循环的次数是 M * N 次!
PHP 版代码:

foreach($arrayM as $keyM => $nameM) {
foreach($arrayN as $nameN) {
if ($nameM == $nameN) {
// 本行执行了 M * N 次!
unset($arrayM[$keyM]);
}
}
}
return $arrayM;
?>

另一种方式,利用数组索引。

PHP 是一种弱类型的语言,不像 C 语言那样有严格的变量类型限制。C 语言的数组,每一个元素的类型必须一致,而且索引都是从 0 开始。
PHP 的数组,可以用字符串作为索引,也称为关联数组。
数组索引,有一个天然的限制就是不会重复,而且访问的时候不需要查找,可以直接定位。

还是刚才的那个问题,我们采用另一种办法。

把黑名单列表的用户名组织到一个数组里,数组的索引就是用户名。

然后,遍历用户列表的时候,只需直接用 isset 查询那个用户名是否存在即可。

PHP 版代码:

$arrayHash = array();
foreach($arrayN as $nameN) {
// 本行执行了 N 次。
$arrayHash[$nameN] = 1;
}

foreach($arrayM as $keyM => $nameM) {
if (isset($arrayHash[$nameM])) {
// 本行执行了 M 次!
unset($arrayM[$keyM]);
}
}
return $arrayM;
?>

可以看到,优化过的代码,循环次数是 M + N 次。

假如 M 和 N 都是 10000,优化前,循环了 1 亿次;优化后,只循环了 20000 次,差了 5000 倍!
如果第二个程序耗时 1 秒,则第一个程序需要将近一个半小时!

最近在做 Perl 的开发,Perl 在处理文本的时候有很高的效率,同样,它也支持关联数组!

只是语法和 PHP 的那种类 C 的方式有很大不同,以第二段代码为例,Perl 版的实现:


#!/usr/bin/perl
my %arrayHash;
for(my $i = 0; $i < @arrayN; ++$i) {
$arrayHash{$arrayN[$i]} = 1;
}

for(my $i = 0; $i < @arrayM; ++$i) {
if ($arrayHash{$arrayM[$i]}) {
$arrayM[$i] = undef;
}
}

Perl 的数组是 @ 开头,哈希是以 % 开头,unset 实际上就是 undef。
Perl 的哈希和数组都是有具体类型的,而且向函数传递变量的时候要传引用,我刚学时间不长,快被搞晕了。

不过,现在刚刚实现了一个以 hash 方式进行 IP 位置查找的算法,平均比较次数大概在 3 次左右,比传统的折半查找方式少了很多次,它大概需要 8 次以上的比较。

刚刚做了一个小的性能测试,对 10 万个 IP 进行查找,在我的台式机上,耗时 15 秒,平均每秒 7500 次,感觉还不错,呵呵。

不过,还是喜欢 PHP 的数组,真的很强大!

God Bless PHP!

Go to Top