如何在Heroku中运行需长时间执行的PHP消息推送脚本?
解决PHP长时间推送消息任务中断的问题
这个场景我碰到过不少,咱们一步步拆解问题,从临时解决到长期可靠方案都给你捋清楚:
先试试延长超时时间(临时应急方案)
你的脚本30分钟就停,大概率是PHP或服务器层面的超时限制在起作用,先逐个排查调整:
- PHP脚本层面:在脚本最开头加上
set_time_limit(0);,这会让PHP本身取消脚本执行时间限制,但注意如果是FPM环境,这个设置可能被全局配置覆盖。 - FPM配置层面:找到你的PHP-FPM配置文件(比如
www.conf),修改request_terminate_timeout和max_execution_time这两个参数,设为足够大的值(比如18000,也就是5小时,留足冗余),改完记得重启PHP-FPM服务。 - Web服务器层面:如果是Nginx,要调整
proxy_read_timeout和fastcgi_read_timeout;如果是Apache,修改Timeout指令,确保服务器不会提前断开和脚本的连接。
不过要提醒:长时间跑单个PHP脚本风险很高,服务器重启、脚本崩溃、内存泄漏都可能导致任务半途而废,这只是临时救急,更推荐下面的标准方案。
更可靠的长期方案:队列系统 + 常驻Worker
这是处理这类异步延迟任务的行业标准做法,稳定性拉满:
核心思路是把每个用户的推送任务拆成独立单元,放进队列,再用常驻Worker进程逐个消费,配合进程管理器保证Worker不挂。
举个简单的Redis延迟队列实现例子
- 先把任务批量存入延迟队列:
set_time_limit(0); $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 假设这是你的30个用户ID列表 $userIds = range(1, 30); $currentTimestamp = time(); foreach ($userIds as $index => $userId) { // 计算每个任务的执行时间:第N个用户延迟6*N分钟 $delaySeconds = $index * 6 * 60; $executeAt = $currentTimestamp + $delaySeconds; // 把任务序列化后存入有序集合(用时间戳作为分数) $redis->zAdd('message_push_queue', $executeAt, json_encode(['user_id' => $userId])); }
- 写一个常驻Worker脚本消费队列:
set_time_limit(0); ignore_user_abort(true); // 忽略客户端断开连接 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); while (true) { $now = time(); // 取出当前时间点到期的第一个任务 $tasks = $redis->zRangeByScore('message_push_queue', 0, $now, ['limit' => [0, 1]]); if (!empty($tasks)) { $task = json_decode($tasks[0], true); // 执行你的消息推送逻辑 sendMessageToUser($task['user_id']); // 任务完成后从队列移除 $redis->zRem('message_push_queue', $tasks[0]); } // 每隔10秒检查一次队列,避免占用过多CPU资源 sleep(10); } // 你的消息推送函数 function sendMessageToUser($userId) { // 这里写具体的推送逻辑,比如调用短信API、站内信接口等 error_log("推送消息给用户{$userId}成功,时间:" . date('Y-m-d H:i:s')); }
- 用Supervisor管理Worker进程:
写一个Supervisor配置文件(比如/etc/supervisor/conf.d/message_worker.conf):
[program:message_push_worker] command=php /path/to/your/worker.php autostart=true # 开机自动启动 autorestart=true # Worker挂了自动重启 user=www-data # 运行Worker的用户(和你的Web服务用户一致) stdout_logfile=/var/log/message_push.log # 正常日志路径 stderr_logfile=/var/log/message_push_error.log # 错误日志路径
然后重启Supervisor:supervisorctl reread && supervisorctl update && supervisorctl start message_push_worker
这样Worker会一直后台运行,就算服务器重启,Supervisor也会自动把它拉起来,每个任务独立执行,单个任务失败也不会影响全局。
备选方案:Cron定时任务
如果不想搞队列和常驻进程,也可以用系统定时任务拆分任务:
- 把每个用户的推送时间提前存在数据库里(比如
send_at字段) - 配置一个每分钟执行一次的Cron任务:
* * * * * php /path/to/check_send_task.php - 在
check_send_task.php里,查询当前时间需要推送的用户,执行推送逻辑,然后标记为已推送
这个方案适合简单场景,不需要维护常驻进程,但灵活性不如队列系统。
最后提醒:不管用哪种方案,一定要加详细的日志记录,推送成功/失败都要记下来,方便后续排查问题。
内容的提问来源于stack exchange,提问作者Jerin A Mathews
相关产品推荐
相关产品推荐

