PHP 本身無內建排程器,早期常見寫法是在腳本中使用 ignore_user_abort(true) + set_time_limit(0) + while(true){ sleep(); } 實作常駐進程。本文分析此寫法的缺陷,並推薦現代生產環境標準:**系統 Crontab**、**Systemd Timer**、**Supervisor 管理長運行進程**、以及 **任務佇列**。
<?php
// 缺陷示範:請勿在生產環境直接使用
ignore_user_abort(true); // 瀏覽器關閉繼續執行
set_time_limit(0); // 無執行時間限制
$interval = 300; // 5 分鐘
$url = 'https://api.example.com/heartbeat';
while (true) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
curl_exec($ch);
curl_close($ch);
sleep($interval); // 阻塞整個進程
}
# /etc/cron.d/myapp # 每 5 分鐘執行一次 */5 * * * * www-data /usr/bin/php /var/www/myapp/bin/cron.php heartbeat >> /var/log/myapp/cron.log 2>&1 # 每天凌晨 2 點執行重度報表 0 2 * * * www-data /usr/bin/php /var/www/myapp/bin/cron.php daily-report >> /var/log/myapp/cron.log 2>&1
優點:系統級可靠、自動重啟、日誌輪轉 (logrotate)、無需額外程式碼。
缺點:最小粒度 1 分鐘、無法防止重疊執行 (需自行加鎖)。
<?php
// bin/cron.php
$lockFile = fopen('/var/lock/myapp_cron.lock', 'w');
if (!flock($lockFile, LOCK_EX | LOCK_NB)) {
exit("另一實例正在執行,跳過\n");
}
// 註冊關閉釋放鎖
register_shutdown_function(function() use ($lockFile) {
flock($lockFile, LOCK_UN);
fclose($lockFile);
});
// 實際任務邏輯
require __DIR__ . '/../vendor/autoload.php';
$app = new \App\Kernel();
$app->runCommand($argv[1] ?? 'default');
# /etc/systemd/system/myapp-heartbeat.service [Unit] Description=MyApp Heartbeat Cron After=network.target [Service] Type=oneshot User=www-data WorkingDirectory=/var/www/myapp ExecStart=/usr/bin/php bin/cron.php heartbeat StandardOutput=append:/var/log/myapp/heartbeat.log StandardError=append:/var/log/myapp/heartbeat.error.log # /etc/systemd/system/myapp-heartbeat.timer [Unit] Description=Run Heartbeat every 30 seconds [Timer] OnBootSec=1min OnUnitActiveSec=30sec AccuracySec=1s Persistent=true [Install] WantedBy=timers.target
systemctl daemon-reload systemctl enable --now myapp-heartbeat.timer systemctl list-timers --all
# /etc/supervisor/conf.d/myapp-worker.conf [program:myapp-worker] command=/usr/bin/php /var/www/myapp/bin/queue-worker.php process_name=%(program_name)s_%(process_num)02d numprocs=4 # 4 個併發 worker autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/myapp/worker.log stdout_logfile_maxbytes=10MB stdout_logfile_backups=5 environment=APP_ENV="production"
supervisorctl reread supervisorctl update supervisorctl status myapp-worker:*
將「定時觸發」與「實際處理解耦」:
// 推送端 (Cron 執行)
$redis->lpush('queue:heartbeat', json_encode(['time' => time(), 'task' => 'heartbeat']));
// Worker 端 (Supervisor 常駐)
while (true) {
$job = $redis->brpop('queue:heartbeat', 5); // 阻塞等待
if ($job) {
$data = json_decode($job[1], true);
processHeartbeat($data);
}
}
Schedule::command() (Laravel) / Messenger (Symfony)last_heartbeat,Prometheus 抓取告警超過 2 個間隔無更新。ERROR、Exception。