跳转至内容

Laravel Octane

简介

Laravel Octane 通过使用高性能应用服务器(包括 FrankenPHPOpen SwooleSwooleRoadRunner)来提升您的应用性能。Octane 只需启动应用一次,将其保留在内存中,然后以超音速处理请求。

安装

可通过 Composer 包管理器安装 Octane

1composer require laravel/octane

安装 Octane 后,您可以执行 octane:install Artisan 命令,将 Octane 的配置文件安装到您的应用中

1php artisan octane:install

服务器先决条件

FrankenPHP

FrankenPHP 是一个用 Go 编写的 PHP 应用服务器,支持现代 Web 特性,如早期提示 (Early Hints)、Brotli 和 Zstandard 压缩。当您安装 Octane 并选择 FrankenPHP 作为服务器时,Octane 会自动为您下载并安装 FrankenPHP 二进制文件。

通过 Laravel Sail 使用 FrankenPHP

如果您计划使用 Laravel Sail 开发应用,请运行以下命令安装 Octane 和 FrankenPHP

1./vendor/bin/sail up
2 
3./vendor/bin/sail composer require laravel/octane

接下来,您应该使用 octane:install Artisan 命令安装 FrankenPHP 二进制文件

1./vendor/bin/sail artisan octane:install --server=frankenphp

最后,将 SUPERVISOR_PHP_COMMAND 环境变量添加到应用 docker-compose.yml 文件中的 laravel.test 服务定义中。该环境变量将包含 Sail 使用 Octane 而不是 PHP 开发服务器来运行应用所需的命令

1services:
2 laravel.test:
3 environment:
4 SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=frankenphp --host=0.0.0.0 --admin-port=2019 --port='${APP_PORT:-80}'"
5 XDG_CONFIG_HOME: /var/www/html/config
6 XDG_DATA_HOME: /var/www/html/data

要启用 HTTPS、HTTP/2 和 HTTP/3,请改为应用这些修改

1services:
2 laravel.test:
3 ports:
4 - '${APP_PORT:-80}:80'
5 - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
6 - '443:443'
7 - '443:443/udp'
8 environment:
9 SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --host=localhost --port=443 --admin-port=2019 --https"
10 XDG_CONFIG_HOME: /var/www/html/config
11 XDG_DATA_HOME: /var/www/html/data

通常,您应该通过 https:// 访问您的 FrankenPHP Sail 应用,因为使用 https://127.0.0.1 需要额外配置且不被推荐

通过 Docker 使用 FrankenPHP

使用 FrankenPHP 的官方 Docker 镜像可以提供更好的性能,并使用 FrankenPHP 静态安装中未包含的额外扩展。此外,官方 Docker 镜像支持在 FrankenPHP 本身不支持的平台(如 Windows)上运行。FrankenPHP 的官方 Docker 镜像适用于本地开发和生产环境。

您可以将以下 Dockerfile 作为容器化您的 FrankenPHP Laravel 应用的起点

1FROM dunglas/frankenphp
2 
3RUN install-php-extensions \
4 pcntl
5 # Add other PHP extensions here...
6 
7COPY . /app
8 
9ENTRYPOINT ["php", "artisan", "octane:frankenphp"]

然后,在开发过程中,您可以使用以下 Docker Compose 文件来运行您的应用

1# compose.yaml
2services:
3 frankenphp:
4 build:
5 context: .
6 entrypoint: php artisan octane:frankenphp --workers=1 --max-requests=1
7 ports:
8 - "8000:8000"
9 volumes:
10 - .:/app

如果将 --log-level 选项显式传递给 php artisan octane:start 命令,Octane 将使用 FrankenPHP 的原生日志记录器,并且除非进行特殊配置,否则将生成结构化的 JSON 日志。

您可以查阅官方 FrankenPHP 文档以获取有关在 Docker 中运行 FrankenPHP 的更多信息。

自定义 Caddyfile 配置

使用 FrankenPHP 时,您可以在启动 Octane 时通过 --caddyfile 选项指定自定义的 Caddyfile

1php artisan octane:start --server=frankenphp --caddyfile=/path/to/your/Caddyfile

这允许您在默认设置之外自定义 FrankenPHP 的配置,例如添加自定义中间件、配置高级路由或设置自定义指令。您可以查阅 Caddy 官方文档以了解更多有关 Caddyfile 语法和配置选项的信息。

RoadRunner

RoadRunner 由基于 Go 构建的 RoadRunner 二进制文件驱动。首次启动基于 RoadRunner 的 Octane 服务器时,Octane 将询问是否为您下载并安装 RoadRunner 二进制文件。

