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

集成Laravel和Swoole,Shadowfax是这样做的

时间:2023-03-30 04:47:21 PHP

给大家推荐了Shadowfax扩展包,现在说说Shadowfax是如何集成Laravel和Swoole的。PHP为什么“慢”众所周知,PHP是一种解释型语言,解释型语言的特点是在运行时进行编译。当执行PHP脚本时,Zend引擎首先解析并构建语法树,然后将语法树编译成opcode,最后执行opcode。并且每次执行都要重复上述步骤,这也是其性能低下的原因之一。不过PHP早在5.5版本就引入了opcache技术。解析编译后,将操作码缓存起来,使性能得到了质的提升。但是,由于PHP每次都分配新的内存来执行操作码,这导致无法重用资源。Swoole可以改变这一切。它使程序驻留在内存中,不仅让程序代码只需要解析编译一次,还实现了资源复用,从而大大提高了程序运行的效率。简单的版本集成让Laravel运行在Swoole上的想法并不难。熟悉Swoole的朋友应该知道,使用Swoole创建HTTP服务器只需要设置一个请求回调,那么我们就可以把Laravel搬进请求回调中去执行呢?确实如此,我们试试看,先新建一个Laravel项目:composercreate-project--prefer-distlaravel/laravel然后在Laravel项目根目录下新建一个swoole.php脚本,代码如下:set(['worker_num'=>1,'enable_coroutine'=>false,]);$server->on('request',function($request,$response){$app=require__DIR__.'/bootstrap/app.php';$kernel=$app->make(Kernel::class);$illuminateResponse=$kernel->handle($illuminateRequest=Request::make($request)->toIlluminate());响应::make($illuminateResponse)->send($response);$kernel->terminate($illuminateRequest,$illuminateResponse);});$服务器->开始();阅读过Laravel源码的朋友会发现,请求回调里面的代码其实就是public/index.php里面的代码,只不过多了两个比较陌生的类:Hua??ngYi\Shadowfax\Http\Request和HuangYi\Shadowfax\Http\Response,这两个类都来自huang-yi/shadowfax,因为Swoole的request/response对象和Laravel的request/response对象是不兼容的,需要转换,这两个类负责兼容工作。我们不关心它们的具体实现,只需要将huang-yi/shadowfax包require转换为当前项目供我们使用即可(composerrequirehuang-yi/shadowfax)。接下来运行脚本:phpswoole.php然后打开浏览器,访问http://127.0.0.1:9501,看到熟悉的Laravel欢迎页面了吗?至此我们完成了最简单版本的集成。如果你做一个基准测试,你会发现它的性能比运行在PHP-FPM上的Laravel要好得多。熟悉Laravel的朋友都知道IoC容器是整个框架的核心,Laravel提供的几乎所有服务都注册在IoC容器中。每当容器启动时,Laravel都会将大部分服务注册到容器中,部分服务还会加载文件,比如配置、路由等,可以说启动容器是比较“耗时”的。我们再观察一下上面的脚本,可以看到请求回调的第一行是创建IoC容器($app),也就是说每次处理请求都会创建容器,不仅重复执行了很多代码,同时也造成了不小的IO开销,所以上面的脚本显然不是最优的。那我们试试只创建一个容器,然后让所有的请求都复用这个容器。我们可以在worker进程启动时(也就是在workerStart回调中)创建并启动容器,这样就可以在request回调中复用。现在将swoole.php调整一下:set(['worker_num'=>1,'enable_coroutine'=>false,]);$app=null;$server->on('workerStart',function()use(&$app){$app=require__DIR__.'/bootstrap/app.php';$app->instance('请求',IlluminateRequest::create('http://localhost'));$app->make(Kernel::class)->bootstrap();});$server->on('request',function($request,$response)使用(&$app){$kernel=$app->make(Kernel::class);$illuminateResponse=$kernel->handle($illuminateRequest=Request::make($request)->toIlluminate());响应::制作($illuminateResponse)->发送($response);$kernel->terminate($illuminateRequest,$illuminateResponse);});$server->start();重新运行swoole.php后,打开浏览器调试工具,再次请求首页,会发现页面响应变快了。如果你使用基准测试工具进行测试,你也会发现与第一版脚本相比,性能提升了很多。资源污染问题说到资源再利用,就不得不面对资源污染问题。传统的PHP程序每次执行后都会销毁,不会对下一次执行产生任何影响,所以PHP程序员很少担心变量污染。在Laravel中出于性能的考虑,大量的服务以单例的形式注册在IoC容器中,而这些单例很容易在常驻内存的程序中造成副作用。举个简单的例子,Laravel的auth组件就是一个典型的单例服务。用户完成登录后,当前的User对象会被保存在一个成员变量中,那么下次请求调用auth组件时获取的User对象仍然保存在上一次请求中,这样会造成用户身份的混淆,导致在异常数据中,这是非常可怕的。要解决资源污染问题,我们只需要在请求结束后清理或恢复那些“被污染的资源”即可。对于Laravel容器中的服务,我们可以这样清理:getAlias($abstract);$binding=$app->getBindings()[$abstract]??null;unset($app[$abstract]);if($binding){$app->bind($abstract,$binding['concrete'],$binding['shared']);}你可以看到如果抽象有绑定关系,会被反弹到容器中,让服务持续可用。这段代码可以在src/Laravel/RebindsAbstracts.php的Shadowfax源代码中找到。在Shadowfax的配置文件中,提供了一个名为abstracts的数组来帮助开发者清理容器中被污染的服务。当然,有些开发者会使用全局变量或者静态变量来存储数据。这些也是很容易被污染的资源,但是需要开发者自己去处理。Shadowfax在程序执行的各个阶段都提供了事件接口,开发者可以通过监听事件来注入自己的代码。HuangYi\Shadowfax\Events\AppPushingEvent事件可以帮助开发者注入自定义清理代码。此事件将在Shadowfax回收容器之前触发。你可以像这样定义一个监听器:customListener为bootstrap/shadowfax.php文件中的事件监听器:'事件')->listen(AppPushingEvent::class,newCleanPollutedData);return$shadowfax;启用协程协程是Swoole的最强武器,也是实现高并发的精髓所在。那么在Laravel中使用协程会不会有问题呢?我们来做一个简单的实验,首先开启Swoole的协程特性,将enable_coroutine设置为true,然后在routes/web.php中添加两条测试路由:singleton('counter',function(){$counter=newstdClass;$counter->number=0;return$counter;});Route::get('one',function(){app('counter')->number=1;协程::sleep(5);echosprintf("one:%d\n",app('counter')->number);});Route::get('two',function(){app('counter')->number=2;Coroutine::sleep(5);echosprintf("two:%d\n",app('counter')->number);});上面的代码首先在容器中注册了一个计数器单例,routingone将计数器单例的number属性设置为1,然后模拟协程被挂起5秒,恢复后打印出number属性的值。路由二类似,但是number属性设置为2。启动服务器后,我们先访问一个,然后立即访问二(间隔不要超过5秒)。我们可以观察到控制台输出的信息是:一:2二:2结果没有达到我们的预期。这是因为容器是共享的,两个请求访问同一个计数器单例。当请求一挂起时,请求二将number属性更改为2,所以请求一打印的值也是2。那么我们是否可以通过资源污染的解决方案来解决这个问题呢?当然是行不通的,而且结果会更加诡异。请求一打印出来的值还是2,请求二打印出的值是0。因为当请求一结束的时候,cleaner会重置计数器单例,此时number的值又变成了0。所以在协程环境下,我们无法共享IoC容器。我们应该为每个协程提供一个容器,以保证程序的正常执行。那么问题又来了。当我们的应用程序并发量很大的时候,意味着同时运行的协程也很多。如果给每个协程都提供一个容器,内存不就爆了吗?这里我们将使用“池”技术来解决这个问题。worker进程启动时,使用Swoole的Channel创建容器池。当一个请求来的时候,从当前协程环境的容器池中取出一个容器。End然后返回到容器池中,那些拿不到容器的协程会等到拿到容器后再执行。Shadowfax在启动worker进程时,会判断服务器是否开启了协程特性。如果启用,它将创建一个容器池,否则它将重用一个容器以实现最佳性能。Shadowfax只会为每个请求分配一个容器,如果有子协程,它会使用父协程中的容器。协程环境Laravel容器中的app()方法采用单例模式,其构造函数中会调用static::setInstance($this)。这一步会将创建的容器保存到一个静态变量(Container::$instance)中,这样就可以通过Container::getInstance()方法获取容器单例。另外,Laravel还提供了一个辅助函数app()来获取容器单例,这个函数被广泛使用。也正是因为单例这个特性,如果我们在协程环境下使用app()函数,得到的总是同一个容器,导致容器池无用武之地。想到的第一个解决方案是在每个容器从池中取出后立即调用Container::setInstance()将其设置为全局容器(即覆盖Container::$instance的值)。但是,此解决方案存在问题。如果A协程在暂停期间执行B协程,则全局容器会被B协程的容器覆盖。然后当A协程恢复时,将调用app()方法。是B协程的容器。遗憾的是Swoole没有提供coroutineYied和coroutineResume等事件,不然我们只能通过监听事件来切换,确实很头疼。最后,Shadowfax使用了一个有点老套的解决方案。既然恢复协程的时候不能切换,那我们就在Container::getInstance()方法中切换。为了实现这个方案,首先需要将获取到的容器保存在当前协程的上下文中,以便协程恢复时可以直接从上下文中获取。然后在Container::getInstance()方法中加入切换逻辑,判断当前协程Context中的容器是否与全局容器相同。如果没有,只需将当前协程容器替换为全局容器即可。具体实现请参考Shadowfax源码,位于src/helpers.php文件中的shadowfax_correct_container()函数。下一个问题是如何将这段用于切换容器的代码注入到Container::getInstance()方法中。第一个想到的方案是通过类继承实现注入,然后重写getInstance方法。但是这种方法需要把bootstrap/app.php中的Illuminate\Foundation\Application改成继承的类名,侵入性太大。如果有一天我想切换回PHP-FPM模式,我也需要把类名修改回来,所以果断放弃这个计划。Shadowfax的做法是这样的。程序启动时,首先读取vendor/laravel/framework/src/Illuminate/Container/Container.php的文本内容,然后使用字符串替换的方式将shadowfax_correct_container()函数写入getInstance方法中。去吧,另存为一个新的coroutine_container.php文件,最后我们只需要将coroutine_container.php文件require到程序中即可。需要理解的一点是,由于coroutine_container.php文件中也提供了Illuminate\Containe\Container类,一旦程序需要,就不会再通过autoload方式加载Laravel框架中的Container类,从而实现置换效果。现在,您可以安全地在程序中使用app()函数。这个方案虽然很简陋,但是确实很有效。既能保证Shadowfax的功能,又能在没有Shadowfax的情况下运行程序依然正常。有兴趣的朋友可以阅读Shadowfax的源码,这个逻辑位于src/Bootstrap/CreateCoroutineContainer.php。数据库连接池现代web应用几乎离不开数据库的使用。如果数据库在没有连接池的协程环境下使用,会出现连接异常。当然,使用Swoole的Channel创建连接池是很简单的,但是如果直接在业务代码中使用连接池,需要程序员控制什么时候取,什么时候回收,不能用Laravel的模型了。我绝对确定这是不可接受的。还有一点,既然业务代码中使用了Swoole接口,那么就意味着你的程序必须运行在Swoole上,不能再切换回PHP-FPM。Shadowfax在没有感知的情况下使用连接池。开发人员仍然像往常一样使用模型来查询或更新数据。唯一需要做的就是将程序中使用的数据库连接名配置为db_pools。Shadowfax是如何做到的?我们只需要稍微弄明白原理就明白了。Laravel中的数据库连接是通过Illuminate\Database\DatabaseManager::connection()方法获取的。我们可以继承这个类,修改connection()方法。如果我们取的是db_pools中配置的连接,那么就从对应的连接池中获取。最后,使用这个修改后的类注解覆盖原来的db服务。具体实现请阅读源码,文件为src/Laravel/DatabaseManager.php。当然Shadowfax也支持redis连接池,只需将程序中使用的连接名配置为redis_pools即可。结论相信和我一样使用这个扩展包的人都非常喜欢Laravel。Laravel的开发体验让我们爱不释手,所以Shadowfax会在整个设计过程中避免破坏这种体验,尽量让开发者以最低的成本应用Laravel。在Swoole上运行以获得性能提升。Shadowfax是一个开源项目,它的诞生也花费了作者大量的时间和精力。如果觉得有用,请投个star,表示支持。如果您在使用过程中遇到问题,请提交问题。如果你能改进程序,欢迎你提交PR。开源项目需要每个人都做出贡献。