Serverless PHP 与 Bref:Lambda 运行时、事件驱动与 Laravel Octane

Serverless PHP 与 Bref 实战:Lambda 运行时与层(php-82 / php-82-fpm / php-82-console)、serverless.yml 部署配置、HTTP 事件与 API Gateway、SQS/S3/Cron 事件驱动、冷启动与预热优化、Laravel Octane on Lambda、DynamoDB 会话与 S3 存储、成本模型与可观测性。

引言

传统 PHP 的部署模型是「一台机器跑 N 个 FPM 进程」,低峰期大量 CPU 闲置、高峰期又要紧急扩容。Serverless 把这层抽象掉了:你只管交代码,Lambda 按请求拉起容器、按毫秒计费、闲时归零。Bref 就是 PHP 生态通往 Lambda 的那座桥——它提供了 PHP 运行时层、事件适配器和 Laravel 集成。本文讲清 Bref 的架构、部署、事件驱动、冷启动优化与 Octane 落地。

前置:部署运维、开发趋势与云原生、Laravel 内核。


目录


1. Serverless 与 PHP 的契合点

1.1 模型对比

维度PHP-FPM + NginxLambda + Bref
伸缩单位机器/容器请求(并发实例)
计费常驻小时费请求数 + GB-秒
空闲成本仍要付费归零
冷启动无(进程常驻)有(几十到几百 ms)
状态可写本地磁盘默认无状态、/tmp 临时

1.2 为什么 PHP 特别适合 Serverless

  • 共享无状态模型:PHP 天生「一次请求一次进程」,本就无跨请求状态,天然契合函数计算
  • 启动快:解释执行 + OPcache,比 JVM/CLR 冷启动更轻
  • 生态成熟:Laravel/Symfony 可直接跑,Composer 打包依赖
  • 按需付费:中小流量站点成本可能比常驻 ECS 低一个数量级

1.3 主要代价

冷启动、单次 15 分钟上限、/tmp 非持久、并发模型带来的连接数爆炸(每个容器都要连数据库)。这些正是后文要解决的。

记忆:PHP 天生无状态、启动轻,是 Serverless 的天然适配者;代价是冷启动、超时上限、/tmp 非持久与「每容器一条数据库连接」的并发压力。


2. Bref 架构与运行时

2.1 Bref 由三部分组成

组件作用
Serverless Framework 插件把 serverless.yml 翻译成 CloudFormation
Lambda Layers提供 PHP 二进制、扩展、FPM
事件适配器把 SQS/S3/Cron 等事件转成 PHP 可处理对象

2.2 三个核心运行时层

在 serverless.yml 里,runtime: php-82 表示用 PHP 8.2 的函数运行时,layers 再叠加 FPM 或 console 层:

层用途
php-82函数式(CLI 风格),适合事件处理
php-82-fpm内置 PHP-FPM,适合 HTTP
php-82-console命令行入口(artisan)

2.3 Handler 是什么

Lambda 的入口是「handler」——一个 文件.函数 或一个实现 Bref\Event\Handler 的类:

// 最简单的 handler:接收事件数组,返回数组
return function (array $event, Context $context): array {
    return ['statusCode' => 200, 'body' => 'hello'];
};

对 HTTP,Bref 直接用 public/index.php 作为 handler,由 FPM 层接管请求;对事件,则用 Bref\Event\Sqs\SqsHandler 这类类。

记忆:Bref = Serverless 插件 + PHP Lambda 层 + 事件适配器;三个层 php-82(函数)、php-82-fpm(HTTP)、php-82-console(CLI);handler 就是 Lambda 入口,HTTP 用 public/index.php,事件用 Bref\Event*Handler。


3. 部署配置与 serverless.yml

3.1 一个 Laravel 应用的最小配置

service: my-app

provider:
    name: aws
    region: us-east-1
    runtime: php-82-fpm
    environment:
        APP_ENV: production
        LOG_CHANNEL: stderr

plugins:
    - ./vendor/bref/bref

functions:
    web:
        handler: public/index.php
        timeout: 28            # API Gateway 上限 29 秒,留 1 秒余量
        layers:
            - ${bref:layer.php-82-fpm}
        events:
            - httpApi: '*'

    artisan:
        handler: artisan
        timeout: 120
        layers:
            - ${bref:layer.php-82}
            - ${bref:layer.php-82-console}
        events:
            - schedule:
                  rate: rate(1 minute)

3.2 关键点

  • plugins 指向 vendor/bref/bref,提供 ${bref:layer.*} 变量
  • 日志必须走 stderr(CloudWatch),不能写文件
  • httpApi 比 http(REST API)更便宜、更简单,多数场景首选
  • 环境变量用 ssm 或 Secrets Manager 注入,切勿提交 .env

3.3 部署与调试命令

composer require bref/bref
vendor/bin/bref init            # 生成 serverless.yml 骨架
serverless deploy               # 部署到 AWS
serverless invoke local -f web  # 本地模拟调用

记忆:serverless.yml 三件套——runtime 定 PHP 版本、layers 定运行时层、events 定触发器;日志走 stderr、密钥走 SSM,部署用 serverless deploy,本地用 invoke local。