通过 Laravel Sail 使用 RoadRunner

如果您计划使用 Laravel Sail 开发应用,请运行以下命令安装 Octane 和 RoadRunner

1./vendor/bin/sail up
2 
3./vendor/bin/sail composer require laravel/octane spiral/roadrunner-cli spiral/roadrunner-http

接下来,启动 Sail shell 并使用 rr 可执行文件获取最新的 Linux 版 RoadRunner 二进制文件

1./vendor/bin/sail shell
2 
3# Within the Sail shell...
4./vendor/bin/rr get-binary

然后,将 SUPERVISOR_PHP_COMMAND 环境变量添加到应用 docker-compose.yml 文件中的 laravel.test 服务定义中。该环境变量将包含 Sail 使用 Octane 而不是 PHP 开发服务器来运行应用所需的命令

1services:
2 laravel.test:
3 environment:
4 SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=roadrunner --host=0.0.0.0 --rpc-port=6001 --port='${APP_PORT:-80}'"

最后,确保 rr 二进制文件具有可执行权限并构建您的 Sail 镜像

1chmod +x ./rr
2 
3./vendor/bin/sail build --no-cache

Swoole

如果您计划使用 Swoole 应用服务器来运行 Laravel Octane 应用,则必须安装 Swoole PHP 扩展。通常可以通过 PECL 安装

1pecl install swoole

Open Swoole

如果您想使用 Open Swoole 应用服务器来运行 Laravel Octane 应用,则必须安装 Open Swoole PHP 扩展。通常可以通过 PECL 安装

1pecl install openswoole

在 Open Swoole 上使用 Laravel Octane 提供了与 Swoole 相同的功能,例如并发任务、Ticks 和间隔。

通过 Laravel Sail 使用 Swoole

在通过 Sail 运行 Octane 应用之前,请确保拥有最新版本的 Laravel Sail,并在应用根目录下执行 ./vendor/bin/sail build --no-cache

或者,您可以使用官方的基于 Docker 的 Laravel 开发环境 Laravel Sail 来开发基于 Swoole 的 Octane 应用。Laravel Sail 默认包含 Swoole 扩展。但是,您仍然需要调整 Sail 使用的 docker-compose.yml 文件。

首先,将 SUPERVISOR_PHP_COMMAND 环境变量添加到应用 docker-compose.yml 文件中的 laravel.test 服务定义中。该环境变量将包含 Sail 使用 Octane 而不是 PHP 开发服务器来运行应用所需的命令

1services:
2 laravel.test:
3 environment:
4 SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port='${APP_PORT:-80}'"

最后,构建您的 Sail 镜像

1./vendor/bin/sail build --no-cache

Swoole 配置

Swoole 支持一些额外的配置选项,如有必要,您可以将它们添加到 octane 配置文件中。由于它们很少需要修改,因此这些选项未包含在默认配置文件中

1'swoole' => [
2 'options' => [
3 'log_file' => storage_path('logs/swoole_http.log'),
4 'package_max_length' => 10 * 1024 * 1024,
5 ],
6],

运行您的应用

Octane 服务器可以通过 octane:start Artisan 命令启动。默认情况下,此命令将使用应用 octane 配置文件中 server 选项指定的服务器

1php artisan octane:start

默认情况下,Octane 将在 8000 端口启动服务器,因此您可以通过 https://:8000 在浏览器中访问您的应用。

在生产环境中保持 Octane 运行

如果您将 Octane 应用部署到生产环境,应使用 Supervisor 等进程监控工具来确保 Octane 服务器保持运行。一个简单的 Octane Supervisor 配置文件示例如下

1[program:octane]
2process_name=%(program_name)s_%(process_num)02d
3command=php /home/forge/example.com/artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000
4autostart=true
5autorestart=true
6user=forge
7redirect_stderr=true
8stdout_logfile=/home/forge/example.com/storage/logs/octane.log
9stopwaitsecs=3600

通过 HTTPS 运行您的应用

默认情况下,通过 Octane 运行的应用生成的链接前缀为 http://。当通过 HTTPS 运行应用时,可以在应用的 config/octane.php 配置文件中将 OCTANE_HTTPS 环境变量设置为 true。当此配置值设为 true 时,Octane 将指示 Laravel 为所有生成的链接添加 https:// 前缀

1'https' => env('OCTANE_HTTPS', false),

通过 Nginx 运行您的应用

