初识 Laravel Octane

初识 Laravel Octane

Octane 发布已有两年多了,最近才重新看相关内容。从 GitHub 来看,目前 Octane 已经非常稳定。最初阶段,我留意到有不少问题,但现在问题已经很少,目前只剩下一个下载问题。这个问题似乎并不严重,主要是由于下载大文件导致的,无法以流的形式输出,从而导致内存溢出。

安装

安装 Octane 非常简单,只需运行以下命令:

composer require "laravel/octane"

选择使用 Server

php artisan octane:install

我选择使用 Swoole,因为这是我目前最熟悉的。虽然我听说过 RoadRunner 和 FrankenPHP,但从未实际使用过。

启动


php artisan octane:start --workers=5

流程

首先找到 vendor/laravel/octane/src/Commands/StartCommand.php 文件,这是启动命令的文件。


// 找到 handle 方法

public function handle()

{

    $server = $this->option('server') ?: config('octane.server');

    return match ($server) {

        // 只关注与 Swoole 相关的代码

        // 找到启动的 startSwooleServer

        'swoole' => $this->startSwooleServer(),

        'roadrunner' => $this->startRoadRunnerServer(),

        'frankenphp' => $this->startFrankenPhpServer(),

        default => $this->invalidServer($server),

    };

}

接着找到 startSwooleServer() 方法。

// vendor/laravel/octane/src/Commands/StartCommand.php
protected function startSwooleServer()

{

    return $this->call('octane:swoole', [

        '--host' => $this->getHost(),

        '--port' => $this->getPort(),

        '--workers' => $this->option('workers'),

        '--task-workers' => $this->option('task-workers'),

        '--max-requests' => $this->option('max-requests'),

        '--watch' => $this->option('watch'),

        '--poll' => $this->option('poll'),

    ]);

}

继续查找,找到与 octane:swoole 相关的命令文件。

// vendor/laravel/octane/src/Commands/StartSwooleCommand.php
public function handle(

        ServerProcessInspector $inspector,

        ServerStateFile $serverStateFile,

        SwooleExtension $extension

    ) {

        //...一些其他逻处理

        // 最重要的是这里

        // Swoole 服务器进程

        $server = tap(new Process([

                    (new PhpExecutableFinder)->find(),

                    ...config('octane.swoole.php_options', []),

                    config('octane.swoole.command', 'swoole-server'),

                    $serverStateFile->path(),

                ], realpath(__DIR__.'/../../bin'), [

                    'APP_ENV' => app()->environment(),

                    'APP_BASE_PATH' => base_path(),

                    'LARAVEL_OCTANE' => 1,

                ]))->start();



        return $this->runServer($server, $inspector, 'swoole');

    }

这段代码的目的是执行 bin 目录下的 swoole-server 脚本。 找到对应的 swoole-server 脚本 如下

// vendor/laravel/octane/bin/swoole-server

// worker 状态

// 这个对象是父进程的一个对象

// 因此,每次启动 worker 之后,里面的 workerstate 是不同的

$workerState = new WorkerState;



// 如果之前使用过 Swoole

// 这里会看到非常熟悉的 Swoole 几个回调

// 首先来看 workerstart

$server->on('workerstart', fn (Server $server, $workerId) =>

    (fn ($basePath) => (new OnWorkerStart(

        new SwooleExtension, $basePath, $serverState, $workerState

    ))($server, $workerId))($bootstrap($serverState))

);

在 Swoole 文档中有这样一句话:

此事件在 Worker 进程 / Task 进程 启动时发生,这里创建的对象可以在进程生命周期内使用。

首先要明确的是 workerstart 只会运行一次。在该回调中创建的对象是进程内的全局对象,只要 workerstart 未被终止,这个全局对象就会一直存在。

WorkerStart 处理如下

// vendor/laravel/octane/src/Swoole/Handlers/OnWorkerStart.php
 public function __invoke($server, int $workerId)

{

    $this->clearOpcodeCache();

    // 让进程保存 server 对象

    $this->workerState->server = $server;

    // 保存 worker ID

    $this->workerState->workerId = $workerId;

    // 保存父进程 ID

    $this->workerState->workerPid = posix_getpid();

    // workerState 保存 Worker 对象

    // 后续 Request 需要使用到

    $this->workerState->worker = $this->bootWorker($server);

    $this->dispatchServerTickTaskEverySecond($server);

    $this->streamRequestsToConsole($server);

    if ($this->shouldSetProcessName) {

        $isTaskWorker = $workerId >= $server->setting['worker_num'];

        $this->extension->setProcessName(

            $this->serverState['appName'],

            $isTaskWorker ? 'task worker process' : 'worker process',

        );
    }
}

// 查看一下 bootWorker

protected function bootWorker($server)
{
    // 继续找到 Worker 对象
    try {
        return tap(new Worker(

            new ApplicationFactory($this->basePath),

            $this->workerState->client = new SwooleClient

        ))->boot([

            'octane.cacheTable' => $this->workerState->cacheTable,

            Server::class => $server,

            WorkerState::class => $this->workerState,

        ]);

    } catch (Throwable $e) {

        Stream::shutdown($e);
        $server->shutdown();
    }
}

Worker 对象的 Boot 方法

// vendor/laravel/octane/src/Worker.php
public function boot(array $initialInstances = []): void
{
    // 这里最重要的就是这个容器实例,这个容器是框架 boot 后保存的初始化的容器。

    // 每次请求时将会 clone 这个容器实例

    // 所以后续请求处理中,如果改变容器中的对象,也不会影响下一个请求容器实例

    $this->app = $app = $this->appFactory->createApplication(

        array_merge(

            $initialInstances,

            [Client::class => $this->client],

        )
    );

    // 自定义处理的事件,如果需要对容器实例更改,可以使用这个事件

    // 但是注意更改的内容,将会在后续请求中复用

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

}

workerstart 的主要流程到这里就结束,最重要的一点是产生了一个 Laravel 初始化后的一个容器实例,可以在进程的后续请求中复用。

Laravel 主要用于 HTTP 服务器,因此在 Swoole 的回调事件中,onRequest 事件用于处理 HTTP 请求。


$server->on('request', function ($request, $response) use ($server, $workerState, $serverState) {

    $workerState->lastRequestTime = microtime(true);

    if ($workerState->timerTable) {

        $workerState->timerTable->set($workerState->workerId, [

            'worker_pid' => $workerState->workerPid,

            'time' => time(),

            'fd' => $request->fd,

        ]);

    }

    // 主要关注这里的 handle 处理
    // WorkerState 对象中保存了 worker 对象
    // $workerState->client->marshalRequest() 主要是将 Swoole Request 转换为 Illuminate\Http\Request
    // 因此,我们需要回到 worker 对象中

    $workerState->worker->handle(...$workerState->client->marshalRequest(new RequestContext([

        'swooleRequest' => $request,

        'swooleResponse' => $response,

        'publicPath' => $serverState['publicPath'],

        'octaneConfig' => $serverState['octaneConfig'],

    ])));

    if ($workerState->timerTable) {

        $workerState->timerTable->del($workerState->workerId);

    }

});

找到 Worker 对象中的 handle 方法。

// vendor/laravel/octane/src/Worker.php
public function handle(Request $request, RequestContext $context): void

{

    if ($this->client instanceof ServesStaticFiles &&

        $this->client->canServeRequestAsStaticFile($request, $context)) {

        $this->client->serveStaticFile($request, $context);
        return;

    }

    // 克隆 Worker 对象的 Laravel 初始化容器实例

    // 此后 Laravel 容器将是 sandbox 这个沙箱对象

    CurrentApplication::set($sandbox = clone $this->app);

    // 初始化 Gateway 对象

    $gateway = new ApplicationGateway($this->app, $sandbox);

    try {

        $responded = false;

        ob_start();
        
        // 处理每个请求的 handle 方法

        // 这里和正常的 Laravel 请求处理是一样的

        $response = $gateway->handle($request);

        $output = ob_get_contents();

        if (ob_get_level()) {

            ob_end_clean();

        }

        // 将 Swoole 的响应包装起来,发送给客户端

        $this->client->respond(

            $context,

            $octaneResponse = new OctaneResponse($response, $output),

        );

        $responded = true;

        $this->invokeRequestHandledCallbacks($request, $response, $sandbox);

        // 处理发送响应后的逻辑

        $gateway->terminate($request, $response);

    } catch (Throwable $e) {

        $this->handleWorkerError($e, $sandbox, $request, $context, $responded);

    } finally {

        $sandbox->flush();

        $this->app->make('view.engine.resolver')->forget('blade');

        $this->app->make('view.engine.resolver')->forget('php');

        // 在请求处理过程完成后,我们将取消一些变量的设置

        // 并将当前应用程序状态重置为克隆之前的原始状态

        // 然后我们将准备好进行下一个 worker 迭代循环

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

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

最后是 workerstop,这部分不太复杂。只需实现一个 WorkerStopping 事件处理后续逻辑。


$server->on('workerstop', function () use ($workerState) {

    if ($workerState->tickTimerId) {

        Timer::clear($workerState->tickTimerId);

    }

    $workerState->worker->terminate();

});

总结

我梳理了一下 Octane Swoole 模式的处理流程。最重要的是要了解容器实例的生命周期,对于使用 Octane 扩展程序至关重要。需要清楚地了解哪些对象是在 worker 进程中共享的,哪些对象需要在每次请求后清理。避免全局数据污染,这可能导致数据混乱。如有勘误,请指正

原文发布 Laravel octane 初识

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值