关键词:Swoole、mysqli、MySQL、errno=110、send of 5 bytes failed、连接复用、死连接
在 PHP-FPM 时代,mysqli 几乎不会暴露什么“连接管理问题”。
但一旦进入 Swoole / 常驻进程 / RPC 服务,很多团队会突然遇到类似这样的错误:
send of 5 bytes failed with errno=110 Connection timed out而且往往具备以下特征:
- ❌ 不是高并发
- ❌ 不是慢 SQL
- ❌ 不是攻击
- ✅ 每天稳定、零散出现
- ✅ 重试一次就恢复
本文结合真实生产案例,讲清楚 为什么会出现、为什么“你明明重连了还有报错”、以及如何正确处理。
一、错误现象解析
典型错误日志:
Links\Db::getMysql(): send of 5 bytes failed with errno=110 Connection timed out关键信息拆解
- errno=110
Linux 网络错误码:ETIMEDOUT,TCP 发送超时 send of 5 bytes
MySQL 协议层的极小包:- ping
- handshake
- 协议头
👉 说明问题发生在“连接层”,而不是 SQL 执行阶段
二、为什么在 Swoole 中特别容易出现?
1️⃣ Swoole 是常驻进程
- Worker 生命周期:小时 / 天
- MySQL 连接生命周期:分钟级(
wait_timeout、NAT、SLB)
生命周期不匹配是根因。
2️⃣ mysqli 会复用“已经死掉的连接”
mysqli 的行为是:
- PHP 对象还在
- TCP 连接可能已经被回收
- mysqli 不会主动告诉你“我已经死了”
直到你下一次使用它。
3️⃣ “每天几百条”的错误是一个强信号
如果你看到的是:
- 少量
- 长期
- 稳定
- 与流量无强相关
那几乎可以直接判定:
这是“空闲连接被回收后再次复用”
三、为什么“我明明重连了,还是会报错?”
这是最容易被误解的地方。
❗关键事实
mysqli 的ping()/ 状态检测,本身就会触发底层send()
示例代码:
if (!$mysqli->ping()) {
$mysqli = new mysqli(...);
}实际执行顺序(真实世界)
调用 ping()
→ mysqli 内部立刻向 socket 发送 5 bytes
→ TCP 已经半死
→ errno=110 产生(Notice / Warning)
→ PHP 才返回控制权
→ 你的重连逻辑才开始执行👉 错误发生在“检测阶段”,不是“使用阶段”
所以你看到的是:
- 日志里有 notice
- 但业务逻辑已经成功重连并继续执行
这是一个时序问题,不是代码逻辑错误。
四、mysqli 在 Swoole 中的三大坑
❌ 1. 共享一个 mysqli 实例给多个协程
static $mysqli;后果:
- 非协程安全
- 随机超时
- 状态错乱
❌ 2. 依赖 mysqli 自动重连
mysqli.reconnect = On这是 不可靠的历史遗留特性,在现代环境中不应使用。
❌ 3. 把持久连接当“优化手段”
在 Swoole 中:
- 长连接 × 常驻进程
- = 死连接概率翻倍
五、生产级正确做法
✅ 方案一:在统一入口做连接健康兜底(推荐)
public static function getMysql(string $name): \mysqli
{
$mysqli = self::$pool[$name] ?? null;
try {
if (!$mysqli || !$mysqli->ping()) {
if ($mysqli instanceof \mysqli) {
@$mysqli->close();
}
$mysqli = self::createMysql($name);
self::$pool[$name] = $mysqli;
}
} catch (\Throwable $e) {
if ($mysqli instanceof \mysqli) {
@$mysqli->close();
}
$mysqli = self::createMysql($name);
self::$pool[$name] = $mysqli;
}
return $mysqli;
}📌 所有业务代码不感知重连逻辑
✅ 方案二:定时任务 / 低频任务直接“每轮新连接”
$mysqli = new mysqli($host, $user, $pass, $db);适用场景:
- cron
- 定时 RPC
- 结算 / 对账
成本可接受,逻辑最简单。
✅ 方案三(最推荐):使用 Swoole 官方 MySQL 客户端
$mysql = new Swoole\Coroutine\MySQL();
$mysql->connect([
'host' => '127.0.0.1',
'user' => 'user',
'password' => 'pass',
'database' => 'db',
]);优势:
- 协程安全
- 错误是返回值而不是 PHP notice
- 官方支持连接池
六、MySQL 参数建议(配合使用)
wait_timeout = 60
interactive_timeout = 60
net_read_timeout = 30
net_write_timeout = 30目的不是延长连接寿命,而是:
让死连接尽早暴露,而不是潜伏
七、关于日志:要不要“消灭” notice?
现实建议
- 业务已正确重连 → 无需恐慌
可以:
- 降级为 info
- 或精准吞掉 errno=110
set_error_handler(function ($errno, $errstr) {
if (strpos($errstr, 'send of') !== false &&
strpos($errstr, 'errno=110') !== false) {
return true;
}
return false;
});八、一句话总结
**mysqli 在 Swoole 里不是不能用,
而是不能再用“PHP-FPM 时代的用法”。**
九、适用人群
- Swoole / RPC / TCP 服务
- 常驻 Worker
- MySQL 偶发超时但无法复现
- 日志中出现
send of 5 bytes failed