如果您还没有准备好自行管理服务器配置,或者对配置运行强大的 Laravel Octane 应用所需的各种服务不熟悉,请查看 Laravel Cloud,它提供完全托管的 Laravel Octane 支持。

在生产环境中,您应该在传统的 Web 服务器(如 Nginx 或 Apache)之后运行 Octane 应用。这样 Web 服务器可以处理静态资源(如图片和样式表),并管理 SSL 证书终止。

在下面的 Nginx 配置示例中,Nginx 将负责处理站点的静态资源,并将请求代理到运行在 8000 端口的 Octane 服务器

1map $http_upgrade $connection_upgrade {
2 default upgrade;
3 '' close;
4}
5 
6server {
7 listen 80;
8 listen [::]:80;
9 server_name domain.com;
10 server_tokens off;
11 root /home/forge/domain.com/public;
12 
13 index index.php;
14 
15 charset utf-8;
16 
17 location /index.php {
18 try_files /not_exists @octane;
19 }
20 
21 location / {
22 try_files $uri $uri/ @octane;
23 }
24 
25 location = /favicon.ico { access_log off; log_not_found off; }
26 location = /robots.txt { access_log off; log_not_found off; }
27 
28 access_log off;
29 error_log /var/log/nginx/domain.com-error.log error;
30 
31 error_page 404 /index.php;
32 
33 location @octane {
34 set $suffix "";
35 
36 if ($uri = /index.php) {
37 set $suffix ?$query_string;
38 }
39 
40 proxy_http_version 1.1;
41 proxy_set_header Host $http_host;
42 proxy_set_header Scheme $scheme;
43 proxy_set_header SERVER_PORT $server_port;
44 proxy_set_header REMOTE_ADDR $remote_addr;
45 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
46 proxy_set_header Upgrade $http_upgrade;
47 proxy_set_header Connection $connection_upgrade;
48 
49 proxy_pass http://127.0.0.1:8000$suffix;
50 }
51}

监控文件变更

由于应用在 Octane 服务器启动时一次性加载到内存中,因此任何文件更改在刷新浏览器时都不会生效。例如,添加到 routes/web.php 文件中的路由定义在服务器重启之前不会生效。为了方便起见,您可以使用 --watch 标志指示 Octane 在任何文件更改时自动重启服务器

1php artisan octane:start --watch

在使用此功能之前,请确保本地开发环境中已安装 Node。此外,您还应该在项目中安装 Chokidar 文件监控库

1npm install --save-dev chokidar

您可以在应用的 config/octane.php 配置文件中使用 watch 配置选项来设置需要监控的目录和文件。

指定 Worker 数量

默认情况下,Octane 将为机器提供的每个 CPU 核心启动一个应用请求 Worker。这些 Worker 将用于处理进入应用传入的 HTTP 请求。您可以在调用 octane:start 命令时使用 --workers 选项手动指定要启动的 Worker 数量

1php artisan octane:start --workers=4

如果您使用 Swoole 应用服务器,还可以指定要启动多少个 “任务 Worker”

1php artisan octane:start --workers=4 --task-workers=6

指定最大请求数

为了防止内存泄漏,Octane 会在 Worker 处理 500 个请求后优雅地重启它。要调整此数字,可以使用 --max-requests 选项

1php artisan octane:start --max-requests=250

指定最大执行时间

默认情况下,Laravel Octane 通过应用 config/octane.php 配置文件中的 max_execution_time 选项为传入请求设置 30 秒的最大执行时间

1'max_execution_time' => 30,

此设置定义了传入请求在终止前允许执行的最大秒数。将此值设置为 0 将完全禁用执行时间限制。此配置选项对于处理长时间运行的请求(例如文件上传、数据处理或对外部服务的 API 调用)的应用特别有用。

当您修改 max_execution_time 配置时,必须重启 Octane 服务器以使更改生效。

重载 Workers

您可以使用 octane:reload 命令优雅地重启 Octane 服务器的 Worker。通常,这应该在部署后执行,以便将新部署的代码加载到内存中,并用于处理后续请求

1php artisan octane:reload

停止服务器

您可以使用 octane:stop Artisan 命令停止 Octane 服务器

1php artisan octane:stop

检查服务器状态

您可以使用 octane:status Artisan 命令检查 Octane 服务器的当前状态

1php artisan octane:status

依赖注入与 Octane

由于 Octane 在启动时加载应用一次并将其保持在内存中,在构建应用时需要考虑一些注意事项。例如,应用服务提供者的 registerboot 方法仅在请求 Worker 最初启动时执行一次。在随后的请求中,将重复使用同一个应用实例。

