长时服务器任务处理:避免 AJAX 请求超时导致进程重启的正确实践

5次阅读

长时服务器任务处理:避免 AJAX 请求超时导致进程重启的正确实践

本文详解如何应对耗时 7–11 分钟的 xml 生成等长周期服务端操作,指出直接延长 php/http 超时参数无效的根本原因,并推荐基于异步队列的生产级解决方案,包含数据库建模、守护进程设计与容错机制。

本文详解如何应对耗时 7–11 分钟的 xml 生成等长周期服务端操作,指出直接延长 php/http 超时参数无效的根本原因,并推荐基于异步队列的生产级解决方案,包含数据库建模、守护进程设计与容错机制。

在 Web 开发中,当 ajax 请求需触发耗时数分钟的服务端任务(如批量 XML 构建、报表导出或数据同步),常见误区是简单调高 timeout、set_time_limit(0) 或 max_execution_time。然而,问题往往并非源于 PHP 脚本自身超时——而是被更上层的中间件拦截或中断:例如 nginx 的 proxy_read_timeout(默认 60 秒)、apache 的 Timeout 指令、负载均衡器空闲超时,甚至浏览器对长连接的静默终止。此时你观察到“无响应但服务端进程重启”,正是请求链路某处主动断连后,Web 服务器(如 PHP-FPM)因连接丢失而放弃当前 worker,后续新请求又触发全新执行实例所致。

因此,根本解法不是“撑住更久”,而是“不阻塞请求”。推荐采用异步任务队列模式,将耗时逻辑从 HTTP 生命周期中剥离:

✅ 正确架构:请求-队列-工作者分离

  1. 客户端(AJAX)仅提交任务,立即返回任务 ID
    修改前端代码,不再等待 XML 生成完成,而是发起轻量级提交并轮询状态:
// 提交任务请求(毫秒级响应) $.ajax({   url: '/api/submit-xml-job',   method: 'POST',   data: { config: JSON.stringify(yourParams) },   success: function(resp) {     const jobId = resp.job_id;     // 启动轮询     pollJobStatus(jobId);   } });  function pollJobStatus(id) {   $.get(`/api/job-status?id=${id}`, function(data) {     if (data.status === 'completed') {       window.location.href = `/download/xml?job_id=${id}`; // 或注入结果     } else if (data.status === 'failed') {       alert('任务失败:' + data.error);     } else {       setTimeout(() => pollJobStatus(id), 3000); // 3秒后重试     }   }); }
  1. 服务端(PHP)仅写入队列表,绝不执行耗时逻辑
    创建数据库表 job_queue:
CREATE TABLE job_queue (   id BIGINT PRIMARY KEY AUTO_INCREMENT,   params JSON NOT NULL,   status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,   started_at TIMESTAMP NULL,   finished_at TIMESTAMP NULL,   result TEXT NULL,   error TEXT NULL );

对应提交接口(/api/submit-xml-job)仅做插入:

// submit-xml-job.php $params = json_encode($_POST['config'] ?? []); $stmt = $pdo->prepare("INSERT INTO job_queue (params) VALUES (?)"); $stmt->execute([$params]); echo json_encode(['job_id' => $pdo->lastInsertId()]);
  1. 后台守护进程持续消费队列
    编写独立 CLI 脚本(如 worker.php),使用 Supervisor 管理其生命周期:
<?php // worker.php require 'vendor/autoload.php'; $pdo = new PDO(/* your DSN */);  $loopCount = 0; while (true) {     try {         // 乐观锁式获取待处理任务(防止并发重复执行)         $stmt = $pdo->prepare("             select id, params FROM job_queue              WHERE status = 'pending'              ORDER BY id ASC LIMIT 1              for UPDATE SKIP LOCKED         ");         $stmt->execute();         $job = $stmt->fetch(PDO::FETCH_ASSOC);          if (!$job) {             sleep(5); // 空闲时休眠,降低数据库压力             continue;         }          // 标记为处理中         $pdo->prepare("UPDATE job_queue SET status='processing', started_at=NOW() WHERE id=?")            ->execute([$job['id']]);          // 执行核心逻辑(此处可安全运行 10+ 分钟)         $result = generateXmlFromJson($job['params']);          // 更新结果         $pdo->prepare("UPDATE job_queue SET status='completed', finished_at=NOW(), result=? WHERE id=?")            ->execute([$result, $job['id']]);      } catch (Exception $e) {         // 记录错误并标记失败         $pdo->prepare("UPDATE job_queue SET status='failed', error=? WHERE id=?")            ->execute([$e->getMessage(), $job['id'] ?? 0]);     }      $loopCount++;     // 每 100 次循环主动退出,由 Supervisor 重启,避免内存泄漏累积     if ($loopCount >= 100) {         exit(0);     } }

使用 Supervisor 配置确保进程永驻(/etc/supervisor/conf.d/xml-worker.conf):

[program:xml-worker] command=php /var/www/worker.php autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/xml-worker.log

⚠️ 关键注意事项

  • 禁止在 Web 请求中调用 set_time_limit(0):它无法绕过反向代理/Nginx 层超时,且会拖垮 Web 服务器并发能力;
  • 队列表必须支持高并发安全消费:使用 SELECT … FOR UPDATE SKIP LOCKED(mysql 8.0+/postgresql)或 redis List + BRPOP 实现原子出队;
  • 任务需幂等设计:若工作者崩溃重试,重复执行不应导致数据异常;
  • 增加监控与告警:记录任务耗时、失败率,对长时间 pending 任务自动告警;
  • 前端需提供取消机制:通过更新队列表 status 为 cancelled 并在工作者中检查中断信号。

该方案将响应时间稳定控制在毫秒级,彻底规避网络层超时陷阱,同时具备可伸缩性(可横向扩展多个工作者)、可观测性(每任务有完整生命周期日志)和健壮性(崩溃自愈)。对于任何超过 10 秒的后台操作,这应是你的默认技术选型。

text=ZqhQzanResources