关于laravel:Laravel-Octane-初体验

31次阅读

共计 8940 个字符,预计需要花费 23 分钟才能阅读完成。

Laravel Octane 曾经公布好几周了,虽说目前还处于 beta 状态,也挡不住开发者对他的酷爱,一个月不到,其在 GitHub 的 star 数量已超过 2K;局部开发者已将他们的我的项目运行在 Laravel Octane 之上。

如果你还在张望,也可等等一两周后的稳定版(原文公布于 2021-04-29,目前 (05-27) 已 releases)。

We will likely go ahead and tag Octane 1.0 as stable next week Taylor Otwell on Twitter.

为了体验一把减速的魔力,作者已拿一个简略的 H5 我的项目在生产环境下试了试水,除了一些乌七八糟的问题,其余的都令作者激动不已,客户还示意咱们的平台好快啊,下次还找你。

Laravel Octane 的组成

Laravel Octane 内置了两个高性能的应用服务:Swoole 和 RoadRunner,正如官网文档介绍的:

Octane boots your application once, keeps it in memory, and then feeds it requests at supersonic speeds.

咱们晓得,Laravel 框架始终很优良,然而他在性能方面却始终为人诟病。框架的 boot 工夫可能比业务解决工夫还长,并且随着我的项目第三方 service provider 的增多,其启动速度越来越不受控。而 Laravel Octane 则通过启动 Application 一次,常驻内存的形式来减速咱们的利用。

Laravel Octane 须要 PHP8.0 反对,如果你是在 macOS 下工作,你能够参考这篇文章来更新你的 PHP 版本 Upgrade to PHP 8 with Homebrew on Mac。

Octane 简略示列

虽说官网文档曾经形容的很具体,不过作者这里还是通过一个简略的示列我的项目来演示。

Create Laravel Application

➜ laravel new laravel-octane-test

 _                               _
| |                             | |
| |     __ _ _ __ __ ___   _____| |
| |    / _` | '__/ _` \ \ / / _ \ |
| |___| (_| | | | (_| |\ V /  __/ |
|______\__,_|_|  \__,_| \_/ \___|_|

Creating a "laravel/laravel" project at "./laravel-octane-test"
Installing laravel/laravel (v8.5.16)
...
Application ready! Build something amazing.

Install Laravel Octane

$ composer require laravel/octane

装置胜利后,读者能够间接执行 artisan octane:install 来装置依赖;Octane 将提醒你想应用的 server 类型。

➜ php artisan octane:install

 Which application server you would like to use?:
  [0] roadrunner
  [1] swoole
 >

如果你抉择的是 RoadRunner,程序将会主动帮你装置 RoadRunner 所需的依赖;而如果你抉择的是 Swoole,你只须要确保你曾经手动装置了 PHP swoole 扩大。

应用 RoadRunner Server

RoadRunner 的应用过程不尽人意,作者在装置过程中总会呈现一些官网文档漠视的谬误。

下载 rr 可执行文件失败

在执行 octane:install 装置 RoadRunner 依赖时,作者本机根本无法通过 GitHub 下载 rr 可执行文件,提醒的谬误如下:

In CommonResponseTrait.php line 178:

HTTP/2 403  returned for "https://api.github.com/repos/spiral/roadrunner-binary/releases?page=1".

如果你也遇到了这样的谬误,倡议间接去 RoadRunner 官网 下载对应平台的 rr 可执行文件及 .rr.yaml 配置文件并放到我的项目根目录。如 macOS 平台的可执行文件及配置文件地址:

  • https://github.com/spiral/roa…
  • https://github.com/spiral/roa…

最初记得批改 rr 的可执行权限及 RoadRunner 的 Worker starting command。

chmod +x ./rr
server:
  # Worker starting command, with any required arguments.
  #
  # This option is required.
  command: "php artisan octane:start --server=roadrunner --host=127.0.0.1 --port=8000"

ssl_valid: key file ‘/ssl/server.key’ does not exists

RoadRunner 的配置文件中,默认开启了 ssl 配置,若你不须要启用 https 拜访,可正文 http.ssl 配置。

Error while dialing dial tcp 127.0.0.1:7233

RoadRunner 默认开启 temporal 个性,其 listen 端口为 7233,若你不想启用该个性,可正文 temporal 配置。

# Drop this section for temporal feature disabling.
temporal:

对于 temporal 的信息可查看官网 temporalio/sdk-php: Temporal PHP SDK

Executable file not found in $PATH

这种状况个别是配置文件中未制订程序执行门路,请查看以下配置。

  1. Server.command

批改为 RoadRunner worker 的启动命令,如:

php artisan octane:start —server=roadrunner —host=127.0.0.1 —port=8000
  1. Service.some_service_*.comment

如果你不想应用该个性,正文该配置。至此,作者的 RoadRunner 终于 启动起来了。

AB Test For RoadRunner

作者用本人的笔记本 (2018-13inch/2.3GHz/16GB) 做了一个简略的 AB Test,框架代码未做任何改变,为 Laravel 默认的 welcome 页面。

通过扭转不同的并发参数和申请数,失去的后果都如下图所示高低轻微稳定,其 QPS 根本维持在 230/s 左右。

➜  ~ ab -n 2000 -c 8 http://127.0.0.1:8000/
Server Software:
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /
Document Length:        17490 bytes

Concurrency Level:      8
Time taken for tests:   8.418 seconds
Complete requests:      2000
Failed requests:        0
Total transferred:      37042000 bytes
HTML transferred:       34980000 bytes
Requests per second:    237.59 [#/sec] (mean)
Time per request:       33.671 [ms] (mean)
Time per request:       4.209 [ms] (mean, across all concurrent requests)
Transfer rate:          4297.28 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        3   11   4.6     11      29
Processing:     3   20  34.8     15     270
Waiting:        3   18  34.8     12     270
Total:          7   31  35.2     25     284

默认状况下,Laravel 的 welcome 页面会先通过 web 中间件,最初在渲染 blade 页面;而 web 中间件蕴含大量 Cookie 和 Session 操作:

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

所以作者从新定义了一个测试路由,该路由不蕴含任何中间件(全局除外),并只输入一个 Hello World。

// RouteServiceProvider.php
public function boot()
{require base_path('routes/test.php');
}

// test.php
Route::get('/_test', function () {return 'Hello World';});

再次测试后如下,能够看到其 QPS 曾经达到官网宣传规范 2300/s(难道官网测试也是这样 Remove All Middleware?)。

Server Software:
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /_test
Document Length:        11 bytes

Concurrency Level:      8
Time taken for tests:   0.867 seconds
Complete requests:      2000
Failed requests:        0
Total transferred:      374000 bytes
HTML transferred:       22000 bytes
Requests per second:    2307.81 [#/sec] (mean)
Time per request:       3.466 [ms] (mean)
Time per request:       0.433 [ms] (mean, across all concurrent requests)
Transfer rate:          421.45 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       3
Processing:     1    3   8.8      2     143
Waiting:        1    3   8.8      2     142
Total:          1    3   8.8      2     143

上述测试过程中,作者本机的资源限度如下。

~ ulimit -n
256

应用 Swoole Server

Swoole server 的应用就要顺畅多了;通过 pecl 装置好 PHP swoole 扩大后,无需任何配置就能启动。

AB Test For Swoole Server

作者用同样的配置对 swoole server 进行 AB Test,后果如下,其 QPS 也根本维持在 230/s 左右。

Server Software:        swoole-http-server
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /
Document Length:        17503 bytes

Concurrency Level:      8
Time taken for tests:   8.398 seconds
Complete requests:      2000
Failed requests:        0
Total transferred:      37130000 bytes
HTML transferred:       35006000 bytes
Requests per second:    238.15 [#/sec] (mean)
Time per request:       33.592 [ms] (mean)
Time per request:       4.199 [ms] (mean, across all concurrent requests)
Transfer rate:          4317.61 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        3   11   6.6     10     102
Processing:     4   20  50.3     12     442
Waiting:        2   18  50.3     11     441
Total:          7   30  50.9     23     450

无中间件路由测试后果如下,能够看到其 QPS 已达到了 1650/s。

Server Software:        swoole-http-server
Server Hostname:        127.0.0.1
Server Port:            8000

Document Path:          /_test
Document Length:        21 bytes

Concurrency Level:      8
Time taken for tests:   1.212 seconds
Complete requests:      2000
Failed requests:        0
Total transferred:      528000 bytes
HTML transferred:       42000 bytes
Requests per second:    1650.63 [#/sec] (mean)
Time per request:       4.847 [ms] (mean)
Time per request:       0.606 [ms] (mean, across all concurrent requests)
Transfer rate:          425.55 [Kbytes/sec] received

从 AB Test 后果来看,两种 Server 的性能根本持平;但因为是在本地开发环境测试,未思考到的因素较多,测试后果仅供参考。

部署上线

Laravel Octane 尽管提供了 start 命令用于启动 Server,但该命令只能在前台运行(不反对 -d);在部署到生产环境时,常见的方法还是利用 Supervisor 来进行过程治理。读者能够参考 Laravel Sail 的 Supervisor 配置。

[program:php]
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=127.0.0.1 --port=80
user=sail
environment=LARAVEL_SAIL="1"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

后续继续交付时,可通过 Jenkins 连贯到服务节点,应用 octane:reload 命令从新加载服务。

stage("部署 ${ip}") {withCredentials([sshUserPrivateKey(credentialsId: env.HOST_CRED, keyFileVariable: 'identity')]) {remote.user = "${env.HOST_USER}"
        remote.identityFile = identity
        sshCommand remote: remote, command: "php artisan config:cache && php artisan route:cache && php artisan octane:reload"
    }
}

不过这里须要留神的是,当你更新了 Composer 依赖,如新增了一个第三方包时,你最好在生产环境重启下 Laravel Octane。

sudo supervisorctl -c /etx/supervisorctl.conf restart program:php

否则可能会呈现如 Class “Godruoyi\Snowflake\Snowflake” not found 的谬误。

Laravel Octane 是线程平安的吗?

在答复这个问题之前,咱们先来看看 Laravel Octane 的申请解决流程。

随着 Server 的启动,程序会创立指定数量的 Worker 过程。当申请到来时,会从可用的 Worker 列表中选取一个并交由他解决。每个 Worker 同一时刻只能解决一个申请,在申请处理过程中,对资源(变量 / 动态变量 / 文件句柄 / 链接)的批改并不会存在竞争关系,所以 Laravel Octane 时线程 (过程) 平安的。

这其实和 FPM 模型是统一的,不同的中央在于 FPM 模型在解决完一个申请后,会销毁该申请申请的所有内存;后续申请到来时,仍然要执行残缺的 PHP 初始化操作(参考 PHP-FPM 启动剖析)。而 Laravel Octane 的初始化操作是随着 Worker Boot 进行的,在整个 Worker 的生命周期内,只会进行一次初始操作(程序启动的时候)。后续申请将间接复用原来的资源。如上图,Worker Boot 实现后,将会初始化 Laravel Application Container,而后续的所有申请,都将复用该 App 实例。

Laravel Octane 工作原理

Octane 只是一个壳,真正解决申请都是由内部的 Server 解决的。不过 Octane 的设计还是值得一说的。

从源码也能够看出,随着 Worker 的 Boot 实现,Laravel Application 已被胜利初始化。

// vendor/laravel/octane/src/Worker.php
public function boot(array $initialInstances = []): void
{
    $this->app = $app = $this->appFactory->createApplication(
        array_merge(
            $initialInstances,
            [Client::class => $this->client],
        )
    );

    $this->dispatchEvent($app, new WorkerStarting($app));
}

在解决后续到来的申请时,Octane 通过 clone $this->app 获取一个沙箱容器。后续的所有操作都是基于这个沙箱容器来进行的,不会影响到原有的 Container。在申请完结后,Octane 会清空沙箱容器并 unset 不再应用的对象。

public function handle(Request $request, RequestContext $context): void
{CurrentApplication::set($sandbox = clone $this->app);

    try {$response = $sandbox->make(Kernel::class)->handle($request); 

    } catch (Throwable $e) {$this->handleWorkerError($e, $sandbox, $request, $context, $responded);
    } finally {$sandbox->flush();

        unset($gateway, $sandbox, $request, $response, $octaneResponse, $output);

        CurrentApplication::set($this->app);
    }
}

再次留神,因为同一个 Worker 过程同一时刻只能解决一个申请,故这里是不存在竞争的,即便是对 static 变量的批改,也是平安的。

注意事项 & 第三方包适配

因为同一个 Worker 的多个申请会共享同一个容器实例,所以在向容器中注册单例对象时,应该特地小心。如上面的例子:

public function register()
{$this->app->singleton(Service::class, function ($app) {return new Service($app['request']);
    });
}

例子中采纳 singleton 注册一个单例对象 Service,当该对象在某个 Provider 的 Boot 办法被初始化时,利用容器中将始终保持着惟一的 Service 对象;后续 Worker 在解决的其余申请时,从 Service 中获取的 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']);
});

// Or...

$service->method($request->input('name'));

强烈推荐读者浏览官网提出的注意事项。如果你感觉文章对你有帮忙,你也能够订阅作者的博客 RSS 或间接拜访作者博客 二愣的闲聊杂鱼。

参考

  • Upgrade to PHP 8 with Homebrew on Mac https://stitcher.io/blog/php-…
  • Laravel Octane https://github.com/laravel/oc…
  • Laravel Sail https://github.com/laravel/sail
  • FastCgi 与 PHP-fpm 之间的关系 https://godruoyi.com/posts/th…
  • PHP-FPM vs Swoole https://developpaper.com/php-…
  • Swoole 编程须知 https://wiki.swoole.com/#/get…
  • 原文地址 https://godruoyi.com/posts/la…

正文完
 0