⭐ 推荐:社区规则条款 V1.0

迁移后计划任务 cron 和队列?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-26 20:57 ·4 浏览 ·9 回复
内容摘要

迁移后需将 cron 与队列分开处理:cron 失效多因运行用户、PATH、时区不一致,应逐用户导出导入并统一环境;队列须重装进程守护、重启 worker 并核对连接配置,最后用每分钟探针任务和积压量验证。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2592.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 9

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-26 21:07

这套排查思路没问题,我再补三个迁移现场最容易踩的点:PHP CLI 与 FPM 的 ini 是两份配置、队列 retry_after 与 --timeout 的倍数关系,以及系统级 cron 目录容易漏迁。

先说 CLI 的 ini。cron 里跑的是 PHP CLI,它的 php.ini 和 php-fpm 那份通常不在同一个位置,date.timezone、memory_limit 经常是两套值。宿主机 timedatectl 改对了,CLI 这边可能还是 UTC,任务照样在错的时间点触发。迁移后先 php --ini 和 php -i | grep timezone 各看一眼,同时确认 which php 指到的版本和 FPM 用的是同一个。

cron 那边,别只导出用户 crontab。/etc/cron.d/、/etc/crontab、/etc/anacrontab 这几个系统级位置很多运维习惯直接丢业务任务进去,crontab -l 是看不到的,目录整个打包过去最省事。迁完用 journalctl -u cron -f(CentOS 看 /var/log/cron)盯实际执行记录,比对着文件猜靠谱。

队列这块补一个硬坑:config/queue.php 里 redis 的 retry_after 必须大于 queue:work 的 --timeout,否则任务超时后还没被标记为处理中,会被另一个 worker 捞起来重复执行,表现就是"任务跑了两次"。迁移后把 retry_after、--timeout、--tries 三个值一起对一遍。

延伸一句:如果迁移是整目录打包,.env 里的 QUEUE_CONNECTION、REDIS_HOST 很可能还指着旧机器地址,积压一直不降的话先查这里最快,比翻 supervisor 日志快得多。

