谈计算(cpu)密集型和io密集型与php性能优化
这篇⽂章计划很久了⼀直感觉⽆从下⼿, ⼀直想全⾯、深⼊的写⼀篇关于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密集型也叫计算密集型,指的是系统的磁盘、内存性能相对CPU要好很多,I/O读写可以在很短的时间就可以完成,IO读写却很快,没什么事情要做,⽽CPU还有许多运算要处理,CPU Loading⾮常⾼,CPU出现满载、负载情况,任务积压,需要处理很久,IO在等CPU。
并发、解析、多进程、多进程、并⾏处理、调度都是由CPU处理的,若CPU同时处理的任务过多就是CPU密集型,处理不过来就会负载,超频。
当然⼤部分情况都是CPU在等I/O读写完成返回。
什么是io密集型
IO是input、output两个单词的缩写,输⼊输出的意思。磁盘、内存的读写都属于都io范畴,频繁的IO读写就是IO密集。CPU的载荷情况很低,⽽硬盘、内存的负载很⾼就是IO密集型。 这也是web应⽤的特点,⼤多数情况都是在读写I/O,⽽CPU总是在等IO返回,另⼀⽅⾯也是因为CPU计算本⾝就⽐IO读写要快很多。
很多时候I/O读写在达到性能极限时,CPU占⽤率仍然较低。这是因为web应⽤场景下本就需要的⼤量的I/O操作,⽽计算任务很少,只有⽤户并发时才会出现需要⼤量的计算型任务。
在⼤多数情况下,拖慢响应速度都是IO,只有在极少数是CPU,⼀旦是CPU影响响应速度,这问题就不是⼩问题,轻则宕机崩溃,重则系统卡死,数据丢失损坏。
CPU计算密集型 vs I/O密集型
我们可以把任务分为计算密集型和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 语⾔最差。
异步IO
考虑到CPU和IO之间巨⼤的速度差异,⼀个任务在执⾏的过程中⼤部分时间都在等待IO操作,单进程单线程模型会导致别的任务⽆法并⾏执⾏,因此,我们才需要多进程模型或者多线程模型来⽀持多任务并发执⾏。
现代操作系统对IO操作已经做了巨⼤的改进,最⼤的特点就是⽀持异步IO。如果充分利⽤操作系统提供的异步IO⽀持,就可以⽤单进程单线程模型来执⾏多任务,这种全新的模型称为事件驱动模型,Nginx就是⽀持异步IO的Web服务器,它在单核CPU上采⽤单进程模型就可以⾼效地⽀持多任务。
在多核CPU上,可以运⾏多个进程(数量与CPU核⼼数相同),充分利⽤多核CPU。由于系统总的进程数量⼗分有限,因此操作系统调度⾮常⾼效。⽤异步IO编程模型来实现多任务是⼀个主要的趋势。
如何优化
如何运行php项目⽹卡、硬盘都由南桥芯⽚控制,并属于中、低速设备,所以,在服务器上进⾏⽹络通讯、⽹络传输、内存、磁盘读写均受南桥控制,此类即为IO操作。
IO密集型服务/业务即是以⽹络请求压⼒⼤、磁盘读写频繁的操作类型,当进⾏这些IO密集型操作时,CPU的负载相对较低(现代计算机均集成了对硬件访问控制的操作逻辑,使得CPU从这些操作中解放出来,提⾼核⼼资源的利⽤率)。
计算密集型,可以理解为在北桥芯⽚与CPU之间的通讯较⾼的服务/业务,往往这类操作常见的都是以计算为主的,⽽计算⼜是CPU/GPU 的专长。
对于服务器,通过开发的服务或是业务,可以在项⽬之初就根据需求来对资源进⾏预先估算,⼤致属于IO密集型还是计算密集型的业务,并进⾏项⽬前期的资源预算等⼯作的开展,也包括前期的设计和后期的优化。
1. 项⽬⽴项过程中,根据需求对应的资源负载类型,提出对服务资源的需求配置
IO密集型的需求,⼀般来说,如果是磁盘读写频繁,通过对磁盘进⾏升级,提⾼磁盘的响应速度和传输效率或通过负载技术,将⽂件读写分散到多台服务器中;如果是⽹络请求负载较⾼,可以通过负载均衡技术,⽔平扩展服务,提⾼负载能⼒;或使⽤代理缓存服务器,降低核⼼服务的负载压⼒。
计算密集型的需求,⾸先可以考虑使⽤计算能⼒更好的CPU,然后考虑通过消息队列或其它降维算法,将计算分散的不同的计算结点,进⾏处理。
2. 项⽬开发时,进⾏合理的规划和业务开发
于IO密集型的需求,在开发过程中,就要考虑尽可能减少IO开销,对磁盘读写频繁的业务,可以考虑通过内存缓存将热数据缓存起来,减少磁盘的请求。
对于计算密集型的需求,在开发过程中,需要注意计算算法的优化及结果重⽤,并尽可能进⾏降维处理,⽐如通过某种算法将原业务需求的计算分散成可拆分的逻辑,并分散计算进⾏结果求解,最后进⾏组合(很像现在⼤数据处理⾥的⼀些模式,可以参考),或通过消息队列将⼤量的计算请求分发到其它的计算结点上去。
从语⾔层⾯切⼊,从语⾔本⾝来优化,直⾯问题
PHP的两个优化⽅向
⼀、改变运⾏模式
php的优化思路有两种/两个⽅向, ⼀种基于cli运⾏模式与web服务器深度结合,常驻内存,php通过调⽤语⾔底层提供的socket套接字api来监听⽹络请求,这种事基于websocket拓展出来的⼀种优化思路,就是⼤多数php程序员所熟知的swoole那种优化思路 — —改变php运⾏模式。改变php运⾏模式后,php将不再按照原先那种作为(应⽤服务器http server)外部脚本来运⾏,⽽是⾸次运⾏后常驻进程,
⾄少会有⼀个master主进程,⾄于需不需要⼦进程或开多少⼦进程,那完全看你的实际应⽤场景了。
优点:
1. 这种运⾏模式优点是⾮常抗⾼并发,在应对⾼并发上优势显著,轻松应对数⼗万乃⾄百万并发。
2. 有望横向扩展各种连接池(和外部组件/软件更加紧密⾼效,⽐如数据库)、io线程池(i/o操作优化)...等,类似于静态语⾔的各种特性
3. 不⽤再每次重新解释运⾏,第⼀次解释运⾏即可,之后常驻进程不退出
4. 有望进军其他领域,不再局限于web主战场
5. 可实现异步,RPC远程调⽤
缺点:
1. 动态脚本语⾔运⾏在这种模式下存在很⼤的风险,稳定性较难以保证。php是弱类型语⾔,这种模式下类型出错可能会导致全盘崩
溃。
2. php在内存管理上颗粒度不是很细,可以说官⽅在语⾔设计的时候就没有考虑常驻内存这种运⾏模式,⽽⼤部分php程序员也没有
内存管理这个意识,在绝⼤多数情况下变量释放 不释放、各种连接、⽂件句柄close不close都没太⼤关系,也没太⼤⽤,反正就⼏百毫秒的⽣命周期,就⼀瞬间的事,不⽤你释放php退出进程后系统全部回收了,⼀切结束。
3. 隐患问题多、bug多
4. 虽然带来了显著的性能提升但也带来了新的问题。
5. 因为是常驻内存单进程运⾏模式,在这种模式下所有的的东西都是共享,不再是多进程模式,⽽这样⼀个地⽅或者⼀个请求出问题
可能影响整个项⽬,如果出现致命错误或者内存泄漏可能导致整应⽤崩溃。
6. 长时间常驻内存,内存容易出现飙升⽆法控制,尤其在⾼并发的场景下,极容易出现master猪进程占⽤内存过⾼,长期不释放。
7. 不成熟,不稳定,⾄少⽬前来说是(本⼈载过,下⾯会说)
8. 学习和使⽤成本较⾼
9. ⽤c扩展实现就像⼀个⿊匣⼦,⼀旦出问题就傻眼了,你束⼿⽆策。
10. 有⼈说,那可以⽤php本⾝实现,php确实可以实现,但php实现的运⾏效率不⾼,唯⼀的好处就是可控,出了问题可以查看源代码⾃
⼰改,当然这也得是能⼒强调。
我之前就⽤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做聊天服务器,才解决了这个问题
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论