easydowork / yii2s
Swoole 6 coroutine accelerator for Yii2: resident-memory HTTP server, connection pools, coroutine-safe DB/Redis/Cache/Session components.
Requires
- php: >=8.4
- ext-swoole: ^6.0
- yiisoft/yii2: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: 3.95
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^9.6
- swoole/ide-helper: ^6.0
- yidas/yii2-composer-bower-skip: ^2.0
- yiisoft/yii2-redis: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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>(Prometheusauthorization原生支持), 缺失/错误一律 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.md4.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