这篇文章计划很久了一直感觉无从下手, 一直想全面、深入的写一篇关于php优化,但思绪很乱,经过很多天的构思和整理,终于有点头绪了。
几十年来,php以超高的开发效率、低成本的投入、内置丰富的函数库、灵活便捷、简单易学、短平快的开发周期、低廉的试错成本、实用...等特性,一直深受人们的喜爱,也是php能走到今天作为大众主流语言,能与java平分天下的原因。可是在高并发下php的性能问题就暴露无遗,这块一直是php短板,为人们所诟病。尤其是在近些年来各种新语言层出不穷,百家争鸣,都想从web这块蛋糕挖一勺子奶油尝尝甜淡。与其他语言比,php能拿得出手的似乎只剩下开发效率了,很多人也跟风起哄唱衰php,似乎现在的php一直在吃老本。
php作为一门动态脚本语言,架构周边组件和高并发一直是它天生的短板和弱点,正如脚本语言与生俱来超高开发效率一样。当我们来设计架构时我们通常绕开语言本身,直接从架构层面做流量负载和分发,这种情况下php更像一门工具语言,就如同它的名字一样“超文本预处理器”,始终无法和周围环境深度结合。
动态语言的性能问题分析
php提供了非常多的功能供我们拿来就用,这是静态编译语言所不具备的,但随着互联网和时代的发展,php的劣势也逐渐显现出来,甚至愈来愈明显。针对PHP暴露出的一系列的问题,我们下面就来分析和尝试解决这些问题。
在谈php优化前,我想先来讨论一下php为什么慢,究竟慢在哪里?我们先分析问题。
作为一个动态脚本语言来说,其实php本身并不慢,而且功能强大(注:功能这个词是脚本语言专属,静态的不能叫功能),在众多动态语言中php的性能已经非常出色了,只是动态语言在特定的应用场景下会显得力不从心,比如高并发。高并发就是密集计算的业务场景,这个特定应用场景就是密集型计算,动态语言在此场景下毫无优势可言,反而弱点毫无保留的全部暴露出来了。
高并发会产生两个问题,密集计算和密集io。密集io可以通过各种缓存来解决,尤其是内存缓存,而密集计算这个问题无法借助外部的东西来解决,因为问题就出在语言本身。只能寄希望于拆分项目来解决,而且拆分也不能拆的太细,越细管理起来越困难。通过拆分项目优化其实有点牵强,它也没有从根本上解决这个问题,只是一定程度上缓解减轻了这个问题,还带来了新的难题:拆分让简单的东西变得更复杂,更难管理。软件开发有一个原则:尽量保持简单,要变复杂就得有足够的理由,没有足够的理由就不应该变复杂,尤其是不应该为了复杂而复杂。的确,越简单的东西出问题的几率越小,越复杂的东西出问题几率越大,这句话放之四海之内皆准。
拆分还面临一个问题,如果已经做了根据业务横向切割模块拆分,可核心模块仍然并发量很大,你的解决方案是什么?能再拆?盲目的拆分只会把项目和架构越搞越乱。毕竟拆分只是优化一种手段,一个方法,而不是唯一方法,没必要过度依赖拆分。
为什么高并发对php毁灭性这么大?
这就要php web运行模式说起,我们先假设在应用服务器不挂的情况下,php的状况是怎样,php web模式下是每一个请求都产生一个对于进程来处理,也就是http server通过请求来唤起php运行,按次运行,请求处理完结束后php退出进程释放一切资源(进程、内存、io),php是被动运行的。通常这个过程从唤起运行请求到结束退出进程只有几百毫秒甚至几十毫秒的时间(因此也有一个好处是也不用关心什么内存溢出,只管写业务逻辑就行了,这也是php为什么开发快的一个原因,当然这是题外话),但如果瞬间并发十万至几十万乃至上百万请求呢?我们假设http server抗得住,那php呢,它会根据的请求数量产生对应的进程数(尽管有的web server有进程复用和动态调度,但只能改善这种情况,无法从根本上改变),此时首先CPU会爆满(跑满100%),随后内存爆满,轻则短暂宕机,重则服务器瘫痪丢失数据、数据损坏。我知道在网站打不开的那一刻,你第一反应想连上服务器看看什么情况,实际上,在CPU满载的那一刻你已经失去了对服务器的控制,你会发现服务器已经连不上了,你束手无策。
cpu密集型和io密集型
CPU密集型也叫计算密集型,指的是系统的磁盘、内存性能相对CPU要好很多,I/O读写可以在很短的时间就可以完成,IO读写却很快,没什么事情要做,而CPU还有许多运算要处理,CPU Loading非常高,CPU出现满载、负载情况,任务积压,需要处理很久,IO在等CPU。
并发、解析、多进程、多进程、并行处理、调度都是由CPU处理的,若CPU同时处理的任务过多就是CPU密集型,处理不过来就会负载,超频。
当然大部分情况都是CPU在等I/O读写完成返回。
IO是input、output两个单词的缩写,输入输出的意思。磁盘、内存的读写都属于都io范畴,频繁的IO读写就是IO密集。CPU的载荷情况很低,而硬盘、内存的负载很高就是IO密集型。 这也是web应用的特点,大多数情况都是在读写I/O,而CPU总是在等IO返回,另一方面也是因为CPU计算本身就比IO读写要快很多。
很多时候I/O读写在达到性能极限时,CPU占用率仍然较低。这是因为web应用场景下本就需要的大量的I/O操作,而计算任务很少,只有用户并发时才会出现需要大量的计算型任务。
在大多数情况下,拖慢响应速度都是IO,只有在极少数是CPU,一旦是CPU影响响应速度,这问题就不是小问题,轻则宕机崩溃,重则系统卡死,数据丢失损坏。
从语言层面切入,从语言本身来优化,直面问题
一、改变运行模式
优点:
缺点:
我之前就用php写过一个简单websServer当静态服务器用,如下:
"text/html",
".htm"=>"text/html",
".xhtml"=>"text/html",
".xml"=>"text/html",
".php"=>"text/html",
".java"=>"text/html",
".jsp"=>"text/html",
".css"=>"text/css",
".ico"=>"image/x-icon",
".jpg"=>"application/x-jpg",
".jpeg"=>"image/jpeg",
".png"=>"application/x-png",
".gif"=>"image/gif",
".pdf"=>"application/pdf",
);
public function __construct($ip="192.168.48.152",$port=65500){
set_time_limit(0);
$this->ip=$ip;
$this->port=$port;
$this->webroot=__DIR__.'/www';
echo "\nServer init sucess\n";
}
public function listen(){
$socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
if(!$socket)
echo "CREATE ERROR:".socket_strerror(socket_last_error()).'\n';
$bool=socket_bind($socket,$this->ip,$this->port);
if(!$bool)
echo "BIND ERROR:".socket_strerror(socket_last_error()).'\n';
while(true){
$bool=socket_listen($socket);
if(!$bool)
echo "LISTEN ERROR:".socket_strerror(socket_last_error()).'\n';
$new_socket=socket_accept($socket);
if(!$new_socket)
echo "ACCPET ERROR:".socket_strerror(socket_last_error()).'\n';
$string=socket_read($new_socket,20480);
$data=$this->request($string);
$num=socket_write($new_socket,$data);
if($num==0)
echo "WRITE ERROR:".socket_strerror(socket_last_error())."\n";
else
echo "request already succeed\n";
socket_close($new_socket);
}
}
/**
* [读取get或post请求中的url,返回相应的文件]
* @param [string]
* @return [string]
* http头
* method url protocols
*/
public function request($string){
echo $string;
$pattern="/\s+/";
$request=preg_split($pattern,$string);
if(count($request)<3)
return "request error\n";
$filename=$this->webroot.$request[1];
echo "filename:".$filename."\n";
$type=$this->setContentType($filename);
if(file_exists($filename)){
$data=file_get_contents($filename);
return $this->addHeader($request[2],200,"OK",$data,$type);
}
else{
$data="this resource is not exists";
return $this->addHeader($request[2],1000,"not exists",$data,$type);
}
}
private function addHeader($protocol,$state,$desc,$str,$type){
return "{$protocol} {$state} {$desc}\r\nContent-type:{$type}\r\n"."Content-Length:".
strlen($str)."\r\nServer:MyServer\r\n\r\n".$str;
}
private function setContentType($filename){
$type=substr($filename,strpos($filename,'.'));
if(isset($this->contentType[$type]))
return $this->contentType[$type];
else
return "text/html";
}
}
$server=new MyServer();
$server->listen(); //调用listen方法,使脚本处于监听状态,然后浏览器访问192.168.48.152:65500
这里再说一句,其实php还可以写代理服务器给你做流量转发(基于OSI网络模型第四层传输层,nginx的代理是在最上层应用层),我之前折腾过,就是忘了那个文件放在那个电脑。
插一个swoole上载过的坑
很多年前,我在一个中大型网络公司做组长,因为管理理念是尽量轻松自由、扁平化。所以我对组内成员的管理是让他们自由发挥个人的最大能力,天高任鸟飞,海阔任鱼跃,所以我们组里每个人之间是相对独立的,一个人尽可能负责多个项目。组里面有个同事在做直播app项目的时候,直播聊天服务器用的就是swoole,结果写完测试的时候没问题,给客户投入使用了 ,一波用户(三四千用户)上来master进程内存飙升,就几千并发,然后用户走了3/2,master内存一直不释放,内存使用率也不下来,没到晚上就崩溃了,因为我是组长烂摊子还得我来搞,还扣了我的季度奖金,我找谁说理去?谁能赔我季度奖金,好几万呢!
他用swoole写直播聊天服务器这个事情我是不知情的,我要是知道肯定不会让他这么做到。一是因为我是技术上的保守派,我倾向于用成熟稳定可控的技术,因为风险的原因我不太愿意去尝新,尝新是有代价的,你自己私下愿意怎么玩怎么玩,不要在商业项目上玩。二是我看事情,事物总是先到它的负面,我喜欢预知风险,风险过大的事情我不能控制的我不会做,因为我要为组的盈收负责。
我刚开始介入这个项目的时候,也想看看是什么问题,在这方面改尽量改,毕竟他也花了十几天在这东西上面。但是因为webServer、socket长连接都是swoole用纯c实现的,根本无从下手,就不了了之。最后实在没办法我把swoole换成了nodejs做聊天服务器,才解决了这个问题
吃一堑长一智,反正直播聊天最好不用swoole,对实时性要求太高,容易出问题。关键是出了问题也没人能解决,这才是最无奈、最让让抓狂的,就像一个黑匣子,叫天天不应,叫地地不灵。
一、改变运行时环境
还可以做编译器优化
改变运行时环境的优化方案有很多种类
改变运行时环境优化方案 | 编译器名称 | 描述/说明 | 是否有编译器优化 | 是否在更新维护 |
代码缓存 | apcu、opache、Zend O+ | 只是代码缓存工具非编译器,无真正意义的编译功能 | 无 | 是 |
把php代码编译为扩展 | zephir、phc | 以PHP语法来编写代码,然后编译成php扩展来运行 | 有 | zephir在维护 phc已停止维护 |
独立运行时环境 | Roadsend-php(PCC/rphp)、phc | 基于LLVM的编译器,经过编译后导出php代码编译出来的独立的二进制文件,可以不需要php解释器,pcc还提供了它自己的Web服务器,可直接运行,当然也可以配合和外部web服务器运行。 |
有 | 否,均已停止维护 |
第三方平台php编译器 | jphp、Phalanger、Peachpie | jphp是jvm平台的php编译器,可以把php编译成jvm字节码,让php在jvm上运行。令人惊喜的是jphp还支持php7.1全部新特性,支持php7.4大部分新特性,jphp项目组还为jphp开发了jphp的编辑器,支持桌面开发、android开发、游戏开发。
以上第三方平台编译器只是用了php语法,其本身和Zend Engine上的php并没有多大关系,就像android只用了java语法似的和java特性,和jvm半点关系没有。 |
有 | jphp在维护; Peachpie在维护; Phalanger已存档,停止维护 |
除了以上编译器外,还有c框架,如yaf,phalcon。但c框架并没有做什么根本性的改变,只是用c写了框架暴露api给php调用,而且c框架也是以扩展到形式嵌入到php里的。
“PHP的编译和执行分离的方案, 一直有人提, 也一直有人在做,所以php的第三方编译器有很多,不幸的是都先后停止更新维护了。编译和执行分离确实能带来根本性改变,只是要把这个事情真正做好、做成熟、做完善、做好用很难。从这个角度来看说php对程序员要求更高,因为它没有编译器,你写的代码烂就是烂,它zend引擎一点的不会动你的代码,所见即所得,这和javac(java编译器)完全相反,javac的编译优化是非常厉害的,菜鸟和大牛写的代码到运行时几乎没什么差别。
优点:
缺点:
这大概是目前最可行也是最普遍,最成熟的一种优化问题解决方案,又有大批的php做的大型网站架构能够借鉴,因此通过架构绕开语言本身来解决也何尝不是一种解决办法。站在架构的角度来看我们没必要和语言死磕,何必非要在语言上做优化呢,通过架构来解决问题也可以达到目的,求的是结果,不是过程。
站在架构层面看问题是— —能通过架构来解决的问题就不应该企图从语言层面解决。
根据业务类型,横向拆分项目成若干个模块,各自分布在不同的服务器上,根据二八原则,80%的流量集中在20%的功能上,我们只需要维护好核心模块也可以事半功倍的解决问题。
每一种架构都是为了解决一种问题,像现在很流行的微服务架构,微服务架构最初是java为了解决在中大型项目中一个功能出问题导致整个应用崩溃的难题而提出来的(因为java是单进程)。微服务并不一定是分布式,最初java做的事一个服务器上运行多个实例,每个实例都是一个模块,这样做计算其中一个模块出致命bug崩溃也不会影响到其他bug。
全球70%的网站都是php开发的,无论世界上TOP 20的网站,还是TOP 10,甚至TOP 5都有php的身影。
看到很多人盲目跟风唱衰php我是很心痛,作为从2013年纪开始接触php的人来说,我见证了它的成长,见证了它一点点从低谷到高峰,再到走向成熟。我见证了它的发展。这期间不计其数的语言如雨后春笋一般冒出来,也如昙花一现,之后便再也没有出现在人们的视线当中,甚至被人遗忘在灰暗的角落,好像它从来没有存在过。有些甚至从来没有登上过主流语言的舞台,perl、ruby、python、nodejs、golang......这些小众语言加起来都没有php市场份额的一半,主流语言的市场份额不是一朝一夕积累的,也不是一朝一夕能抢占的;主流语言的地位不是一朝一夕形成的,更不是一朝一夕能撼动的。
时间会证明谁的观察是对,谁都洞察是准确的。
从来都没有完美的语言,从来没有银弹。