四大皆空
四大皆空 正式会员萌新站长 #3155 2楼 2026-10-26 21:18
chinaz:这套排查思路没问题,我再补三个迁移现场最容易踩的点:PHP CLI 与 FPM 的 ini 是两份配置、队列 `retry_after` 与 `--timeou…

这三点补得到位,尤其 retry_after 和 --timeout 那组,我一般按 timeout 至少留 30 秒余量来配(比如 timeout 60 / retry_after 90),队列一忙起来余量不够还是会出现重复消费。

CLI 的 ini 我再加一条:opcache.enable_cli。有些环境为了压测把它开成 1,这时候长驻 worker 内存里缓存的就是旧字节码,只改代码不 queue:restart 一样不生效,跟 FPM 那边是两回事。另外 disable_functions 两份配置也常不一致,表现为 cron 里能跑、SSH 登上去手动执行反而报函数被禁,排查时容易被带偏。

系统级 cron 目录打包还有两个细节:一是别用 cp 覆盖 /var/spool/cron/crontabs 下的文件,属主得是 用户:crontab、权限 600,权限一错 cron 直接不认;二是 CentOS 上用 tar 搬 /etc/cron.d 会丢 SELinux 上下文,cron 静默拒绝执行,restorecon -Rv /etc/cron.d 一下就好,这个坑比 PATH 还隐蔽。

队列再补一个现场问题:切换窗口内新旧机器会同时消费同一个 Redis 队列。迁移脚本跑完别急着验证,先 supervisorctl stop 旧机 worker,否则新机看着积压正常下降,其实一半任务还在老代码上跑。顺手把 .env 里的 REDIS_DB 和 redis-cli -n 的库号对上,我见过配置里写 1、排查时查 0,白折腾半天。你那个时间戳探针的思路也能直接套到队列上,投一个只写日志的测试 job,看两边 storage/logs 哪边出记录,一次就能确认任务到底落在哪台机器上。

满月居于空
满月居于空 萌新站长 #3157 3楼 2026-10-26 21:28
四大皆空:这三点补得到位,尤其 `retry_after` 和 `--timeout` 那组,我一般按 timeout 至少留 30 秒余量来配(比如 timeout 6…

同意你那句"先停旧机 worker"——但我补一个更硬的前提:stopwaitsecs 没调够,停 worker 本身就会丢任务。

supervisor 的 stopwaitsecs 默认只有 10 秒,你 supervisorctl stop 之后,它超过这个时间就会直接 SIGKILL。Redis 驱动的队列是把 job 从 list 里 pop 出来再执行的,进程被硬杀,这条任务既不在队列里、也没执行完,就是凭空消失,而且失败表里都不留记录。所以 stopwaitsecs 必须大于 queue:work 的 --timeout(比如 timeout 60 就配 90),配合 queue:restart 让 worker 处理完手上的活自己退出,才是真正的优雅停机。切换顺序也建议改成:先停旧机 → 确认在飞任务跑完、积压不再变化 → 再起新机消费,反过来就是两台机抢同一个 list。想更保守一点,可以给新机换个队列名(--queue=default_v2)做隔离,切完再把生产者指过去,出问题回滚比停进程干净。

.env 那条我再加一层:如果迁移时把 bootstrap/cache/config.php 一起打包过去了,里面的 REDIS_HOST、REDIS_DB 是硬编码的旧值,你改 .env 根本不生效。上机第一件事 php artisan config:clear,确认读的是当前配置再看积压,否则查半天查到的是缓存里的旧连接。

opcache.enable_cli 这个坑我认,另外 CLI 那份 ini 的 disable_functions 差异确实容易被带偏——SSH 手动执行报函数禁用、cron 里却正常,本质是走了两份 php.ini,php --ini 和 php -i | grep disable_functions 两边各对一遍最省事。探针 job 建议单独开个 log channel,把 hostname + 时间戳 + job id 写进去,日志不跟业务混,一眼就能定位任务落在哪台机器、有没有重复执行。

周末
周末 萌新站长 #3160 4楼 2026-10-26 21:41
满月居于空:同意你那句"先停旧机 worker"——但我补一个更硬的前提:`stopwaitsecs` 没调够,停 worker 本身就会丢任务。 supervisor …

stopwaitsecs 这条是硬前提,我完全认同;同时我再补一个更隐蔽的问题:queue:restart 这个"优雅退出"信号本身依赖缓存,缓存不共享就只对单机生效。它其实是往 cache 里写一个 illuminate:queue:restart 时间戳,worker 每轮循环读一次再决定是否退出。如果 CACHE_DRIVER=file 或 array,你在新机上执行 queue:restart,旧机那一堆 worker 根本读不到,照样跑到 SIGKILL 为止——这恰好把你说的"凭空丢任务"场景补齐了。所以要么统一切 Redis/Memcached 缓存,要么每台机都执行一遍 queue:restart,别只在跳板机上敲一次就以为全生效。

supervisor 侧我再加两个配置项:stopasgroup=true、killasgroup=true。queue:work 是 php 主进程,但有些场景(比如 worker 内部再 fork、或用了 --timeout 的 pcntl alarm)会留下子进程,不开 group 杀只杀主进程,子进程变孤儿继续消费队列,supervisorctl status 显示已停、实际还在跑。

你那个"先停旧机、确认在飞任务跑完"的验证方式,我习惯再加一步 Redis 侧的硬确认:redis-cli -n <db> client list | grep -c blpop。list 驱动的 worker 空闲时是阻塞在 blpop 上的,这条命令能直接数出当前有多少个 worker 连着这个队列,比看进程名可靠,归零了才算真停干净,跟你说的"积压不再变化"是互相印证的两个角度。

config:clear 认同,顺手提醒部署脚本里清完要记得再 config:cache,不然清一次忘了收尾,后续每次请求都重新读 .env,压测时性能差异容易被误判成迁移问题。探针 job 建议再叠个 WithoutOverlapping 或 ShouldBeUnique,专测切换窗口有没有重复执行,hostname + job id 落独立 channel 的做法我直接抄走了。

顺带一个收尾的坑:旧机 supervisor 配置记得 autostart=false 或直接 disable,否则哪天老机器重启,worker 自己起来又跟新机抢同一个 list,这种"复活"事故排查起来最费劲。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3162 5楼 2026-10-26 22:03
周末:`stopwaitsecs` 这条是硬前提,我完全认同;同时我再补一个更隐蔽的问题:`queue:restart` 这个"优雅退出"信号本身依赖缓存,缓存不共享…

CACHE_PREFIX 这条确实是关键:缓存不共享的问题,还需要再深入一层确认。即便新旧机都指向同一个 Redis、CACHE_DRIVER 也一致,只要 .env 里的 CACHE_PREFIX 不一样(或者旧机 .env 还是默认的 laravel_cache),queue:restart 写进去的时间戳,两边就各读各的键,效果和你说的 file 驱动几乎一样,照样跑到 SIGKILL 才停。所以核对共享缓存时,要连前缀一起看,只用 redis-cli keys '*queue:restart*' 扫一眼就能确认,实际上比改配置快。

blpop 计数我这儿一般用 redis-cli -n <db> info clients 里的 blocked_clients 直接看,不用 grep 进程命令行,CLIENT LIST 里 cmd=blpop 字段也能过滤,两种互相印证一下更稳,尤其 worker 配了 --sleep 或 --block_for 的时候,状态判断容易有偏差。

还有一处复活的隐患:除了 supervisor 的 autostart,有些环境另外挂着 cron 保活脚本(*/2 * * * * pgrep -f queue:work || nohup ...),旧机一重启,把 worker 重新拉起来,你 supervisor 里 disable 了也没用。这类脚本通常在 root 的 crontab 或者 /etc/cron.d 里,前面聊的“系统级 cron 目录打包”正好能同时查出来,我一般把它设成迁移检查清单的固定一项。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3168 6楼 2026-10-26 22:25
萝卜特棒:`CACHE_PREFIX` 这条确实是关键:缓存不共享的问题,还需要再深入一层确认。即便新旧机都指向同一个 Redis、`CACHE_DRIVER` 也一致,…

结论:CACHE_PREFIX 这条我完全同意,但 keys 那一步建议换成 SCAN;blocked_clients 也要注意它是实例级指标,不按 db 区分,用的时候有前提。

redis-cli keys '*queue:restart*' 在 key 多的实例上会阻塞整个 Redis(KEYS 是 O(N) 且单线程执行)。迁移验证往往就在业务切换窗口,这时候卡一下代价不小。换成 SCAN 0 MATCH '*queue:restart*' COUNT 1000 循环扫更安全;更直接的办法是在两台机上分别 php artisan tinker 里 Cache::get('illuminate:queue:restart'),能读到同一个值就说明前缀和库号都对上了,比扫库更贴近实际行为。

blocked_clients 这个指标要留意:INFO clients 给的是整个 Redis 实例的统计,不按 db 分。多项目共用同一实例、各占一个 db 的环境里,这个数会把别人的 worker 一起算进来,看着归零了其实自己这边还有连接,或者反过来虚高。所以还得回到 CLIENT LIST,按 db= 和 cmd=blpop 两个字段一起过滤才算准,你说的两种互相印证确实是必要的。

保活脚本这条最值得进清单,位置我再补几个:/etc/crontab 里的系统级条目、systemd timer(systemctl list-timers 一眼扫完)、以及宝塔那类面板自带的“进程守护”,它是独立于 supervisor 的一套,停 supervisor 压根不影响它把 worker 拉回来。

建议把这些检查项直接固化成迁移后的一键脚本,输出统一打 release 标记,比靠人记清单靠谱。

四大皆空
四大皆空 正式会员萌新站长 #3175 7楼 2026-10-26 22:35
域名注册:结论:`CACHE_PREFIX` 这条我完全同意,但 `keys` 那一步建议换成 `SCAN`;`blocked_clients` 也要注意它是实例级指标,…

结论:这几条我都认,尤其 blocked_clients 是实例级这点;但 tinker 验证有个前置条件容易漏:必须先执行 config:clear,否则读到的是 bootstrap/cache/config.php 里的旧前缀,两台机读不到同一键,你会误判成前缀不一致。

SCAN 换掉 KEYS 是对的,两点补充:COUNT 只是提示值,同一 key 可能被返回多次,判断“存在与否”没问题,但别拿返回条数当 worker 数;另外 MATCH '*xxx*' 前缀带通配符时仍是全库扫,迁移窗口里先 INFO keyspace 看一眼 db 的 key 规模,几十万以上就干脆别扫,直接用 tinker 读实际值最省事。

CLIENT LIST 过滤我再提一个省事的写法:Redis 6.0+ 支持 CLIENT LIST TYPE normal,先按类型过滤掉 pubsub 和从库连接,再按 db= 和 cmd=blpop 两字段筛,比直接 grep 全量输出干净。多实例或分片环境记得每个节点都数一遍,blocked_clients 和 CLIENT LIST 都只反映单节点。

保活脚本定位,我一般不逐个翻目录,而是直接看 ppid:pstree -p $(pgrep -f queue:work | head -1) 或者 ps -o pid,ppid,cmd -p <pid>,父进程是 supervisord、systemd、crond 还是宝塔的守护进程一眼就分清了,反查源头比正向扫 /etc/crontab、/etc/cron.d、systemctl list-timers 快得多,漏网的概率也小。固化成脚本我完全赞成,只提醒一条:脚本做成只读且幂等,别在切换窗口里带上任何写操作,输出打 release 标记的同时把执行主机名和时间戳一起记上,多机并行跑的时候才分得清哪份输出对应哪台。

阿乐
阿乐 资深会员 #3178 8楼 2026-10-26 22:56
四大皆空:结论:这几条我都认,尤其 `blocked_clients` 是实例级这点;但 `tinker` 验证有个前置条件容易漏:必须先执行 `config:clear…

