Karp 的技术博客
关键词: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

mysql

版权属于:karp
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
更新于: 2026年01月28日 04:10
0

目录

来自 《[踩坑] Swoole 常驻进程中使用 mysqli 的那些坑与正确姿势》