基于 RoadRunner 驱动 Laravel Octane 构建高性能 Laravel 应用
Laravel Octane 已于昨天发布了 Beta 版,关于 Laravel Octane 学院君在之前专门发布过一篇文章简单介绍过,这是 Laravel 官方提供的基于 Swoole/RoadRunner 构建高性能 Laravel 应用的解决方案,现在你可以按照官方文档安装这个扩展包并进行测试。
由于后续学院君主要精力都在 Golang 上,这里我们以 RoadRunner 为例进行演示。
Laravel Octane 需要 PHP 8.0+ 及 Laravel 8.35+ 环境。
安装 Octane 扩展包
我们可以通过如下两条指令安装 Laravel Octane:
composer require laravel/octane
php artisan octane:install
接下来,我们就可以在 config/octane.php
中指定使用 Swoole 还是 RoadRunner 作为 HTTP 服务器,默认是 roadrunner
:
什么是 RoadRunner
RoadRunnber 是一个基于 Go 语言编写的高性能 PHP 应用服务器,它可以利用 Go 在并发编程中的优势,基于协程实现高性能的 HTTP 服务器,然后将用户请求转发给常驻内存的 PHP-Worker 进行处理,这样一来,在原有 PHP 代码基本不变的情况下,可以充分利用 Go 的高性能和 PHP 的开发效率打造支持高性能、高并发的 Web 系统:
更多详情可以参考 RoadRunner 官方文档:https://roadrunner.dev/。
通过 Sail 安装 RoadRunner
我们可以基于 Sail 的本地 Docker 开发环境中安装 RoadRunner:
./vendor/bin/sail up
./vendor/bin/sail composer require spiral/roadrunner
安装完扩展包后,还要在 Sail 容器环境中安装适用于当前 Linux 发行版本的 RoadRunner 二进制可执行文件:
./vendor/bin/sail shell
# 在 Sail shell 环境中执行
./vendor/bin/rr get-binary
至此,我们就准备好了 RoadRunner 底层环境,接下来,就可以基于 Octane 来启动 RoadRunner 服务器了。
通过 Octane 启动 RoadRunner
要实现这个功能,需要自定义 Sail 容器启动关联文件 supervisor.conf
,为此需要先发布它:
./vendor/bin/sail artisan sail:publish
然后修改 docker/8.0/supervisord.conf
中的 command 指令如下:
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=roadrunner --host=0.0.0.0 --port=80
这样一来,Sail 容器就会基于 RoadRunner 作为 PHP 应用的 HTTP 服务器。
确保项目根目录下的 rr
具备可执行权限后,重新构建 Sail 容器:
chmod +x ./rr
./vendor/bin/sail build
最后重新启动 Sail 容器中的服务:
./vendor/bin/sail down
./vendor/bin/sail up -d
这个时候,容器中的 Laravel 应用就是基于 RoadRunner 驱动的了。
基于 Swoole 驱动 Laravel Octane 的操作流程可以参考 Octane 官方文档,这里不再单独演示了。
Octane 日常使用
监听本地文件变动
RoadRunner/Swoole 之所以能够极大提升 Laravel 性能,本质上都是将 Laravel 应用常驻内存了,这样做的一个代价是牺牲了 PHP 本地调试的便利性,每次修改文件后需要重启 RoadRunner/Swoole 服务器才能让修改生效。
为了方便本地开发,Laravel Octane 引入了 --watch
标识告知 Octane 在项目文件发生变更后自动重启服务器,只需要在启动 Octane 时带上这个标识即可:
php artisan octane:start --watch
该功能依赖本地开发环境安装了 Node,并且安装了 Chokidar 文件监听库:
npm install --save-dev chokidar
指定 Worker 进程数和最大处理请求数
默认情况下,Octane 会根据机器 CPU 的内核数来启动对应数量的请求处理器进程(Worker),你也可以在基于 Octane 启动服务器时通过 --workers
参数手动指定 Worker 数量:
php artisan octane:start --workers=4
PHP 应用常驻内存带来的另一个问题是内存泄露,你可以通过 --max-request
参数指定一个 Worker 最多能够处理的请求数来解决这个问题:
php artisan octane:start --max-requests=250
当超过这个限制后,Octane 会优雅重启该 Worker。
优雅重启 Worker 进程
和 Nginx 类似,你可以通过 roload
指令优雅重启所有 PHP Worker 进程:
php artisan octane:reload
以上是 RoadRunner/Swoole 驱动 Octane 服务器的通用功能,针对 Swoole,Octane 还提供了独有的并发编程、定时器、高性能缓存等功能,你可以参考 Octane 文档了解明细,这里不专门介绍了。
注意事项
由于一个 Worker 会处理多个请求,而在同一个 Worker 中,只会在初始化时加载一次 Laravel 应用,后面的请求会复用第一次加载的服务容器(意味着所有服务提供者的 register
和 boot
方法只有第一次加载时会被调用,这就是所谓的「常驻内存」),所以我们在切换到基于 Laravel Octane 驱动 的 HTTP 服务器时,对于服务注入要格外小心,不要将后续会变动的对象以单例模式注入服务容器,也不要让有状态的数据被所有请求共享。
Octane 会在不同请求间自动处理所有官方框架提供功能的状态重置,但是无法重置你自己在业务代码中编写的全局状态,这里我们列举一些常见的容易出问题的几个典型示例,如果你的业务代码目前存在这些问题,需要进行调整。
容器注入
不要将服务容器、请求实例或者其他会发生变动的对象以单例模式注入到某个服务的构造函数:
use App\Service;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(Service::class, function ($app) {
return new Service($app);
});
}
这会导致后续请求只能解析出初次调用该 register
方法时传入构造函数的对象。要解决这个问题,可以通过普通模式注入或者闭包方式注入:
use App\Service;
use Illuminate\Container\Container;
$this->app->bind(Service::class, function ($app) {
return new Service($app);
});
$this->app->singleton(Service::class, function () {
return new Service(fn () => Container::getInstance());
});
这样一来,每次解析出来的都将是最新的对象实例。
请求注入
请求注入和服务容器类似,因为不同用户请求对象不同,并且可能带有认证状态,所以不能在不同请求之间共享,也就不能作为构造函数参数以单例模式注入服务容器:
use App\Service;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(Service::class, function ($app) {
return new Service($app['request']);
});
}
解决思路和服务容器一样,通过普通模式注入或闭包模式注入即可:
use App\Service;
$this->app->bind(Service::class, function ($app) {
return new Service($app['request']);
});
$this->app->singleton(Service::class, function ($app) {
return new Service(fn () => $app['request']);
});
// 或者,还可以直接在服务方法中传入具体请求字段值
$service->method($request->input('name'));
对于控制器而言,由于其构造函数也是在服务注册初始化期间完成的,所以不要在其构造函数中注入请求对象,但是可以在具体的控制器方法中注入 Illuminate\Http\Request
实例获取请求信息。
配置注入
应用配置也是一个会在运行时发生变更的对象,所以不应该在单例模式服务注入时以构造函数参数形式传入:
use App\Service;
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(Service::class, function ($app) {
return new Service($app->make('config'));
});
}
解决方案还是普通模式注入和闭包模式注入:
use App\Service;
use Illuminate\Container\Container;
$this->app->bind(Service::class, function ($app) {
return new Service($app->make('config'));
});
$this->app->singleton(Service::class, function () {
return new Service(fn () => Container::getInstance()->make('config'));
});
在低版本 Laravel 中引入 RoadRunner
目前 Laravel Octane 只能在 PHP 8.0+ 和 Laravel 8.35+ 版本中使用,如果想要在低版本 PHP/Laravel 中引入 RoadRunner/Swoole,该怎么做呢?
对应 Swoole 而言,对应的解决方案是 LaravelS 扩展包,对于 RoadRunner 而言,对应的解决方案是 RoadRunner 官方提供的 Laravel 扩展包,其安装流程也非常简单:
composer require spiral/roadrunner:v2.0 nyholm/psr7 # 安装 roadrunner 依赖
./vendor/bin/rr get-binary # 下载 roadrunner 二进制文件(与平台相关)
composer require spiral/roadrunner-laravel "^4.0" # 安装 roadrunner laravel 扩展包
php ./artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config # 发布配置文件
在项目根目录下更新下载 rr
过程中自动生成的 .rr.yaml
文件如下:
server:
command: "php ./vendor/bin/rr-worker start"
http:
address: 0.0.0.0:8080
middleware: ["headers", "static", "gzip"]
pool:
num_workers: 6
max_jobs: 0
supervisor:
exec_ttl: 60s
headers:
response:
X-Powered-By: "RoadRunner"
static:
dir: "public"
forbid: [".php"]
启动 RoadRunner:
./rr serve -c ./.rr.yaml
这样也可以访问基于 RoadRunner 驱动的 Laravel 应用。
基准测试性能对比
最后,我们来看下基于传统 PHP-FPM 驱动的 Laravel 应用和基于 RoadRunner 驱动的 Laravel 应用基准测试性能对比。
这里我们模拟通过 4 个线程对 50 个并发请求进行基准测试,持续时间是 30s,基于 PHP-FPM 驱动 Laravel 应用的 RPS 是 500+:
同等条件下,基于 RoadRunner 驱动 Laravel 应用的 RPS 则达到了 4000+,是 PHP-FPM 的 8 倍左右,在短短 30s 内处理的请求量达到了 12万+,各项细节指数也优于 PHP-FPM:
11 Comments
今天用了 swoole ,提升了10倍的 qps 。对于云原生,php 最大的问题可能还不是性能问题,而是环境的复杂性和资源的消耗太大。相比 go 一个应用10几兆的镜像,php 动不动就一个应用过G,内存好几百,运维难度和 devops 都没有优势。Laravel 是我用过最优雅的开发语言和框架,如果能集成运行时环境,并且减少资源使用率,简直完美。