如何判断每分钟运行的 cron 是否会使 Apache 过载?
结论
不要让 payment gateway 的 HTTP 请求一直等待 60 秒。收到支付通知后,应尽快完成校验和第一阶段处理,把订单记录为“等待发送”,立即返回成功响应,再交给独立的后台任务处理到期订单。
每分钟运行一次 cron 通常不会让 Apache 过载。cron 的调度开销很小,真正需要评估的是每次任务扫描的数据量、数据库查询是否使用索引、文件操作需要多长时间,以及多次任务是否会重叠。只要控制并发、为查询建立合适的索引,并保证处理过程具备幂等性,这种方案是可行的。
推荐的处理流程
可以把支付回调和库存系统传输分成两个阶段:
- 验证 payment gateway 的签名或认证信息。
- 使用 gateway 提供的交易 ID 去重。
- 在数据库事务中更新支付状态,完成第一阶段任务。
- 写入后台任务,把执行时间设为当前时间之后 60 秒。
- 立即向 payment gateway 返回成功响应。
- 后台 worker 定期领取到期任务,并向客户的库存系统发送订单。
- 发送成功后将任务标记为完成;发送失败则记录错误并安排重试。
这样不会占用 Apache/PHP worker 等待,也不需要考虑 payment gateway 是否允许请求持续 60 秒。
“60 秒后才能执行”和“必须恰好在第 60 秒执行”是两种要求。cron 每分钟运行一次时,任务一般会在写入后的 60~120 秒内得到处理。如果执行时间需要更接近准确的 60 秒,应使用常驻队列 worker,或者支持延迟任务的队列系统。
数据库任务表示例
可以把待处理任务直接保存在 PostgreSQL 中:
CREATE TABLE order_transfer_jobs (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL,
status text NOT NULL DEFAULT 'pending',
available_at timestamptz NOT NULL,
attempts integer NOT NULL DEFAULT 0,
locked_at timestamptz,
last_error text,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX order_transfer_jobs_pending_idx
ON order_transfer_jobs (available_at)
WHERE status = 'pending';
支付成功后,创建一项延迟任务:
INSERT INTO order_transfer_jobs (order_id, available_at)
VALUES (:order_id, now() + interval '60 seconds');
gateway 可能会重复发送支付通知,因此还要建立业务唯一约束,防止同一订单生成多个相同类型的传输任务:
CREATE UNIQUE INDEX order_transfer_jobs_order_id_idx
ON order_transfer_jobs (order_id);
能否只用 order_id 作为唯一键,需要根据业务规则决定。如果一个订单可以合法传输多次,就应增加任务类型或版本字段,并将其纳入联合唯一键。
使用 cron 执行后台任务
crontab 可以每分钟调用一次 PHP CLI 脚本:
* * * * * /usr/bin/flock -n /var/run/order-transfer.lock /usr/bin/php /opt/shop/bin/process-order-transfers.php >> /var/log/order-transfer.log 2>&1
需要注意以下几点:
flock -n会在前一次任务尚未结束时,让新任务直接退出。- 应使用 PHP CLI,不要通过 HTTP 请求访问网站上的脚本。
- 所有路径都使用绝对路径,因为 cron 的
PATH、工作目录可能与交互式 shell 不同。 - 锁文件所在目录必须存在,运行 cron 的用户也要有写入权限。
- 日志需要配置轮转,以免长期运行后占满磁盘。
也可以在脚本中添加非阻塞锁:
<?php
$lock = fopen('/var/run/order-transfer.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
exit(0);
}
try {
processDueOrderTransfers();
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
外部 flock 一般更简单。即使 PHP 脚本异常退出,操作系统也会释放文件描述符和对应的锁。
安全领取任务
如果以后需要运行多个 worker,可以使用 PostgreSQL 的 FOR UPDATE SKIP LOCKED,防止多个进程领取同一项任务:
WITH selected AS (
SELECT id
FROM order_transfer_jobs
WHERE status = 'pending'
AND available_at <= now()
ORDER BY available_at
FOR UPDATE SKIP LOCKED
LIMIT 50
)
UPDATE order_transfer_jobs AS jobs
SET status = 'processing',
locked_at = now(),
attempts = attempts + 1
FROM selected
WHERE jobs.id = selected.id
RETURNING jobs.*;
领取任务后应尽快提交事务,不要在持有数据库行锁时发起网络请求或执行耗时较长的文件传输。事务提交后再逐项处理,并分别更新任务的成功或失败状态。
使用单进程 flock 时,SKIP LOCKED 不是必需的。不过,保留这套领取机制可以改善容错,也方便以后增加 worker。
任务必须具备幂等性
避免任务重叠只是第一层保护。进程可能在文件已经发送、数据库还没来得及标记成功时崩溃。下次运行时,这个文件就可能被再次发送。因此,传输操作本身也要尽量做到幂等:
- 使用稳定且唯一的订单编号作为外部请求 ID。
- 如果库存系统支持幂等键,每次都发送相同的 idempotency key。
- 使用确定的文件名生成文件,并明确规定如何处理重复文件。
- 在数据库中记录发送状态、尝试次数、响应结果和错误信息。
- 谨慎处理超时。超时不代表对方没有收到,应先查询对方的状态,再决定是否重发。
- 为失败任务设置最大重试次数,超过上限后发出告警。
还要处理长时间停留在 processing 状态的任务。例如,worker 崩溃后,可以把锁定时间超过合理范围的任务重新设为 pending。这个时限应根据实际的最长处理时间确定。如果设得太短,反而可能造成重复处理。
每分钟 cron 的实际负载如何判断
是否会过载,不能只看 cron 是不是每分钟运行一次,还要看每次运行的成本以及最坏情况下的积压量。
操作系统指标
在 cron 运行前后和业务高峰期观察:
- CPU 使用率和 load average
- 可用内存、swap 使用情况
- 磁盘 I/O 延迟和 I/O wait
- 网络吞吐量及连接错误
- 任务运行时间和同时运行的进程数
如果每次运行只执行一次带索引的小查询,再移动少量本地文件,开销通常很低。如果“传输文件”指的是通过网络发送到客户系统,那么网络延迟、超时和重试往往比 cron 调度本身更值得关注。
Apache 和 PHP 指标
后台任务虽然不经过 Apache,却仍会与网站争用 CPU、内存、磁盘和网络资源。需要监控:
- Apache busy/idle workers
- PHP-FPM active、idle 和 queued requests;使用其他 PHP 运行方式时,则查看相应的进程指标
- 网站请求延迟的 p50、p95、p99
- HTTP 5xx、timeout 和连接拒绝数量
如果 cron 运行期间网站的尾延迟明显上升,即使平均 CPU 使用率不高,也说明两者存在资源争用。
PostgreSQL 指标
重点检查:
- 查询执行时间
- 扫描和返回的行数
- 查询是否使用预期索引
- 锁等待和长事务
- 数据库连接数
- 磁盘读取量
- 待处理任务的积压数量
可以使用 EXPLAIN (ANALYZE, BUFFERS) 检查领取任务的查询,但最好先在测试环境或其他可控条件下执行,因为 ANALYZE 会实际运行语句。测试修改数据的 SQL 时,应将其放入可回滚事务,或者改用等价的只读查询来评估访问路径。
建议的测试方法
先在测试环境模拟正常流量和突发流量,包括:
- 正常情况下每分钟产生的订单数。
- 促销期间预计达到的订单峰值。
- 库存系统中断数分钟后产生的积压量。
- 文件传输变慢或持续超时。
- worker 在处理过程中异常终止。
测试时记录:
- 每批处理的任务数量。
- 每次 cron 的总运行时间。
- 单个任务的平均耗时和高分位耗时。
- pending 队列长度以及最老任务的等待时间。
- 数据库查询耗时和锁等待。
- 网站请求延迟和错误率的变化。
- 重试次数和最终失败数量。
如果任务经常运行超过一分钟,应先找到耗时较长的环节,不要直接增加并发。提高并发量可能让数据库或外部库存系统承受更大的压力。
cron 与任务队列如何选择
如果这只是临时 workaround,订单量不高,而且业务可以接受几十秒的调度误差,那么数据库任务表配合每分钟一次的 cron,是一种简单可靠的方案。
出现以下情况时,可以改用支持延迟任务的队列系统或常驻 worker:
- 执行时间需要更接近准确的 60 秒。
- 每分钟有大量订单等待处理。
- 需要指数退避、优先级、死信队列或更完善的重试管理。
- 单个任务的耗时不稳定。
- 需要多个 worker 并行消费。
- 需要更完整的任务监控。
无论使用 cron 还是队列,处理原则都一样:支付回调要快速响应,延迟工作通过异步方式执行,任务状态应当可以追踪,重复执行也不能破坏业务数据。
备注:内容仅供参考。
