初识 Laravel Octane

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

安装

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

shell
composer require "laravel/octane"

选择使用 Server

shell
php artisan octane:install

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

启动

shell
php artisan octane:start --workers=5

流程

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

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() 方法。

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 相关的命令文件。

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 脚本 如下

php
// 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 处理如下

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 方法

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 请求。

php
$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 方法。

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 事件处理后续逻辑。

php
$server->on('workerstop', function () use ($workerState) {
    if ($workerState->tickTimerId) {
        Timer::clear($workerState->tickTimerId);
    }

    $workerState->worker->terminate();
});

总结

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

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接