因此,在向任何对象的构造函数注入应用服务容器或请求时,应格外小心。这样做可能会导致该对象在后续请求中持有过时的容器或请求版本。

Octane 会自动处理在请求之间重置任何第一方框架状态。但是,Octane 并不总是知道如何重置由您的应用创建的全局状态。因此,您应该了解如何以 Octane 友好的方式构建应用。下面我们将讨论在使用 Octane 时可能导致问题的最常见情况。

容器注入

通常,您应该避免将应用服务容器或 HTTP 请求实例注入到其他对象的构造函数中。例如,以下绑定将整个应用服务容器注入到绑定为单例的对象中

1use App\Service;
2use Illuminate\Contracts\Foundation\Application;
3 
4/**
5 * Register any application services.
6 */
7public function register(): void
8{
9 $this->app->singleton(Service::class, function (Application $app) {
10 return new Service($app);
11 });
12}

在此示例中,如果 Service 实例在应用启动过程中被解析,容器将被注入到该服务中,并且在后续请求中,Service 实例将持有同一个容器。对于您的特定应用,这可能不是问题;但是,它可能导致容器意外丢失后续请求添加的绑定。

作为变通方法,您可以停止将该绑定注册为单例,或者向服务中注入一个始终解析当前容器实例的容器解析器闭包

1use App\Service;
2use Illuminate\Container\Container;
3use Illuminate\Contracts\Foundation\Application;
4 
5$this->app->bind(Service::class, function (Application $app) {
6 return new Service($app);
7});
8 
9$this->app->singleton(Service::class, function () {
10 return new Service(fn () => Container::getInstance());
11});

全局 app 辅助函数和 Container::getInstance() 方法将始终返回应用容器的最新版本。

请求注入

通常,您应该避免将应用服务容器或 HTTP 请求实例注入到其他对象的构造函数中。例如,以下绑定将整个请求实例注入到绑定为单例的对象中

1use App\Service;
2use Illuminate\Contracts\Foundation\Application;
3 
4/**
5 * Register any application services.
6 */
7public function register(): void
8{
9 $this->app->singleton(Service::class, function (Application $app) {
10 return new Service($app['request']);
11 });
12}

在此示例中,如果 Service 实例在应用启动过程中被解析,HTTP 请求将被注入到服务中,并且在后续请求中,Service 实例将持有同一个请求。因此,所有的标头、输入、查询字符串数据以及所有其他请求数据都将是不正确的。

作为变通方法,您可以停止将该绑定注册为单例,或者向服务中注入一个始终解析当前请求实例的请求解析器闭包。或者,最推荐的方法是仅在运行时将对象所需的特定请求信息传递给对象的方法之一

1use App\Service;
2use Illuminate\Contracts\Foundation\Application;
3 
4$this->app->bind(Service::class, function (Application $app) {
5 return new Service($app['request']);
6});
7 
8$this->app->singleton(Service::class, function (Application $app) {
9 return new Service(fn () => $app['request']);
10});
11 
12// Or...
13 
14$service->method($request->input('name'));

全局 request 辅助函数将始终返回应用当前正在处理的请求,因此在应用中使用是安全的。

在控制器方法和路由闭包上对 Illuminate\Http\Request 实例进行类型提示是可以接受的。

配置仓库注入

通常,您应该避免将配置仓库实例注入到其他对象的构造函数中。例如,以下绑定将配置仓库注入到绑定为单例的对象中

1use App\Service;
2use Illuminate\Contracts\Foundation\Application;
3 
4/**
5 * Register any application services.
6 */
7public function register(): void
8{
9 $this->app->singleton(Service::class, function (Application $app) {
10 return new Service($app->make('config'));
11 });
12}

在此示例中,如果配置值在请求之间发生更改,该服务将无法访问新值,因为它依赖于原始仓库实例。

作为变通方法,您可以停止将该绑定注册为单例,或者向类中注入配置仓库解析器闭包

1use App\Service;
2use Illuminate\Container\Container;
3use Illuminate\Contracts\Foundation\Application;
4 
5$this->app->bind(Service::class, function (Application $app) {
6 return new Service($app->make('config'));
7});
8 
9$this->app->singleton(Service::class, function () {
10 return new Service(fn () => Container::getInstance()->make('config'));
11});

全局 config 辅助函数将始终返回配置仓库的最新版本,因此在应用中使用是安全的。

管理内存泄漏

