初识 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 进程中共享的,哪些对象需要在每次请求后清理。避免全局数据污染,这可能导致数据混乱。如有勘误,请指正