4. HTTP 事件与 Lambda 函数

4.1 请求如何变成 PHP 请求

Lambda 收到 API Gateway 事件 → Bref 的 FPM 层启动 php-fpm → 把事件翻译成 CGI 环境变量 → 交给 public/index.php(Laravel 入口)→ 返回响应 → Bref 转成 API Gateway 响应格式。

4.2 路由与冷启动的关系

Lambda 的伸缩单位是「并发实例」:并发 100 就意味着最多 100 个容器同时冷启动。HTTP 场景要特别关注:

  • timeout: 28:API Gateway 29 秒硬上限
  • 请求体上限:HTTP API 10MB;大文件务必走 S3 直传(见文件上传专题)
  • WebSocket 不支持:实时通信用 API Gateway WebSocket 或 Pusher

4.3 自定义 HTTP 函数

// src/health.php
return function (array $event, Context $context): array {
    return [
        'statusCode' => 200,
        'body' => json_encode(['ok' => true, 'id' => $context->getAwsRequestId()]),
    ];
};

Context 提供 getAwsRequestId()、getRemainingTimeInMillis()——后者在长任务里用来判断「还剩多少时间」。

记忆:HTTP 路径 = API Gateway 事件 → Bref FPM 层 → public/index.php;牢记 29 秒超时、10MB 请求体、无 WebSocket;Context 给到 requestId 与剩余毫秒。


5. 事件驱动:队列、定时与存储

5.1 SQS 队列消费者

use Bref\Event\Sqs\SqsEvent;
use Bref\Event\Sqs\SqsHandler;

class ProcessOrder extends SqsHandler
{
    public function handleSqs(SqsEvent $event, Context $context): void
    {
        foreach ($event->getRecords() as $record) {
            $body = json_decode($record->getBody(), true);
            Order::find($body['id'])?->process();
        }
    }
}

Laravel 的队列也能跑在 Lambda 上:用 SQS 事件触发一个处理器,内部调用 Artisan::call('queue:work', ['--max-jobs' => 1, '--stop-when-empty' => true]),把一次 Lambda 调用当成一次 Worker 生命周期。

5.2 S3 事件与定时任务

use Bref\Event\S3\S3Event;
use Bref\Event\S3\S3Handler;

class Thumbnailer extends S3Handler          // S3 上传触发:生成缩略图
{
    public function handleS3(S3Event $event, Context $context): void
    {
        foreach ($event->getRecords() as $record) {
            Thumbnail::make($record->getObject()->getKey());
        }
    }
}

Cron 则由 events: - schedule: rate(1 hour) 声明,Bref 用 EventBridge 触发;注意 Lambda 的 Cron 是 UTC 时区,且没有「集群只跑一次」的保证(要用分布式锁兜底)。

5.3 事件处理器一览

事件源Bref 类
SQSBref\Event\Sqs\SqsHandler
S3Bref\Event\S3\S3Handler
SNSBref\Event\Sns\SnsHandler
KinesisBref\Event\Kinesis\KinesisHandler
DynamoDB StreamsBref\Event\DynamoDb\DynamoDbHandler

记忆:事件驱动 = 用 Bref\Event*Handler 承接 SQS/S3/SNS/Kinesis/DynamoDB 事件,Cron 用 schedule 事件;Laravel 队列可在 SQS 处理器里 queue:work --stop-when-empty 跑完即退。


6. 冷启动与预热优化

6.1 冷启动由什么组成

冷启动 = 容器创建(AWS 侧,约 100~400ms,不可控)+ PHP 启动与加载扩展 + 框架 bootstrap(Laravel 服务提供者注册,最耗时)+ OPcache 编译。真正能优化的是后三项,其中框架 bootstrap 通常是大头。

6.2 六条优化手段

手段效果
减少 Layer 体积少装无用扩展,缩短下载/解压
预加载 preloading预热 OPcache 类缓存
config:cache / route:cache跳过配置与路由解析
Provisioned Concurrency预留实例,冷启动归零(有成本)
Warmup 插件定时 ping 保持容器热
Octane 常驻一次 bootstrap 服务多次请求

6.3 一个预加载配置

在 php.ini 里设置 opcache.preload=/var/task/preload.php,preload.php 中 require 框架核心类,即可把它们提前编译进共享内存。

6.4 什么时候不必纠结冷启动

后台任务(SQS/Cron)对冷启动不敏感——几百毫秒淹没在任务本身耗时里;只有面向用户的同步 HTTP 才值得为冷启动买单。

记忆:冷启动 = 容器创建 + PHP 启动 + 框架 bootstrap + OPcache;优化靠减层、预加载、config/route 缓存、Provisioned Concurrency、Warmup 与 Octane;只有同步 HTTP 才值得优化它。


7. Laravel Octane on Lambda

7.1 为什么需要 Octane

默认模型下,每次请求都要完整 bootstrap 一次 Laravel(注册上百个服务提供者)。Octane 把应用常驻在容器里,一次 bootstrap 服务多次请求,冷启动与单请求延迟都显著下降。

