查看文章 |
Java还是PHP扩展【优化代码?还是改变结构】
2009-10-25 12:19
一直以来我都有些话想说,今天,我就让天下所有PHP玩家鄙视吧。 曾经的时候,我也像各位一样,抱着一腔热血,坚信只要扩展就能救PHP。但是,大约一年前,我已经不这么认为了。当我提出一个性能问题的时候,总有人会提出用扩展解决,但实际上,我相信,给出这种建议的人估计是没有真正的使用过那个扩展或者是压根没有维护服务器的经验,不知道问题的根本在哪里,完全是意淫。 如果我一直这么说下去,估计大家也都只会认为我是在污蔑,没有真凭实据。下面我讲讲我的一些经历吧。 我们的项目都是基于PHP、Oracle的。当然,PHP连接Oracle得用到oci扩展,使用这个扩展PHP连接Oracle的性能不知道各位测试过没有。在采取了Oracle官方给的所有优化建议之后(适当加大 oci_set_prefetch 的值,采用持久连接、基于测试修改游标共享策略),性能仍然离期望的有相当大的距离。更糟糕的是,持久连接在生产环境下并不是一个有效的优化策略,理由如下:Oracle服务器可以同时接受的连接数是非常有限的,是非常珍贵的资源,生产环境下的Oracle服务器是由DBA管理的,基本上,他们不会让它处于MTS模式。前端PHP进程数是远大于可以建立的Oracle连接数的,可以这么认为,绝大多数PHP进程是不可能获得持久连接的。由于PHP的持久连接是基于进程的资源、A进程获得的连接B进程是无法享受的。如果把持久连接的资源也视为一种资源Cache的话,那么实际上生产环境下的命中是非常低的。况且,随着前端PHP服务器的伸展,命中率会越降越低。 以下给出几个测试结果(很久很久之前测试的,具体的数值已经淡忘了,只记得结论呢了),若有异议,还请各位自行测试下(测试平台如下: Windows XP Sp3、 内存 2.5g(DDR533)、CPU Pentium D 双核2.8G,前端PHP,后端Oracle均安装在一台机器上,数据量100w),测试结果如下(百度表格无法正常显示,以下不以表格形式给出): PHP(5.26) + OCI8 + Oracle 10g + 持久连接 + 绑定变量: 性能最高 PHP(5.26) + OCI8 + Oracle 10g + 非持久连接 + 绑定变量: 上面测试的1/2性能 PHP(5.26) + OCI8 + Oracle 10g + 持久连接 + 非绑定变量: 第一项测试性能的1/3 PHP(5.26) + PHP Java Bridge(Sourceforge版) + DBCP + Oracle + 绑定变量: 约等于第一项测试,略低 PHP(5.26) + PHP Java Bridge(Sourceforge版) + DBCP + Oracle + 非绑定变量: 约等于第一项测试的3/5 附加非量化的结果: PHP直接通过OCI8扩展连接Oracle,会导致Oracle消耗更多的CPU资源。并且,在Oracle表索引不优化的情况下,PHP通过OCI8连接的性能下降程度远高于PHP通过Bridge使用DBCP访问的下降程度。 对上面的测试结果,我推测原因如下:OCI8扩展无论是否开启了持久连接,估计初始化、清理过程比较多,导致Oracle服务器消耗更多的CPU(两个版本语句是一样的)。而 Sourceforge版的Java桥,因为它采用http协议传输XML格式的数据(可以理解为简化版本的XML-RPC),也额外的需要消耗一些资源。上面的测试结果只具有一定的参考意义,实际上,生产环境中,需要考虑的东西远比这要多,很显然,上面的测试没有测试网络因素。那么说说生产情况下的结果吧。 在生产环境下,我将访问次数前10位页面上的数据库请求委托给运行于Tomcat上的使用C3P0访问数据库的XML-RPC(粗粒度的调用),结果是,在一般网络条件下,页面执行时间减少约一半,在网络越不稳定的情况下,性能提升越多,并且极度减少了在网络条件不稳定的情况下FastCGI返回50* Bad Gateway的情况。(只要 DB 和前端不在同一个机房,这个性能提升是相当明显的)。 下面,我说说经历过的第二个例子。 有时候,你得接手这种项目、把一些激活码分配给玩家。这是一个简单的项目,逻辑非常简单,不是么。但是,如果有成千上万的玩家同时进来的话,这就不是一个简单的活了。 你也许会这样做。你的激活码是存放在一张MySQL表中的,表结构也许类似这样:有一个自增的ID、有一列存放这些激活码、还有一列存放玩家的Passport(默认是null、表示还没被使用过),还有一些无关紧要的字段。你可以这样分配一个识别码,先 update `tablename` set `passport` = '玩家的Passport' where `passport` is null limit 1 然后在通过select查找刚才更改的那个激活码进行后续操作,或者说通过 select * from `tablename` where `passport` is null limit 1 for update 锁住在进行操作。但你如果维护数据库的话,你就会发现MySQL进程里密密麻麻都被上面条语句给阻塞了,系统的吞吐量无限接近与0。 如何解决上述这种问题呢?Use Java,is Simple!看到上面的自增字段么?如果我们有一个全局的计数器(绝对准确的),当前的计数永远指向下一个待发的激活码ID,那么这条该死的分配语句不就绕开了(变成了使用主键查找一条记录)!那么打造一个可靠的计数器大约需要多久了?1个小时,花15分钟编写这个计数器,然后花45分钟在各种情况下验证它的可靠性。简易如下的一个类就是可靠的: ![]() 使用Tomcat写个简单的Servlet发布这个类,在一台退役的联想双核服务器上(Oracle JRocket 1.6),上述计数器方案在前端PHP请求中先HTTP请求一次 hasMoreId ,然后请求一次 getCurrentId ,每秒大约可以处理14000次请求。结论证明,这个简易的计数器无论是性能,还是可靠性都是满足我们的需求的。(MySQL处理事务事,一般的测试机器sysbench中每秒约可以完成4000次事务,实际上,我们的业务中会比sysbench测试的复杂的多,也就是说,这个计数器的性能是远高于单台MySQL处理的极限的)。 当然,您可以认为,这种计数器,PHP也很容易做到的。文件存储计数器的方案估计您是不会提的,毕竟可靠性、效率都是相当的低。您估计会觉得,使用 Memcache 来完成啊!我同意,Memcache是可以完成一个计数器的,因为 incre 指令是原子的(也就是说:Memcache::increment 应该是原子操作,不会出现计数不准确的情况)。那么,咱们来讨论讨论这个计数器的数据初始化问题吧,请问这里您怎么保证数吧个PHP处理进程之间互斥不导致n个PHP进程同时进行数据初始化(从数据库里求数据,然后把这个数据存到Memcache中)?Memcache作为存储设备是不可靠的,即使是在内存足够的情况下(时间越长越有可能发现这种情况),数据的初始化可能发生在任何时候(一旦原值丢失,都会导致重新初始化数据),这不是人手工通过防火墙限制访问能够解决的。BTW:上面的内容也许对一个PHP初级玩家来说,太高深了,毕竟很多人看待PHP执行的时候还是从PHP源代码的第一行看到最后一行的,而不会考虑其它进程对它的影响。那么,说点简单的吧,您写个PHP 的 Hello World 然后使用 ab 测试一下压力,每秒能处理多少次请求? 3000次每秒?OMG、即使是您使用Memcache写的计数器就像您写的Hello World一样高效,估计您的上司听到这个数据也不会满意的,您的这个计数器早于数据库本身让系统的扩展性达到瓶颈啊。 很长时间以来,我一直在考虑一个问题。当遇到PHP性能瓶颈问题的时候,我们第一反应就是使用扩展代替它的考虑是不是对的。例如,使用扩展版本的纯真IP解析扩展代替CoolCode的PHP版本,但当你真用扩展替代了原来的PHP版本,才发现系统的性能没有提升(当然不可能有性能提升。我维护的不是ip138)。依据日志去优化系统,而不是YY。性能问题,很多都是结构上的,而非代码级别的,扩展不是出路,跳出PHP思维的枷锁才是王道。 海龙和我讨论过如何写好一个Web Game。对市场方面,我不了解,但对技术方面,我还是坚定的坚持我当时的理解。使用Java制作PHP与MySQL的中间件,PHP只通过Socket与Java中间件打交道、Java维持前端数据与数据库中数据的同步,这样的Web Game才能保证Web Game本身的高性能。(他主业是PHP,但对Java也有相当的了解,我看来一个人实现这个中间件,使用Java的话并不是不可实现的难题) 对扩展的崇拜,可能是源自对C语言性能的盲目崇拜。但请注意,您用C语言编写自己的扩展的时候,您也不是连最基本函数也从头码起,您得使用很多库(至少,您得符合Zend Engine的框架吧),库是否高效,您 想过没有,这与Java是否高效,已经是一码事了,把高性能的一些先决条件交给了别人。要不,为什么基于Java 的Quercus引擎为什么会比基于C的原生PHP引擎高效哪么多呢?(您可以使用Tomcat + Quercus 试试,实际上这种方案,PHP是不会编译为Java的Class的,但效率乐观的说,会是原生PHP引擎的8倍以上)。 |
最近读者:
