每个 frankenphp worker 实例必须单独配置日志输出路径,因其进程完全隔离,stderr 不共享,若不显式指定独立日志路径,多个实例日志将混杂难分;正确做法是通过启动命令重定向 stderr 或在 systemd 中配置 standarderror=append:/path/to/app1.log,并确保路径唯一、权限正确、避免共用 error_log 文件。

每个 FrankenPHP worker 实例必须单独配置日志输出路径
FrankenPHP 的 worker 进程彼此完全隔离,不共享 stdout/stderr,也不会自动按实例区分日志。如果你启动了 frankenphp-worker --script=/var/www/app1/worker.php --addr=:8081 和 frankenphp-worker --script=/var/www/app2/worker.php --addr=:8082,但没做任何日志配置,两个进程的 error_log()、var_dump()、PHP 解析错误、甚至 panic 堆栈,**全都会打到各自进程的 stderr**——而 systemd 或 supervisor 默认会把所有 stderr 合并进同一个 journal 或日志流,导致无法分辨哪条是 app1 的队列超时,哪条是 app2 的 Redis 连接失败。
正确做法是:为每个 worker 实例显式指定独立的日志文件路径,且不能依赖 php.ini 的全局 error_log(它不可控、易冲突):
- 在每个 worker 脚本开头用
error_log("app1 started", 3, "/var/log/frankenphp/app1-worker.log")强制定向,但注意:这仅对error_log()生效,不捕获 PHP Fatal Error 或扩展加载失败 - 更可靠的方式是在启动命令中重定向 stderr:
frankenphp-worker --script=... --addr=:8081 2>> /var/log/frankenphp/app1-stderr.log - systemd service 文件里用
StandardError=append:/var/log/frankenphp/app1-stderr.log,配合Rotate=和MaxSize=实现轮转 - 业务日志(如“订单已推送到队列”)必须由应用层自己写,推荐用 Monolog + StreamHandler 输出到固定路径,**绝不要混进 stderr 或 error_log 文件**
Caddyfile 的 log 指令对 worker 日志无效
很多人误以为在 Caddyfile 里加个 { log } 就能收集 worker 日志,其实完全没用。Caddy 的 log 指令只作用于 HTTP 请求生命周期(即 php_server 处理的请求),而 worker 是后台常驻进程,不走 HTTP 路由,也不经过 Caddy 的 request handler。你在 Caddyfile 里写的 log { output file /var/log/caddy/access.log } 只会影响 curl http://localhost:8000/api/health 这类请求,对 frankenphp-worker 进程内部的 sleep(30)、Redis::blPop() 等行为毫无感知。
所以:
- 别在 Caddyfile 里为 worker 日志费劲配
log块 - 别指望
journalctl -u caddy能看到 worker 的业务错误 - worker 的启动失败(如
failed to open stream: No such file or directory)、SIGTERM 处理、内存溢出 panic,只出现在该实例自己的 stderr 流里
多个 worker 共享同一 error_log 路径会导致日志错乱
这是线上最常踩的坑:给两个 worker 都设了 error_log = /var/log/php-error.log(通过 php.ini 或 ini_set()),结果日志内容交叉、行序颠倒、甚至部分写入被截断。原因很简单:多个进程并发写同一个文件,没有锁,没有缓冲协调,Linux 的 write() 系统调用不是原子的。
典型现象包括:
- 一行日志被拆成两段,中间插进另一进程的输出
- tail -f 看到时间戳跳变(前一秒是 app1,后一秒跳回 app2 的旧时间)
- logrotate 切割时某个 worker 正在写,导致新文件权限错误或丢失最后几条
解决方式只有两个字:隔离。每个 worker 必须有专属路径:
- 路径命名带标识:
/var/log/frankenphp/queue-worker.log、/var/log/frankenphp/notify-worker.log - 目录需提前创建并赋予 frankenphp 运行用户可写权限(
chown www-data:www-data /var/log/frankenphp) - 避免用
/tmp下的临时路径——容器重启后丢失,且不同 worker 容易路径冲突
worker 日志里看不到 HTTP 请求上下文
你不能指望在 app1-worker.log 里看到 “用户 ID 12345 在 14:22:01 触发了推送”,因为 worker 不处理 HTTP 请求。它可能是监听 Redis 队列、轮询数据库变更、或定时执行 cron 逻辑——这些行为本身没有客户端 IP、User-Agent、Referer 等字段。
如果业务需要关联请求与后台任务(比如“这个邮件推送是由 API 请求触发的”),必须由上层显式传递上下文:
- HTTP 请求端把 trace_id、user_id、request_id 写进队列消息体
- worker 消费时主动记录:
error_log("[$trace_id] send email to {$user->email}", 3, $logPath) - 禁止在 worker 里调用
$_SERVER['REMOTE_ADDR']——它为空,且语义错误
日志隔离不是配置技巧,而是进程边界的自然体现。一旦你发现两个 worker 的日志混在一起,说明它们还没真正隔离——要么端口冲突导致其中一个没起来,要么日志路径没分清,要么你还在试图用一个进程模拟多个角色。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