7.2 接入步骤

composer require bref/laravel-bridge laravel/octane
php artisan vendor:publish --tag=serverless-config

然后把 web 函数的 handler 换成 Octane 入口:

functions:
    web:
        handler: Bref\LaravelBridge\Http\OctaneHandler
        runtime: php-82-fpm
        layers:
            - ${bref:layer.php-82-fpm}
        environment:
            BREF_LOOP_MAX: 100     # 单个容器最多处理 100 个请求后回收
        events:
            - httpApi: '*'

BREF_LOOP_MAX 是关键:容器不是永久常驻,处理若干请求后会被回收重建,避免「长跑容器」里的内存泄漏与状态累积。

7.3 常驻内存的三大陷阱

陷阱后果对策
静态属性/全局变量累积请求间状态串味不存请求态于静态变量
数据库连接失效空闲后被 RDS 断开连接池 + 重连 + 短 idle
内存只增不减容器 OOM监控 RSS,调小 BREF_LOOP_MAX

Octane 把「每请求重建」换成了「常驻复用」——性能红利与状态泄漏风险是同一枚硬币的两面。

记忆:Octane on Lambda = 应用常驻 + Bref\LaravelBridge\Http\OctaneHandler + BREF_LOOP_MAX 定期回收;收益是一次 bootstrap 服务多请求,风险是静态状态串味与连接失效。


8. 存储、会话与状态

8.1 无状态是铁律

Lambda 容器随时会被销毁重建,任何写在本地磁盘的东西都不可靠(/tmp 只保证单次调用内存在,最多 512MB~10GB 可配)。

8.2 各类状态的落点

状态方案
文件上传/静态资源S3(Flysystem 适配器)
会话 SessionDynamoDB 或 Cookie 驱动
缓存ElastiCache / DynamoDB
队列SQS
大文件临时处理EFS 挂载 或 /tmp

8.3 Laravel 侧配置

# .env —— 所有状态外置
FILESYSTEM_DISK=s3
SESSION_DRIVER=dynamodb
CACHE_STORE=dynamodb
QUEUE_CONNECTION=sqs

会话放 DynamoDB 而非本地文件,是因为同一个用户的两次请求可能落到不同容器上——没有共享存储,会话就会「丢」。

记忆:Lambda 无状态——文件进 S3、会话进 DynamoDB、缓存进 ElastiCache/DynamoDB、队列用 SQS;本地磁盘只有 /tmp 且随容器销毁,绝不能当持久存储。


9. 成本、可观测与本地开发

9.1 成本模型

Lambda 计费 = 请求次数(每百万约 0.2 美元)+ GB-秒(内存 × 时长)。要点:

  • 内存调大 → 单次更快但单价更高,存在最优值(用 Lambda Power Tuning 找)
  • 低流量场景比常驻实例便宜得多;高流量恒定负载则可能更贵
  • API Gateway、CloudWatch Logs、NAT 网关常是「隐藏成本大户」

9.2 可观测性

关注点工具
日志CloudWatch Logs(stdout/stderr)
指标CloudWatch Metrics(Duration/Errors/Throttles)
链路AWS X-Ray 或 OpenTelemetry
告警CloudWatch Alarms + SNS

日志务必输出 JSON 结构化日志,否则 CloudWatch 里根本没法按 requestId 检索。

9.3 本地开发

vendor/bin/bref local                         # 用 Docker 起本地 PHP 环境
serverless invoke local -f web --path event.json
serverless offline                            # 模拟 API Gateway

原则:本地开发用 Docker 复刻 Lambda 环境,不要指望 php -S 能还原运行时差异。

记忆:成本 = 请求数 + GB-秒,内存有最优值;可观测靠 CloudWatch(日志走 stderr 且结构化)+ X-Ray;本地用 bref local / serverless offline 复刻运行时。


10. 速查表与一句话记忆

需求做法
HTTP 应用runtime php-82-fpm + handler public/index.php
事件函数runtime php-82 + Bref\Event*Handler
定时任务events schedule rate(1 hour)
队列消费SQS 事件 + queue:work –stop-when-empty
冷启动预加载 + config:cache + Octane
Octane 入口Bref\LaravelBridge\Http\OctaneHandler
会话DynamoDB 驱动
文件S3(Flysystem)
日志stderr + JSON
本地bref local / serverless offline

一句话记忆:Serverless PHP = Lambda 按请求伸缩 + Bref 提供运行时层(php-82 / php-82-fpm / php-82-console)与事件适配器;HTTP 走 public/index.php、事件走 Bref\Event*Handler、Cron 走 schedule;冷启动靠减层 + 预加载 + config 缓存 + Octane 常驻(BREF_LOOP_MAX 定期回收);状态一律外置——文件进 S3、会话进 DynamoDB、队列进 SQS。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 多租户 SaaS 架构:隔离策略、数据作用域与按租户计费
  2. PHP 的 CQRS 与事件溯源:命令总线、事件存储与投影
  3. PHP 支付集成实战:Stripe、支付宝与微信支付的状态机与回调