Hyperf协程无自动超时终止机制,wait_timeout仅控制连接池排队等待时长,不作用于SQL执行、Redis pipeline或HTTP请求等实际耗时逻辑;必须用go(..., $timeout)显式包裹并手动清理资源,否则导致协程卡死、资源泄漏与Context污染。

Hyperf 协程本身没有自动超时终止机制,wait_timeout 只控制“排队等连接”的时间,不是协程执行超时开关——你必须手动加超时包裹 + 显式清理,否则协程卡住、资源泄漏、Context 污染全都会发生。
协程执行超时不能靠 wait_timeout
wait_timeout 是连接池参数,只影响协程在池里排队拿连接的等待时长。它对 SQL 执行、Redis pipeline、HTTP 请求等实际耗时逻辑完全无效。设成 0.1 秒,协程里一个 Co::sleep(10) 还是会卡满 10 秒;设成 10 秒,也只是让排队更久,掩盖了真实瓶颈。
- MySQL 报
WaitTimeoutException?先查SHOW STATUS LIKE 'Threads_connected',看是否真连满了 - Redis 卡在
$redis->get()?用swoole_get_local_socket_count()看 ESTABLISHED 连接数是否贴近pool.max_connections - 错误堆栈里没出现你写的业务代码?大概率是超时前就卡在 I/O 上,根本没走到你的 catch 块
用 Co::create(..., $timeout) 包裹关键协程逻辑
PHPUnit 的 @timeout 注解对协程无效,Co::sleep(10) 会让测试直接 hang 死 10 秒。真正可控的方式是显式创建带超时的协程:
go(function () {
try {
// 你的耗时操作,比如 DB 查询、Redis pipeline、HTTP 调用
$result = $this->doHeavyWork();
$this->handleSuccess($result);
} catch (\Throwable $e) {
// 这里能捕获到超时异常(Swoole\Coroutine\ExitException)
logger()->warning('Coroutine timeout', ['exception' => $e]);
// 必须手动清理:close Redis、rollback transaction、unset Context
$this->cleanup();
}
}, 5.0); // 第二个参数是超时秒数
- 超时后抛出的是
Swoole\Coroutine\ExitException,不是WaitTimeoutException - 必须在
catch或finally里做清理,否则Co\Redis实例、未exec()的 pipeline、Context::set()写入的数据全滞留 - 别在
go()外层再套try/catch——它不捕获子协程异常
慢查询/慢 HTTP 必须配驱动级超时
协程挂起后,若底层 socket 一直不就绪(比如 MySQL 慢查询没索引、远端 HTTP 不响应),协程就永远等下去。Swoole 不接管协议层超时,必须手动设:
- MySQL:在
config/autoload/databases.php的options里加PDO::ATTR_TIMEOUT => 5,或驱动配置中设'read_timeout' => 5.0 - Redis:确保
config/autoload/redis.php中'timeout' => 3.0和'retry_interval' => 100都有值 - HTTP 客户端:用
HttpClient::get($url, [], ['timeout' => 5.0]),别依赖全局 config -
DB::timeout(5)对原生Db::select()无效,只作用于 Query Builder 链式调用
pipeline/multi/transaction 必须用 try/finally 或 withPipeline()
这是最隐蔽也最常泄漏的点:$redis->pipeline() 后如果中间抛异常、或忘记 ->exec(),连接就永远被该协程占着。协程不退出,泄漏就不止。
- 错误写法:
try { $redis->pipeline()->set('a',1)->get('b'); } catch (\Exception $e) { }→ 连接泄漏 - 正确写法(Hyperf 3.1+):
$redis->withPipeline(fn ($pipe) => $pipe->set('a',1)->get('b')); - 老版本或自定义逻辑:必须
try { ... } finally { $redis->exec(); },且exec()前要判断是否已调用过 - 事务同理:
Connection::transaction()内部已封装 rollback + release,优先用它而非手写beginTransaction()
最容易被忽略的不是“怎么设超时”,而是“超时后协程还在跑,而你写的 catch 里什么都没做”——此时泄漏已开始,且不会报错,只会慢慢拖垮服务。


















