当前位置: 首页 > 后端技术 > PHP

Chapter 14.PHP-FPM模式下,我为框架增加了伪异步(defer)功能

时间:2023-03-29 19:16:21 PHP

Chapter14.在PHP-FPM模式下,我在框架中加入了一个伪异步(defer)功能,但是又不想放弃傻瓜方案的优势;想了想,把问题转化为:提前返回response后,继续同步执行不重要的任务;顺序写逻辑,延迟一些不重要的任务的执行;解决这两个问题:使用fastcgi_finish_request;向golangdefer学习;实施、效果和注意事项。背景团队技术背景我们团队的技术情况如下:以PHP为开发语言开发Web类接口服务;使用传统的Nginx+FPM模式运行服务;我创建并维护了一个新的开发框架;框架基于团队之前依赖的Slimv3.7,考虑到过渡成本,暂时没有改动。场景问题最近在一些项目中,经常发现业务接口有如下特点:下面的主要逻辑)需要保证处理成功,并将结果反馈给调用者;支持:另一部分逻辑(如下面栗子中的2/4,简称分支逻辑)可以容忍(暂时的)失败,即使调用者不关心结果或感知不明显;simple:大部分分支逻辑比较简单,简单的判断加上文件日志记录,写一条MySQL日志记录,发送一个HTTP请求等;mixed:主分支逻辑在代码编写顺序上往往是交叉混杂而不是条理分明。关于代码编写的顺序,当然你也可以刻意将两部分分开,但这不符合常规开发同学的思路,也不利于代码阅读理解和变量控制。举个接口栗子(编):【main】一个重要的【Support】推演失败,不管推演成功与否,都会发送info级别的邮件信息;[Main]一个重要的交付逻辑,只有成功才能继续;[支持]发送失败会发送邮件错误级别的邮件,发送失败会记录本地文件日志;[main]组装结果并返回。同步模型先闭上眼睛,顺序写代码。同步执行过程如下(其实一开始我真的是这么一梭子实现的):异步模型不幸的是,有一天QA同学说你的接口响应时间太长了比较高,而且在执行过程中的性能压力测试更明显;更遗憾的是网上出现了2/4步DNS解析超时的问题(后来发现是整个libcurl的问题,不仅仅是DNS解析,当然这是另外一个话题了)这个传统的我们可能并不陌生以及本场景常用的解决方案——进程级异步任务解决方案!Laravel/Lumen也采用了这种方案。我们组的另一位同学也支持我们开发框架的这种异步任务方案。其实也可以直接采用,不过不是本文的主角。执行过程如下:当然线程、协程+异步IO调度模型还有很多其他方案,但是也比较复杂,不利于保持CGI+同步阻塞模型的门槛优势对团队成员和服务的稳定性和安全性,所以不是本文的重点。主角~尤其是今年团队的主基调是质量和效率,这个时候不想惹事~上面提到的异步模型其实是一个比较成熟的方案选择,但是也存在一些问题/缺点:队列服务依赖:需要额外依赖一个队列服务,这也在一定程度上依赖于它的可用性,也是一种网络IO开销;消费进程管理:需要一个独立的消费进程来消费队列中的异步任务,一个独立的消费进程还需要考虑其运行状态的维护和控制;handlinglinkgrowth:这个我就不多说了,虽然对于异步任务来说不是什么大事。对于分析和转换,有没有在保留现有优势的情况下实现轻量级(使用成本和运行效率)的解决方案?思考分析:由于还是采用了同步阻塞的方案,也就是说没有做串转并行的优化,原来的执行时间开销还是一样的;同时,如果想要轻量化,还需要把队列服务和消费者进程杀掉。所有逻辑只能由当前处理请求的FPM处理进程执行;可以实现伪“异步”吗?让调用者在感知上提前结束,但是FPM处理过程还是会完成剩下的逻辑。问题转换:PHP在FPM运行方式下能否提前将请求响应返回给调用者?代码逻辑可以顺序写,但是代码逻辑块的指定部分的执行会延迟到某个指定的逻辑之后吗?(这是因为我需要考虑能不能封装成一个框架,方便大家使用)问题1的解决方法:fastcgi_finish_request很容易想到fastcgi_finish_request,而FPM刚好是FastCGI模式,所以可以用过的。当然,在使用的时候也有一些要注意的地方。最后~印象中Laravel/Lumen有个终结者中间件,好像实现了类似的功能(先把response返回给调用者,然后继续执行终结者中间件逻辑),所以打算翻源码代码来调用和验证它。实现原理:我对此有印象是因为有同事在使用Laravel时,遇到了框架提供的session修改和保存方法没有生效的问题。在协助调查的时候,他大概看到了framework对session的操作都是进程内存级别的。是的,只有在Terminator中间件中,session才会在存储驱动中被完全覆盖。问题是同事没有使用框架提供的response方法进行逻辑处理后返回response,直接回显然后退出,这样就无法进行框架的后续处理了。返回响应并执行终结器中间件。..所以记忆犹新~~通过Laravel源码验证,这个功能也是通过fastcgi_finish_request实现的:阅读理解为:在kernel->handle之后得到response,在response->send中执行header和body设置后,执行fastcgi_finish_request当然也可以兼容其他运行模式,但是我暂时用不到$app=require_once__DIR__.'/../bootstrap/app.php';$kernel=$app->make(Kernel::class);$response=$kernel->handle($request=Request::capture())->send();$kernel->terminate($request,$response);/***发送HTTP标头。**@return$this*/publicfunctionsendHeaders():static{//headers已经被开发者发送了if(headers_sent()){return$this;}//headersforeach($this->headers->allPreserveCaseWithoutCookies()as$name=>$values){$replace=0===strcasecmp($name,'Content-Type');foreach($valuesas$value){header($name.':'.$value,$replace,$this->statusCode);}}//饼干foreach($this->headers->getCookies()as$cookie){header('Set-Cookie:'.$cookie,false,$this->statusCode);}//状态标题(sprintf('HTTP/%s%s%s',$this->version,$this->statusCode,$this->statusText),true,$this->statusCode);返回$这个;}/***发送当前网络响应的内容。**@return$this*/publicfunctionsendContent():static{echo$this->content;}返回$这个;}/***发送HTTP标头和内容。**@return$this*/publicfunctionsend():static{$this->sendHeaders();$this->sendContent();如果(\function_exists('fastcgi_finish_request')){fastcgi_finish_request();}elseif(\function_exists('litespeed_finish_request')){litespeed_finish_request();}elseif(!\in_array(\PHP_SAPI,['cli','phpdbg'],true)){static::closeOutputBuffers(0,true);}返回$this;}问题2解决方法:参考golangdefer之前的文章,想考虑framework封装,方便群里的同学使用,所以不能裸写,但是考虑到大家方便的时候使用它我认为如果代码逻辑块可以同步写入将是最理想的,但是将标记的代码逻辑块的执行顺序延迟到请求响应返回将是最理想的。等等,这不是类似于golang中的defer吗?而且都是延迟执行的意思!当然,它们本质上是不同的:golang的defer是语言层面的,它的作用是延迟逻辑块的执行,直到当前函数栈帧即将被销毁。请参考《第十五章Godefer介绍与实现》;它在框架层(虽然我也觉得如果php有一个语言级别的defer就好了),用来延迟逻辑块的执行,直到请求响应结束golang的defer的实现是通过链表实现的,php-array走遍天下哈哈!首先定义一个Defer类来注册判断执行逻辑块(可调用)为'defer':classDefer{protectedarray$deferList=[];publicfunctiondefer(callable$function):void{$this->deferList[]=$function;}publicfunctionisEmpty():bool{returnempty($this->deferList);}publicfunctionrun():void{foreach($this->deferListas$function){$function();}}}实例化Defer并挂载到Container上;执行slimApprun,在业务逻辑中可以注册'defer';如果'defer'被注册,每个'defer'都会在提前返回请求响应后执行':$container['defer']=newDefer();$app->run();//响应后的任务$container=$app->getContainer();if($container->has('defer')){$defer=$container['defer'];如果(!$defer->isEmpty()){fastcgi_finish_request();$延迟->运行();}}'defer'业务逻辑中的注册://TODO1$container['defer']->defer(function(){//TODO2});//TODO3$container['defer']->defer(function(){//TODO4});//TODO5对嵌套defer的影响理论上也可以像go的defer一样任意嵌套,但是由于是延迟到请求返回response,嵌套就没有意义了,反而会增加编码和阅读的复杂度,所以它根本不支持使用闭包变量。注意变量的生命周期。毕竟不像go的defer,语言层面执行到defer的时候,先处理完函数签名再继续。具体来说,闭包和变量生命周期相关参考:《第16章.PHP变量生命周期》《第17章.PHP闭包》延迟运行模型通过上面一系列的手势,我们写的代码逻辑执行顺序发生了顺序变化:从上面的理论效果不难看出,伪异步模型可以带来:提高界面响应速度(因为响应是提前返回的);CPU等硬件资源和FPM等软件资源未达到瓶颈时提升QPS性能(QPS=并发)*平均响应时间消耗,并发不变,响应时间消耗减少,收到响应后继续请求,因为资源足够了,QPS增加);当CPU等硬件资源和FPM等软件资源达到瓶颈时,QPS性能无法提升(提前返回请求后,再次发起请求,原来的逻辑还没处理完,就到了瓶颈再次,所以QPS无法提高)。另外,伪'异步'还在FPM进程中,不能像之前传统的异步那样处理长任务:仍然受限于PHP的最大CPU执行时间(php.ini的max_execution_time);但不再受制于FPM请求终止超时(fpm.conf的request_terminate_timeout);想想长任务占用了FPM,那新的请求呢?