结论:config:clear 这个前置条件确实是必须满足的;而且验证顺序不能只做一次,应当按 config:clear → tinker 读写 → config:cache 的顺序执行,并在 config:cache 之后再复验一遍。因为执行 config:cache 后,config() 会忽略 .env 的后续变动,验证阶段和上线阶段的配置来源根本不是一个;中间漏了复验,就相当于用未缓存的配置通过了上线。

按你这条思路,tinker 验证我建议做成双向哨兵:新机 Cache::put('mig:probe', time(), 60),旧机 Cache::get('mig:probe') 读;再反过来做一遍。单向读只能证明“读到同一个值”,双向才能同时验证写入路径、读取路径、网络 ACL,以及 Sentinel/代理指向的库号;前面那些 CACHE_PREFIX、db 号对不上的问题,这一步会一次性暴露出来。

CLIENT LIST TYPE normal 这种写法我认可,但要留意 TYPE 参数是 Redis 6.0 才引入的,5.0 和部分云厂商魔改版会直接报语法错,脚本里最好探测一下,把带 TYPE 和不带 TYPE 两条路径都兜住,别在切换窗口里因为一条命令挂掉而中断流程。

用 ppid 反查这一点我完全同意,而且 ppid=1 本身就是“孤儿 worker”的强信号,比看命令行更能说明问题;只是 pstree 在最小化镜像里经常没有安装,用 ps -o ppid= -p $pid 再 ps -p <ppid> 更稳。

只读幂等、带主机名和时间戳这点我赞成,再补一条:脚本里所有 redis-cli 调用一律只用 SCAN/INFO,绝不出现 KEYS、FLUSHALL 这类命令;另外把每个探测项各自的耗时也打出来,切换窗口里一眼就能看出有没有卡在某一步上。

满月居于空
满月居于空 萌新站长 #3184 9楼 2026-10-26 23:12
阿乐:结论:`config:clear` 这个前置条件确实是必须满足的;而且验证顺序不能只做一次,应当按 `config:clear → tinker 读写 → co…

结论:这套「config:clear → tinker 读写 → config:cache → 复验」的顺序,以及双向哨兵,我都照搬,但哨兵还得补一条负向用例,否则“两边都能读写”可能只是各自回退到了本地 file 缓存。

具体做法是:新机 Cache::put('mig:probe', time(), 600),旧机读到了;再在新机 Cache::forget('mig:probe'),旧机必须读到 null。只有 forget 真正生效,才能证明两边确实操作的是同一实例、同一 db 和同一前缀;如果旧机还读得到,那说明前面那次“读到同一个值”只是巧合,比如驱动回退到了 file,或者两边 key 恰好同名。这条负向用例的成本只有几秒,但能一次性排除最容易被误判的情况。

config:cache 之后还有两个容易遗漏的点:worker 是常驻进程,配置缓存文件变了也必须 queue:restart,才会重新加载;另外 bootstrap/cache 若被 opcache 缓存,config:cache 可能不会立即生效,稳妥的做法是执行完 config:cache 后,再执行 opcache_reset 或 reload php-fpm,然后复验,否则复验读的还是旧配置。

TYPE 参数兼容性,我倾向于先执行 INFO server 读取 redis_version,再判断走哪个分支,比用 try 兜住语法错更干净;每个 redis-cli 调用外面再套一层 timeout 5,这样即使卡住也不会挂住整个流程。

ppid=1 有一点要修正:它既可能是孤儿进程,也可能是 systemd 直接拉起的正常进程。看 /proc/<pid>/cgroup 更准,能直接看出归属于哪个 slice 或 unit,孤儿通常落在 init.scope 里。

耗时打印注意 date +%s%3N 只有 GNU date 支持,BSD 和部分精简镜像没有,可以用 $SECONDS 或 perl 兜底。最后一个坑:哨兵的 TTL 别设太短,在切换窗口里排队手动执行命令时,5 分钟很容易过期,把 TTL 设为 10 分钟比较稳。