Search by

easydowork / yii2s

easydowork

Swoole 6 coroutine accelerator for Yii2: resident-memory HTTP server, connection pools, coroutine-safe DB/Redis/Cache/Session components.

Package info

github.com/easydowork/yii2s

Type:yii2-extension

pkg:composer/easydowork/yii2s

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.1 2026-11-11 12:34 UTC

This package is auto-updated.

Last update: 2026-11-11 12:34:35 UTC


README

让 Yii2 应用运行在 Swoole 6 常驻内存协程 HTTP 服务器上:数据库/Redis 连接池化复用、 request / response / session / user 按协程隔离、优雅停机、守护化命令、内置压测器。 控制器可以继续用 Yii2 的写法。会话、缓存、队列、上传和 pretty URL 要按文末的约束配置, 不是把组件类换掉就结束。

{"app":"yii2s-example","engine":"swoole-coroutine","php":"8.4.14","swoole":"6.2.0","pid":757,"cid":5}

能力一览

领域 特性
核心 多进程 Swoole\Http\Server(worker_num = CPU 核数)、协程连接池、request/response/session/user/view/errorHandler 按协程隔离、优雅停机/热重载
性能 路由缓存、单飞锁(thundering herd 抑制)、raw 路由极简通道、内置压测器
观测 /metrics(Prometheus:RED + 池 + 进程 + 内核级 Server->stats() + 跨 worker 聚合 + 系统负载,可选 adminToken 门禁)、/__pool、swoole/status 运行时面板、生命周期事件(workerError/beforeShutdown/workerExit/workerStop)
韧性 熔断器、限流、重试、连接池健康机制(min-idle / max-lifetime / 空闲保活)、DB 断线自愈
运维 `swoole/start
调度 秒级定时任务(worker 0;多实例用 Redis 锁盖住整次执行)

基准(macOS + swoole-cli,12 worker,keep-alive,100 并发 5s):纯框架 24832 QPS(p50 2.94ms)/ 带 DB 查询 10294 QPS / 带 Redis 7766 QPS,连接池零超时零泄漏,详见 doc/DEVELOPMENT.md 的压测章节。

上生产前的工程硬化计划见 doc/ROADMAP.md。

环境要求

依赖 版本
PHP >= 8.4(实测 8.4 / 8.5)
ext-swoole ^6.0(Swoole Http Server + 连接池)
yiisoft/yii2 ^2.0
ext-redis 原生 phpredis,使用 redis/cache/session/queue 组件时必需(yii2-redis 是纯 PHP 客户端,不提供 \Redis)
MySQL / Redis 可选,按使用的组件而定

目标平台是 Linux(生产环境);macOS 可开发(pecl 编译安装即可)。 两种安装方式:标准 pecl install swoole,或 swoole-cli(自带全部扩展的独立二进制, 免配置,Linux 上开箱即用)。

安装

composer require easydowork/yii2s

发布 Packagist 前可通过 path 仓库引入:

"repositories": [
    {"type": "path", "url": "../yii2s", "options": {"symlink": true}}
]

5 分钟跑起来(example 目录)

# 1. 初始化演示数据库(建库建表插数据,需要本地 MySQL)
php example/init-db.php

# 2. 启动服务器
php example/yii swoole/start

# 3. 访问
curl http://127.0.0.1:9501/
curl http://127.0.0.1:9501/demo/db

架构一览

进程模型与请求流

flowchart LR
    C[客户端] -->|静态文件 + TLS| N[nginx]
    N -->|动态请求| M[master 主进程<br/>监听 / 信号 / 优雅停机]
    M --> MG[manager 进程<br/>worker 管理]
    MG --> W0[worker 0]
    MG --> W1[worker 1]
    MG --> WN[worker N-1]

    W0 & W1 & WN --> D[RequestDispatcher<br/>中间件链]
    D -->|Raw路由 → 静态文件 → /__pool → /metrics → Yii| A[常驻 CoroutineApplication]
    A --> DB[(MySQL 连接池)]
    A --> R[(Redis 连接池)]
    A --> S[CoroutineSession]
Loading

单请求生命周期

sequenceDiagram
    participant C as 客户端
    participant W as Swoole Worker
    participant MW as 中间件链
    participant Y as Yii2 Application
    participant P as 连接池

    C->>W: HTTP 请求
    W->>MW: dispatch()
    alt 命中 raw 路由 / 静态文件 / metrics 端点
        MW-->>C: 直接写回响应(不创建 Yii 对象)
    else 进入 Yii 生命周期
        MW->>Y: beginRequest() 重建 request/response/session/user
        Y->>Y: 路由 + 控制器 + 过滤器
        Y->>P: borrow() 借连接(协程粘性)
        P->>P: 空闲复用 / 新建 / 等待
        Y->>P: release() 归还
        MW->>MW: cleanup()(release + sweep 死协程 + 日志 flush)
        MW-->>C: 发送响应
    end
Loading

连接池工作流

flowchart TD
    B[borrow 借连接] --> I{空闲池有连接?}
    I -- 有 --> V{校验 + 未过期?}
    V -- 通过 --> T[take 借出<br/>记录 coroutine id]
    V -- 失效/过期 --> D[destroy 销毁]
    I -- 无 --> S{size 小于 maxActive?}
    S -- 是 --> CR[create 新建]
    CR --> T
    S -- 否 --> W[wait 等待<br/>waitTimeout 超时抛错]
    W -- 有归还 --> I
    T --> R[return 归还到空闲池]
    R --> K[健康机制<br/>min-idle / max-lifetime / 空闲保活]
    K --> SW[sweep 回收<br/>死协程的连接]
Loading

生产部署拓扑

flowchart LR
    User[用户] --> LB[nginx / 负载均衡]
    LB --> SW1[Swoole 实例 :9501]
    LB --> SW2[Swoole 实例 :9502]
    LB --> SWN[Swoole 实例 :950N]
    SW1 & SW2 & SWN --> DB[(MySQL 主从)]
    SW1 & SW2 & SWN --> RD[(Redis)]
    PR[Prometheus / Grafana] -.->|抓取 /metrics| SW1
    PR -.->|抓取 /metrics| SW2
    PR -.->|抓取 /metrics| SWN
Loading

设计原则:请求态(request、response、session、user、view、errorHandler)在调度层按协程隔离, DB/Redis 连接按协程粘住。CSRF、Basic Auth、事务、MULTI 不用改调用方式。组件替换对照和踩坑记录见 doc/DEVELOPMENT.md。

接入现有 Yii2 应用

方式一:一行接入(推荐)

在 bootstrap 里加一条,stock 的 request / response / db / redis 会换成协程实现,原有参数保留。 这不是业务零改动:还要满足文末的已知限制。session、cache、queue 没有可自动替换的 stock 类,必须写成协程类。

// config/web.php
return [
    'bootstrap' => [
        ['class' => \Yii2S\Swoole\Bootstrap::class],
    ],
    // 你已有的 request/response/db/redis 配置原样保留:
    // Bootstrap 会把 stock 类(yii\web\Request、yii\db\Connection、
    // yii\redis\Connection ...)自动替换为协程版,所有参数原封不动;
    // 没有显式配置 swooleHttpServer 时会注册一个默认服务器组件
    //(127.0.0.1:9501,dispatcher 指向 @app/config/web.php,documentRoot
    // 指向 @app/web —— 直连调试时 css/js/图片/字体也能服务;生产反代形态
    // 下静态请求由 nginx 拦截,此配置不参与)。
];
php yii swoole/start        # 就这么跑起来了

控制台命令 swoole/start 属于 console 应用,config/console.php 的 bootstrap 也需要同样注册一行(只改 web.php 会得到 Unknown command: swoole/start):

// config/console.php
'bootstrap' => ['log', ['class' => \Yii2S\Swoole\Bootstrap::class]],

注意:服务器进程会 require 这个 config 文件两次(控制台入口一次、请求分发一次), 配置里不要写带副作用的顶层代码。

方式二:显式配置(完全控制)

'request' => [
    'class' => \Yii2S\Swoole\Runtime\CoroutineRequest::class,
    'cookieValidationKey' => '...',
    'enableCsrfValidation' => true,       // CSRF 完整可用
],
'response' => [
    'class' => \Yii2S\Swoole\Runtime\CoroutineResponse::class,
],
'log' => [
    'targets' => [[
        'class' => \Yii2S\Swoole\Log\CoroutineFileTarget::class,  // 缓冲批量写
        'maxBufferMessages' => 100,
        'logFile' => '@app/runtime/logs/app.log',
    ]],
],
'db' => [
    'class' => \Yii2S\Swoole\Db\CoroutineDbConnection::class,
    'dsn' => 'mysql:host=127.0.0.1;dbname=app',
    'username' => '...', 'password' => '...',
    'poolMaxActive' => 20,            // 池容量:并发查询的硬上限
    'poolWaitTimeout' => 3.0,         // 借不到连接的等待秒数
    'poolMinIdle' => 2,               // 最小空闲水位(配合服务器定时 keeper)
    'poolMaxLifetime' => 300,         // 连接最长存活秒数(到期回收,0 关闭)
    'poolIdleTimeout' => 60,          // 空闲超时重校验(配合 enableHealthCheck)
    'enableHealthCheck' => true,      // 借出前 SELECT 1 校验
    'slowQueryThreshold' => 0.5,      // 慢查询日志阈值秒(0 关闭)
    'enableCircuitBreaker' => true,   // 熔断:连续连接失败后快速失败
    'circuitFailureThreshold' => 5,
    'circuitCooldown' => 30,
    'enableSchemaCache' => true,      // 表结构缓存:information_schema 只查一次
    'schemaCache' => 'cache',         // 指向一个协程 Redis 缓存组件
    'schemaCacheDuration' => 3600,    // 表结构缓存有效期秒
],
'redis' => [
    'class' => \Yii2S\Swoole\Redis\CoroutineRedisConnection::class,
    'hostname' => '127.0.0.1',
    'poolMaxActive' => 20,
    'enableCircuitBreaker' => true,
],
'urlManager' => [
    'class' => \Yii2S\Swoole\Runtime\CoroutineUrlManager::class,  // 路由缓存(规则多时省 CPU)
    'enablePrettyUrl' => true,
    'showScriptName' => false,
],
'swooleHttpServer' => [
    'class' => \Yii2S\Swoole\Server\HttpServer::class,
    'host' => '127.0.0.1',
    'port' => 9501,
    'pidFile' => '@app/runtime/swoole-server.pid',
    'documentRoot' => '@app/public',           // 可选:静态文件(开发环境)
    'prewarmConnections' => 2,                // 预热连接池,避免冷启动连接风暴
    'poolMaintenanceInterval' => 30.0,        // 连接池健康 keeper 间隔(0 关闭)
    // Swoole 内部日志(worker 崩溃/端口冲突/send 失败等)。默认:系统临时目录
    // yii2s-swoole-<uid>/swoole-<host>-<port>.log + 每日轮转;设为 false 则仅输出
    // stdout(容器采集场景);settings 里的 log_file/log_level 键优先级更高
    'logFile' => '@app/runtime/logs/swoole.log',
    'logLevel' => SWOOLE_LOG_WARNING,
    // 协程运行时 hook(默认 SWOOLE_HOOK_ALL,无需配置)。异步风格 Http\Server
    // 自身不会开启 hook——不开启则 PDO/mysqli/文件 IO 阻塞 worker 事件循环,
    // 连接池退化为串行查询(实测 PDO SLEEP(0.2)×20 并发:92 vs 19 QPS)。
    // 设为 null 可保持你自己通过 Coroutine::set() 配置的值。
    // 'hookFlags' => SWOOLE_HOOK_ALL,
    'dispatcher' => new \Yii2S\Swoole\Server\RequestDispatcher('@app/config/web.php', [
        'enablePoolMetricsEndpoint' => true,  // /__pool
        'enableMetricsEndpoint' => true,      // /metrics
        'enableMetricsAggregation' => true,   // /metrics 跨 worker 聚合(Redis)
    ]),
],

两种方式下,cache / session / queue 都需要显式协程类(无 stock 对应物可自动替换):

'cache' => [
    'class' => \Yii2S\Swoole\Cache\CoroutineRedisCache::class,
    'redis' => 'redis',               // getOrSet 自带单飞锁,冷缓存不惊群
],
'session' => ['class' => \Yii2S\Swoole\Session\CoroutineSession::class, 'redis' => 'redis'],
'queue' => [
    'class' => \Yii2S\Swoole\Queue\CoroutineRedisQueue::class,
    'redis' => 'redis',
    'channel' => 'default',
    'ttr' => 60,                  // handler 抛错后的重试延迟
    'visibilityTimeout' => 60,    // 执行中的租约;超时未 ack 视为崩溃并计一次失败
    'maxAttempts' => 3,           // 超过则进死信列表
    'handler' => function (array $payload): void {
        // 处理任务
    },
],

request / response / session / user / view / errorHandler 由框架在每个协程里重建, 原版 yii\web\User、yii\filters\auth\HttpBasicAuth 等直接可用。Redis 的 MULTI/PIPELINE、 DB 的事务也都原生支持(连接在协程内粘性借用)。

队列取出任务后放进 reserved 集合,处理成功才删除。handler 抛错会按 ttr 延迟重试, 超过 maxAttempts 进死信。进程在 handler 中途退出时,任务留在 reserved 里, 超过 visibilityTimeout 后由下一次 reserve() 按失败重投。处理时间不要超过这个租约。

生产推荐 settings 清单(按需加到 settings 键;每项的取舍见 doc/ROADMAP.md §6.3):

'settings' => [
    'max_request'          => 10000, // 泄漏兜底:worker 每 N 请求优雅重启(灰度期结合 RSS 曲线决定)
    'max_concurrency'      => 200,   // Server 并发请求上限(v5.0.0+ 才有这个键;超限行为以当前 Swoole 为准)
    'max_coroutine'        => 200,   // 每 worker 协程上限(与并发模型对齐)
    'open_tcp_nodelay'     => true,  // API 小响应降延迟
    'heartbeat_check_time' => 300,   // 清理半开连接
    // 'open_cpu_affinity' => true,   // 独立多核机可开(IO 密集可配合 cpu_affinity_ignore 留 CPU0)
],
// 同时:ulimit -n(nofile)要调大——Swoole 的 max_conn 默认跟随它。

WebSocket(同端口,默认关闭)

enableWebSocket=true 时服务器以 Swoole\WebSocket\Server 运行:同一端口 HTTP 照常工作,Upgrade: websocket 请求桥接到你实现的 Yii2S\Swoole\Websocket\WebsocketHandlerInterface——鉴权(session/token/JWT)、 允许的路径、消息格式、连接表全部由你定义,服务器只桥接事件:

'swooleHttpServer' => [
    'enableWebSocket' => true,
    'websocketHandler' => new App\Websocket\ChatHandler(),      // 实现接口
    'websocketBroadcaster' => new App\Websocket\RedisBroadcaster(), // 可选:跨 worker 广播
],
final class ChatHandler implements \Yii2S\Swoole\Websocket\WebsocketHandlerInterface
{
    public function handshake(\Swoole\Http\Request $request): bool
    {
        return str_starts_with($request->server['request_uri'] ?? '', '/ws'); // 路径白名单自定
    }

    public function open(\Swoole\WebSocket\Server $server, \Swoole\Http\Request $request, int $fd): void
    {
        // 鉴权点(握手阶段无法拒绝——Swoole 语义):失败时
        // $server->disconnect($fd, 4001, 'unauthorized')
    }

    public function message(\Swoole\WebSocket\Server $server, \Swoole\WebSocket\Frame $frame, int $fd): void
    {
        $server->push($fd, 'echo:' . $frame->data); // 协程内:池/DB/Redis 直接可用
    }

    public function close(\Swoole\WebSocket\Server $server, int $fd): void {}
}

协议细节:客户端 ping 帧自动回 pong;踢人用 $server->disconnect($fd, 4000, ...); 跨 worker 广播自己实现 transport(文档模式:Redis pub/sub,每个 worker 订阅协程)。

极简接口快速通道(raw 路由)

健康检查、指标、极简只读接口这类请求,可以注册 raw 路由:命中后不创建 Yii 的 request/response,直接写回 Swoole 响应:

'swooleHttpServer' => [
    'class' => \Yii2S\Swoole\Server\HttpServer::class,
    'documentRoot' => '@app/web',
    'dispatcher' => (new \Yii2S\Swoole\Server\RequestDispatcher(__DIR__ . '/web.php', [
        'enablePoolMetricsEndpoint' => true,
    ]))
        ->addRawRoute('GET', '/ping', static fn (): string => 'pong')
        ->addRawRoute('GET', '/health', static fn (): array => ['ok' => true]),
],
  • 返回值:string → text/plain;array → application/json;null → handler 自己 操作 Swoole response(签名 (Request $request, Response $response))。
  • 精确匹配 METHOD + path,query 不参与匹配;方法不匹配会回落到 Yii 路由。
  • 绕过 Yii:不经过路由、session、CSRF、用户认证。只放公开/无状态端点。
  • 微基准:raw 路由约 4 万 QPS,接近裸 Swoole;完整 Yii 生命周期约 2.3 万 QPS(100 并发 5s,见压测章节)。

控制台命令

php yii swoole/start [--host=] [--port=] [--watch]  # 启动(阻塞),写 pid 文件
php yii swoole/status                               # 运行时面板:worker 数/内存/协程数/连接数/请求
php yii swoole/routes                               # 列出 raw 路由 + urlManager 规则
php yii swoole/stop                                 # 优雅停机(在途请求排空后退出)
php yii swoole/reload                               # 优雅重载 worker(SIGUSR1:加载新代码,master 不变,在途请求不中断)
php yii swoole/check                                # 配置体检(Application 隔离、cookie key、prettyUrl、db/redis 探活)
php yii queue/serve [--limit=N]                     # 队列消费 worker(无限轮询)

status 输出示例:

Server is running: pid 84268, listening on 127.0.0.1:9601, started ... (1s ago)
  workers: 12, memory: 47.1 MB (process tree RSS)
  requests: 7 (errors 0) [single worker]
  coroutines: 1 [single worker]
  db: 0 active / 1 idle (created 1, timeouts 0, discarded 0)
  redis: 0 active / 0 idle (created 0, timeouts 0, discarded 0)

status 里的请求/协程/连接数是单 worker 快照(/metrics、/__pool 由随机 worker 处理);内存和 worker 数是全量(进程树 RSS)。要全量聚合用 /metrics(见下)。

开发热重载

php yii swoole/start --watch

修改 @app 下的 PHP 文件后,worker 自动重载并加载新代码(约 1 秒内完成, 在途请求不中断)。监视进程一直留着,后面的修改还会再重载。轮询 mtime、零外部依赖。 开发专用,生产不要开。

观测与运维

/metrics(Prometheus)

# 配置 dispatcher: enableMetricsEndpoint => true
curl http://127.0.0.1:9501/metrics

输出:请求量/状态码(yii2s_http_requests_total)、耗时直方图、池状态、进程内存/协程、 系统负载(yii2s_process_load_average,Linux),以及内核级计数(yii2s_server_connection_num、 abort_count、accept_count、coroutine_num 等,来自 Swoole\Server->stats(),不参与 worker 聚合)。启用 enableMetricsAggregation => true 后,指标跨 worker 聚合(Redis), yii2s_process_memory_bytes 带 worker= 标签。

公网部署请配置 adminToken:非空时 /metrics、/__pool 要求 Authorization: Bearer <token>(Prometheus authorization 原生支持), 缺失/错误一律 401 且无响应体。门禁由内置的 RequestDispatcher 在观测 端点中间件内强制执行;使用自定义 dispatcher 时不会自动获得此门禁(自定义 实现需自行处理访问控制)。详见 doc/DEVELOPMENT.md 的「观测端点访问控制」。

/__pool(连接池)

# 配置 dispatcher: enablePoolMetricsEndpoint => true
curl http://127.0.0.1:9501/__pool

长跑检漏 + 压测

swoole-cli scripts/bench.php --path=/demo/db --concurrency=100 --duration=5   # 协程压测器
swoole-cli scripts/soak.php --path=/demo/db --duration=60 --pid=<server pid>   # 长跑检漏:RSS/池漂移

韧性组件

组件 作用 配置
CircuitBreaker 依赖故障时快速失败,避免 500 连击 DB/Redis 的 enableCircuitBreaker 等
RateLimiter 控制器过滤器,默认按客户端 IP 做 Redis 固定窗口限流 behaviors() 里挂 RateLimiter,limit / window
Retry 指数退避重试 + 可重试异常白名单 (new Retry())->call($fn, $attempts, [Timeout::class])
连接池健康 min-idle 水位 / max-lifetime 回收 / 空闲保活 poolMinIdle/poolMaxLifetime/poolIdleTimeout
DB 断线自愈 连接丢失自动 discard,下个请求重建 内置于 CoroutineCommand
单飞锁 getOrSet 冷缓存并发 miss 只跑一次回调 内置于 CoroutineRedisCache
// 熔断
use Yii2S\Swoole\Resilience\CircuitBreaker;
$breaker = new CircuitBreaker(5, 30);           // 5 次失败打开,30s 冷却
$result  = $breaker->call(fn () => $api->fetch(), fn () => $fallback);

// 限流:挂到控制器上,按客户端 IP 计数,不要求登录。超出抛 429。
// allow($key, $limit, $window) 仍可按自己的 key 调用。
public function behaviors(): array
{
    return [
        'rateLimiter' => [
            'class' => \Yii2S\Swoole\Resilience\RateLimiter::class,
            'only' => ['api'],
            'limit' => 10,
            'window' => 10,
        ],
    ];
}

// 重试
use Yii2S\Swoole\Resilience\Retry;
$result = (new Retry())->call(fn () => $rpc->call(), 3, [TimeoutException::class]);

定时任务(crontab)

秒级定时任务(对标 Hyperf/crontab),@every Ns 或标准 5 字段 cron:

use Yii2S\Swoole\Scheduling\Crontab;
use Yii2S\Swoole\Scheduling\CrontabDispatcher;

$dispatcher = new CrontabDispatcher();
$dispatcher->add(new Crontab('清理过期会话', '@every 5m', fn () => Session::gc()));
$dispatcher->add(new Crontab('每日汇总', '0 2 * * *', fn () => Stats::aggregateDaily()));

// 每个 worker 都可以 tick。没有 Redis 时只有 worker 0 会执行。
// 传入 Redis 后,同一任务名一把锁,回调返回才释放;进程崩溃则等 lockSeconds。
\Swoole\Coroutine::create(fn () => $dispatcher->run());

@every 用当前时间对间隔取模,对齐的是时钟,不是“从进程启动起每隔 N 秒”。 beforeStart 在 master 调用 server->start() 之前触发,挂在那里的协程不会进入 worker。

生产部署(Linux)

标准架构:nginx 直接服务静态文件 + TLS,Swoole 只处理动态请求。

多核部署用 多端口 + nginx upstream(每个实例一个端口,各自独立的 pid 文件):

; supervisor: 4 个实例分别监听 9501-9504
[program:yii2s]
command=swoole-cli /app/yii swoole/start --host=127.0.0.1 --port=950%(process_num)02d
process_name=%(program_name)s_%(process_num)02d
numprocs=4
autorestart=true
stopsignal=TERM
stopwaitsecs=25                ; 必须大于 gracefulTimeout(10s),留出排空时间

多实例只推荐「多端口 + nginx upstream」这一种形态;真实客户端 IP(trustedProxies)、 超时、上传大小等生产清单见 doc/DEVELOPMENT.md 部署章节。

升级与回滚(演练验证的 runbook)

swoole/reload(SIGUSR1)只能更新应用层代码(worker 启动后 require 的文件); yii2s 扩展自身代码与 server settings 在 master fork 前加载,reload 不可达—— 框架升级/回滚必须完整重启(nginx upstream 滚动重启即零中断,已在压测中验证):

# 应用代码变更:零中断
php yii swoole/reload

# yii2s 版本升级/回滚:滚动重启(每实例 stop + start)
php yii swoole/stop && php yii swoole/start
# 回滚:git checkout <tag> && php yii swoole/stop && php yii swoole/start

OPcache 开启时 reload/重启加载新代码存在 ≤2s 的 opcode 窗口,零窗口方案见 doc/DEVELOPMENT.md 4.2「OPcache 与 reload」。

已知限制

  • 需要 urlManager.enablePrettyUrl = true(?r= 路由依赖 $_GET,协程不安全)

  • Http\Server 不使用 Swoole 原生 enable_static_handler,静态文件走 PHP 层 (documentRoot,开发用)或交给 nginx(生产推荐)

  • 不要在常驻进程中使用 $_SESSION/header()/setcookie() 等超全局/过程式 API, 一律走 Yii::$app->request/response/session

  • 自定义 Application 类必须继承 Yii2S\Swoole\Runtime\CoroutineApplication,否则 request/response/session/user/view/errorHandler 会在并发请求间共享

  • 子协程(Coroutine::create())不继承协程上下文:在子协程里访问 Yii::$app->request/session/user/view 会得到该请求之外的实例。并发子任务请先在 父协程取出所需数据再传入(DB/Redis 连接池本身不受影响)

  • 协程请求不把上传写进进程级 $_FILES,官方 UploadedFile 在并发下读不到文件,也不会读到别人的。 读和落盘都用 Yii2S\Swoole\Runtime\CoroutineUploadedFile。官方 saveAs() 还依赖 move_uploaded_file(),Swoole 写入的临时文件不在 PHP 上传登记表里:

    use Yii2S\Swoole\Runtime\CoroutineUploadedFile as UploadedFile;
    $file = UploadedFile::getInstance($model, 'avatar');
    $file?->saveAs('@app/web/uploads/' . $file->name);
  • CoroutineRedisQueue 不实现 yii\queue\Queue。执行中的任务超过 visibilityTimeout 会按失败重投,处理逻辑需要能重复执行

  • CrontabDispatcher 在本机只让 worker 0 执行。多实例传入 Redis 后,同一任务名持有一把锁,回调返回才释放,默认 60 秒后过期。@every 仍按时钟取模

开发与测试

目标平台 Linux;测试可在 Linux 与 macOS 上完整运行,全量用例全绿 (用例数随回归测试增长,以 composer test 的实际输出为准)。

开发命令会自动选择运行时:系统 php ≥ 8.4(带 swoole 扩展)优先,否则回落到 swoole-cli,两者都没有会直接报错;e2e.sh 可用 CLI=... 覆盖。

composer test    # PHPUnit(需要本地 MySQL/Redis)
composer static  # PHPStan level 5
composer cs      # php-cs-fixer(与官方 yii2 同规则)

# 进程级端到端(start/status/reload/SIGTERM)与基准:
bash scripts/e2e.sh
bash scripts/bench.sh

CI(GitHub Actions)

.github/workflows/ci.yml 在 push / PR 时自动跑五个 job:

Job 覆盖
phpunit PHP 8.4–8.5 矩阵 × 真实 MySQL 8 + Redis 7 × pecl swoole 6.2.x,全量用例 + pcov 覆盖率文本
e2e 进程级生命周期(scripts/e2e.sh:start / status / reload / SIGTERM 排空 / pid 清理)
smoke blog demo 端到端(起真实多进程服务器:MVC、错误页、REST API、session+CSRF、上传+静态、CSV 下载、队列、并发)
static PHPStan(--error-format=github,PR 上直接标注行号)
coding-standard php-cs-fixer

压测章节、组件替换对照表、踩坑记录见 doc/DEVELOPMENT.md; 组件清单与 API 参考见 doc/COMPONENTS.md; 质量硬化路线图见 doc/ROADMAP.md。

License

MIT