同步日志会阻塞协程,异步日志需通过TaskWorker实现;小流量可同步写入,高可靠需额外队列保底。
在 Swoole 的 Worker 进程中直接调用 file_put_contents、error_log 或基于 fopen/fwrite 的日志写入,属于同步阻塞操作。哪怕只是追加一行文本,协程也会停在那等磁盘 I/O 完成——尤其当磁盘繁忙、日志文件过大或挂载在 NFS 上时,延迟可能达几十毫秒。这意味着:同一协程里后续的 HTTP 响应、数据库查询、Redis 调用全被拖住。
常见错误现象包括:
strace 显示大量 write 系统调用阻塞co::sleep(0) 也未能让出控制权——因为 I/O 本身未封装为协程调度点Swoole 的异步日志不是靠「非阻塞文件句柄」实现的(PHP 层不支持),而是通过 task 机制把日志内容发给专用的 TaskWorker 进程去写。主协程只做序列化 + 投递,耗时通常在微秒级。
关键实操点:
$server->set() 中启用 'task_worker_num' => 2(至少 1 个)json_encode 大数组$server->task($logData),不能直接在 onTask 回调里再调 task()(会报错 task method can not be called in task callback)onTask 里建议用 file_put_contents($file, $data, FILE_APPEND | LOCK_EX),加锁防多进程写乱有人用 go(function () { file_put_contents(...); }); 以为实现了异步日志——其实没用。go 只是起新协程,但 file_put_contents 底层仍是同步系统调用,该阻塞还是阻塞,只不过阻塞的是另一个协程而已。Worker 进程内协程总数有限(默认 max_coroutine 是 3000),大量日志协程堆积会导致调度器过载甚至崩溃。
真正有效的路径只有一条:主协程投递 → TaskWorker 写入 → 可选回调通知。如果需要确认日志落盘成功,可在 onTask 里 return true,并在 onFinish 回调里处理,但一般没必要——日志本就是尽力而为的。
日志吞吐量低于 100 条/秒、单条日志小于 1KB、且不跑在机械硬盘上时,同步写入的开销几乎不可见。强行上 task 反而引入 IPC 开销、进程间序列化成本和额外运维复杂度。更值得优先优化的是:关闭调试日志、按等级分流(INFO 写文件,ERROR 推 Slack)、用 buffering 批量刷盘(如 Monolog 的 StreamHandler 配 buffer_size)。
容易被忽略的一点:task 进程挂了,日志就丢了——它不持久化队列。高可靠性场景下,得自己加 Redis 队列 + 消费者保底,或者直接对接 syslog-ng / fluent-bit。