反馈对象:webman / webman-database / workerman
建议提交:GitHubwalkor/webman或webman-php/database,社区 https://wenda.workerman.net
【Bug】协程数据库连接池在 Crontab / 队列长驻进程中不会归还连接,导致心跳失效并出现 PDO errno=110
| 项 | 版本 |
|---|---|
| PHP | 8.2.x |
| 事件驱动 | Swoole(enable_coroutine + hook) |
| workerman/webman-framework | v2.2.4 |
| webman/database | v2.1.10 |
| workerman/coroutine | v1.1.6 |
| workerman/crontab | v1.0.7 |
| illuminate/database | v11.51.0 |
使用 webman/database 连接池时:
Context::destroy(),连接能 pool->put(),行为正常。Workerman\Crontab\Crontab 回调、Redis Queue / Streams 等长驻消费者执行完业务后,不会调用 Context::destroy()。因此 Db 连接被借出后一直占着 Context,出现:
Pool::checkConnections)只检查 Channel 内空闲连接,借出连接吃不到心跳 PDO::prepare(): Send of 714 bytes failed with errno=110 Connection timed out
或同类半开连接错误。
webman/database DatabaseManager::connection() 逻辑等价于:
$connection = static::$pools[$name]->get();
Context::set($key, $connection);
Context::onDestroy(function () use ($connection, $name) {
$connection && static::$pools[$name]->put($connection);
});
只有 Context::destroy() 时才会 put 回池。
workerman/webman-framework 的 App::send() 中有:
Context::destroy();
Crontab / 队列消费者没有等价调用。
Workerman\Coroutine\Pool::checkConnections() 只对 Channel 里的连接做 idle / heartbeat,正在被 Context 持有的连接不会被探测。
生命周期对比:
HTTP: 借连接 → 查库 → 响应 → Context::destroy → put 回池 → 心跳可覆盖
Crontab: 借连接 → 查库 → 回调结束(无 destroy)→ 连接一直占着 → 心跳管不到
队列: 借连接 → Job 消费 → 结束(无 destroy)→ 同上
文案:
PDO::prepare(): Send of N bytes failed with errno=110 Connection timed out
不在 DetectsLostConnections(Laravel 12+ 为 LostConnectionDetector)列表中。
官方列表有 SQLSTATE[HY000] [2002] Connection timed out、Error while sending 等,但没有裸的 errno=110 / Send of。
因此不会自动 reconnect 重试,错误直接抛给业务。
(即使升级到 Laravel master,截至反馈时仍未包含上述文案。)
webman/database 连接池(配置 pool.heartbeat_interval 等)。 Db:: / Model 查询。 Webman\Context::destroy()。 期望: 连接已归还并由心跳保活,或断线后自动重连重试。
实际: 连接仍占在 Context 中无心跳,随后出现 errno=110 / Connection timed out;Illuminate 也不将其识别为 lost connection。
队列同理:subscribe($queue, [$consumer, 'consume']) 消费结束后无 destroy。
任选或组合:
Context::destroy(),否则连接池无法归还。 Context::destroy() 的封装;或 workerman/crontab / webman 进程模型中,Timer 回调结束自动 destroy(需评估与嵌套协程兼容性)。 webman/database 层补充对 errno=110、Send of 的断线识别(或提供可扩展的 detector)。finally 中 Context::destroy()。 finally 中 Context::destroy()。 MySqlConnection:扩展 lost-connection 文案 + 空闲超时主动 reconnect(防止半开连接卡死)。这些能缓解,但属于业务补丁;希望官方在框架层补齐长驻进程的连接池生命周期。
| 组件 | 路径 / 说明 |
|---|---|
| 借还连接 | vendor/webman/database/src/DatabaseManager.php → connection() / Context::onDestroy |
| HTTP 销毁 Context | vendor/workerman/webman-framework/src/App.php → send() 内 Context::destroy() |
| 池心跳 | vendor/workerman/coroutine/src/Pool.php → checkConnections() |
| Crontab | vendor/workerman/crontab/src/Crontab.php → Timer 触发 callback,无 Context 处理 |
| 断线识别 | vendor/illuminate/database/DetectsLostConnections.php(或 12+ 的 LostConnectionDetector) |
| 项 | 内容 |
|---|---|
| 整理日期 | 2026-10-15 |
| 项目侧规避参考 | app/common/crontab/SafeCrontab.php、app/common/db/PoolContext.php、app/common/db/ResilientMySqlConnection.php |
Crontab 由定时器触发,每次回调在独立协程里执行。回调返回后协程退出,Context 销毁,数据库连接自动放回连接池,再由池里的空闲心跳保活。
如果在一个协程里写 while 循环且不退出或者很久才退出,连接会一直留在这个协程的 Context 中。这条连接当时还属于该协程,其它协程不能同时使用,所以连接池的心跳碰不到它,swoole/swow等也不允许跨协程使用数据库连接,因为不安全。云厂商或中间网络会清掉长时间没有报文的 TCP,下次查询就会在发送时出现 errno=110。
Illuminate 有断线重连,但当前文案列表不包含 Send of N bytes failed with errno=110 Connection timed out,所以这种失败不会重连。事务中的语句也不会自动重试。
请避免让一个协程长时间占着连接。长循环要在每轮任务结束时调用 Context::destroy(),把连接还回池中,并让 pool.heartbeat_interval 小于云上的空闲超时。
严格来说这个不是webman或者crontab问题, 普通 PHP CLI 常驻脚本如果一直握着同一个 PDO,也会遇到同样的超时。