这篇文章计划很久了一直感觉无从下手, 一直想全面、深入的写一篇关于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满载的那一刻你已经失去了对服务器的控制,你会发现服务器已经连不上了,你束手无策。
详解计算密集型和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影响响应速度,这问题就不是小问题,轻则宕机崩溃,重则系统卡死,数据丢失损坏。
我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。
在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
网卡、硬盘都由南桥芯片控制,并属于中、低速设备,所以,在服务器上进行网络通讯、网络传输、内存、磁盘读写均受南桥控制,此类即为IO操作。
IO密集型服务/业务即是以网络请求压力大、磁盘读写频繁的操作类型,当进行这些IO密集型操作时,CPU的负载相对较低(现代计算机均集成了对硬件访问控制的操作逻辑,使得CPU从这些操作中解放出来,提高核心资源的利用率)。
计算密集型,可以理解为在北桥芯片与CPU之间的通讯较高的服务/业务,往往这类操作常见的都是以计算为主的,而计算又是CPU/GPU的专长。
对于服务器,通过开发的服务或是业务,可以在项目之初就根据需求来对资源进行预先估算,大致属于IO密集型还是计算密集型的业务,并进行项目前期的资源预算等工作的开展,也包括前期的设计和后期的优化。
项目立项过程中,根据需求对应的资源负载类型,提出对服务资源的需求配置
IO密集型的需求,一般来说,如果是磁盘读写频繁,通过对磁盘进行升级,提高磁盘的响应速度和传输效率或通过负载技术,将文件读写分散到多台服务器中;如果是网络请求负载较高,可以通过负载均衡技术,水平扩展服务,提高负载能力;或使用代理缓存服务器,降低核心服务的负载压力。
计算密集型的需求,首先可以考虑使用计算能力更好的CPU,然后考虑通过消息队列或其它降维算法,将计算分散的不同的计算结点,进行处理。
项目开发时,进行合理的规划和业务开发
于IO密集型的需求,在开发过程中,就要考虑尽可能减少IO开销,对磁盘读写频繁的业务,可以考虑通过内存缓存将热数据缓存起来,减少磁盘的请求。
对于计算密集型的需求,在开发过程中,需要注意计算算法的优化及结果重用,并尽可能进行降维处理,比如通过某种算法将原业务需求的计算分散成可拆分的逻辑,并分散计算进行结果求解,最后进行组合(很像现在大数据处理里的一些模式,可以参考),或通过消息队列将大量的计算请求分发到其它的计算结点上去。
从语言层面切入,从语言本身来优化,直面问题
一、改变运行模式
优点:
缺点:
我之前就用php写过一个简单websServer当静态服务器用,如下:
<?php
/**
* @socket 通信的整个过程
* @socket_create //创建套接字
* @socket_bind //绑定IP和端口
* @socket_listen //监听相应端口
* @socket_accept //接收请求
* @socket_read //获取请求内容
* @socket_write //返回数据
* @socket_close //关闭连接
*/
class MyServer{
private $ip;
private $port;
private $webroot;
//将常用的MIME类型保存在一个数组中
private $contentType=array(
".html"=>"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语法,然后多个运行时环境,不一定非要依赖zend engine解释。就像java安卓一样,你看java在安卓上根本没用到jvm,也不需要jvm,但它依然是java语法,java特性,而且运行效率还比在jvm上的java高
优点:
缺点:
其实一直有一个疑问,既然swoole真的在密集型计算上有一定优势,但是不知道为什么国外一直不研发这种类型,这个方向的解决方案,反而第三方php编译器层出不穷,他们一直在搞编译器。
这大概是目前最可行也是最普遍,最成熟的一种优化问题解决方案,又有大批的php做的大型网站架构能够借鉴,因此通过架构绕开语言本身来解决也何尝不是一种解决办法。站在架构的角度来看我们没必要和语言死磕,何必非要在语言上做优化呢,通过架构来解决问题也可以达到目的,求的是结果,不是过程。
站在架构层面看问题是— —能通过架构来解决的问题就不应该企图从语言层面解决。
根据业务类型,横向拆分项目成若干个模块,各自分布在不同的服务器上,根据二八原则,80%的流量集中在20%的功能上,我们只需要维护好核心模块也可以事半功倍的解决问题。
我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
每一种架构都是为了解决一种问题,像现在很流行的微服务架构,微服务架构最初是java为了解决在中大型项目中一个功能出问题导致整个应用崩溃的难题而提出来的(因为java是单进程)。微服务并不一定是分布式,最初java做的事一个服务器上运行多个实例,每个实例都是一个模块,这样做计算其中一个模块出致命bug崩溃也不会影响到其他bug。
全球70%的网站都是php开发的,无论世界上TOP 20的网站,还是TOP 10,甚至TOP 5都有php的身影。
看到很多人盲目跟风唱衰php我是很心痛,作为从2013年纪开始接触php的人来说,我见证了它的成长,见证了它一点点从低谷到高峰,再到走向成熟。我见证了它的发展。这期间不计其数的语言如雨后春笋一般冒出来,也如昙花一现,之后便再也没有出现在人们的视线当中,甚至被人遗忘在灰暗的角落,好像它从来没有存在过。有些甚至从来没有登上过主流语言的舞台,perl、ruby、python、nodejs、golang......这些小众语言加起来都没有php市场份额的一半,主流语言的市场份额不是一朝一夕积累的,也不是一朝一夕能抢占的;主流语言的地位不是一朝一夕形成的,更不是一朝一夕能撼动的。
时间会证明谁的观察和洞察是准确的。
从来都没有完美的语言,从来没有银弹。