请记住,Octane 在请求之间将应用保留在内存中;因此,向静态维护的数组添加数据将导致内存泄漏。例如,以下控制器存在内存泄漏,因为每次对应用的请求都会继续向静态的 $data 数组添加数据

1use App\Service;
2use Illuminate\Http\Request;
3use Illuminate\Support\Str;
4 
5/**
6 * Handle an incoming request.
7 */
8public function index(Request $request): array
9{
10 Service::$data[] = Str::random(10);
11 
12 return [
13 // ...
14 ];
15}

在构建应用时,应特别小心,避免创建此类内存泄漏。建议您在本地开发期间监控应用的内存使用情况,以确保没有引入新的内存泄漏。

并发任务

此功能需要 Swoole

使用 Swoole 时,您可以执行轻量级后台任务以实现并发操作。您可以使用 Octane 的 concurrently 方法来完成此操作。您可以将此方法与 PHP 数组解构结合使用,以检索每个操作的结果

1use App\Models\User;
2use App\Models\Server;
3use Laravel\Octane\Facades\Octane;
4 
5[$users, $servers] = Octane::concurrently([
6 fn () => User::all(),
7 fn () => Server::all(),
8]);

由 Octane 处理的并发任务利用 Swoole 的“任务 Worker”,并在与传入请求完全不同的进程中执行。可用于处理并发任务的 Worker 数量由 octane:start 命令上的 --task-workers 指令决定

1php artisan octane:start --workers=4 --task-workers=6

调用 concurrently 方法时,由于 Swoole 任务系统的限制,您不应提供超过 1024 个任务。

Ticks 与间隔

此功能需要 Swoole

使用 Swoole 时,您可以注册每隔指定秒数执行一次的“Tick”操作。您可以通过 tick 方法注册“Tick”回调。提供给 tick 方法的第一个参数应该是表示 Tick 名称的字符串。第二个参数应该是将在指定间隔调用的可调用对象。

在此示例中,我们将注册一个每 10 秒调用一次的闭包。通常,tick 方法应在应用的服务提供者的 boot 方法中调用

1Octane::tick('simple-ticker', fn () => ray('Ticking...'))
2 ->seconds(10);

使用 immediate 方法,您可以指示 Octane 在 Octane 服务器最初启动时立即调用 Tick 回调,此后每隔 N 秒调用一次

1Octane::tick('simple-ticker', fn () => ray('Ticking...'))
2 ->seconds(10)
3 ->immediate();

Octane 缓存

此功能需要 Swoole

使用 Swoole 时,您可以利用 Octane 缓存驱动,它提供高达每秒 200 万次操作的读写速度。因此,该缓存驱动对于需要缓存层具有极致读/写速度的应用来说是一个绝佳选择。

此缓存驱动由 Swoole 数据表驱动。存储在缓存中的所有数据对服务器上的所有 Worker 均可见。但是,缓存数据将在服务器重启时被清空

1Cache::store('octane')->put('framework', 'Laravel', 30);

Octane 缓存中允许的最大条目数可以在应用的 octane 配置文件中定义。

缓存间隔

除了 Laravel 缓存系统提供的常规方法外,Octane 缓存驱动还具有基于间隔的缓存。这些缓存会在指定间隔自动刷新,并应在应用的服务提供者的 boot 方法中注册。例如,以下缓存将每五秒刷新一次

1use Illuminate\Support\Str;
2 
3Cache::store('octane')->interval('random', function () {
4 return Str::random(10);
5}, seconds: 5);

数据表 (Tables)

此功能需要 Swoole

使用 Swoole 时,您可以定义并与自己的任意 Swoole 数据表进行交互。Swoole 数据表提供极高的吞吐性能,并且这些表中的数据可以被服务器上的所有 Worker 访问。但是,其中的数据将在服务器重启时丢失。

表应在应用 octane 配置文件的 tables 配置数组中定义。一个允许最大 1000 行的示例表已为您预先配置。字符串列的最大大小可以通过在列类型后指定列大小来配置,如下所示

1'tables' => [
2 'example:1000' => [
3 'name' => 'string:1000',
4 'votes' => 'int',
5 ],
6],

要访问一个表,可以使用 Octane::table 方法

1use Laravel\Octane\Facades\Octane;
2 
3Octane::table('example')->set('uuid', [
4 'name' => 'Nuno Maduro',
5 'votes' => 1000,
6]);
7 
8return Octane::table('example')->get('uuid');

Swoole 数据表支持的列类型有:stringintfloat