PHP 本身無內建排程器,常見錯誤做法是用 ignore_user_abort() + sleep() 長連線模擬,極易造成記憶體洩漏、進程殭屍。生產環境應採用 Linux Crontab + Shell 包裝 PHP CLI 架構,穩定、可監控、易維護。
+----------------+ +----------------+ +------------------+ | Crontab 排程 | --> | run_task.sh | --> | php task.php | | (每分鐘檢查) | | (環境/日誌) | | (業務邏輯) | +----------------+ +----------------+ +------------------+
<?php
// task.php - 純 CLI 模式,無 HTTP 頭輸出
define('APP_PATH', __DIR__ . '/../');
require_once APP_PATH . 'bootstrap.php'; // 載入框架/資料庫設定
// 1. 參數解析 (支援多任務)
$action = $argv[1] ?? 'default';
$params = array_slice($argv, 2);
// 2. 記憶體/時間限制
ini_set('memory_limit', '256M');
set_time_limit(0); // CLI 預設 0 (無限制)
// 3. 執行對應任務
try {
switch ($action) {
case 'sync_orders':
(new OrderService())->syncFromERP();
break;
case 'daily_report':
(new ReportService())->generateDaily();
break;
case 'cleanup_logs':
(new LogService())->cleanup(30); // 保留 30 天
break;
default:
echo "Usage: php task.php [action] [params...]\n";
exit(1);
}
echo "[OK] $action completed at " . date('Y-m-d H:i:s') . "\n";
exit(0);
} catch (Throwable $e) {
error_log("[FAIL] $action: " . $e->getMessage());
exit(1);
}
#!/bin/bash
# run_task.sh - 統一環境、日誌、鎖機制
# 專案根目錄
PROJECT_ROOT="/var/www/myapp"
PHP_BIN="/usr/bin/php" # 或 /usr/local/php/bin/php
LOG_DIR="$PROJECT_ROOT/storage/logs/cron"
LOCK_DIR="/tmp/cron_locks"
mkdir -p "$LOG_DIR" "$LOCK_DIR"
# 參數檢查
if [ $# -lt 1 ]; then
echo "Usage: $0 [params...]"
exit 1
fi
ACTION=$1
shift
PARAMS=$@
# 防止重複執行 (flock)
LOCK_FILE="$LOCK_DIR/${ACTION}.lock"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "[$(date)] $ACTION 仍在執行中,跳過本次"
exit 0
fi
# 執行並記錄日誌 (stdout/stderr 合併)
LOG_FILE="$LOG_DIR/${ACTION}_$(date +%Y%m%d).log"
echo "=== [$ACTION] START $(date) ===" >> "$LOG_FILE"
cd "$PROJECT_ROOT"
$PHP_BIN task.php "$ACTION" $PARAMS >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
echo "=== [$ACTION] END $(date) EXIT=$EXIT_CODE ===" >> "$LOG_FILE"
exit $EXIT_CODE
# 編輯當前用戶的 crontab
crontab -e
# 範例排程 (每行: 分 時 日 月 週 指令)
# 每天 02:30 同步訂單
30 2 * * * /var/www/myapp/run_task.sh sync_orders >> /dev/null 2>&1
# 每小時第 5 分產生報表
5 * * * * /var/www/myapp/run_task.sh daily_report >> /dev/null 2>&1
# 每週一 03:00 清理舊日誌
0 3 * * 1 /var/www/myapp/run_task.sh cleanup_logs >> /dev/null 2>&1
# 每 2 分鐘檢查隊列 (高頻任務)
*/2 * * * * /var/www/myapp/run_task.sh process_queue >> /dev/null 2>&1
| 實踐 | 說明 |
|---|---|
| 使用 PHP CLI 模式 | 避開 Web Server (Nginx/Apache) 超時、記憶體限制、Session 鎖 |
| Shell 腳本統一入口 | 集中處理環境變數、日誌輪轉、鎖機制、錯誤碼傳遞 |
| flock 防重複執行 | 上次任務未結束時自動跳過,防止並發衝突 |
| 日誌分檔 + 輪轉 | 按日期/任務分檔,配合 logrotate 自動清理 |
| 非零退出碼告警 | 結合 Zabbix/Promtail 監控退出碼,失敗即時通知 |
| 鎖目錄用 /tmp | 重開機自動清理,避免殘留鎖導致任務永不執行 |
source ~/.bashrc 或直接 export PATH=...chmod +x,且執行用戶對專案目錄有讀寫權set_time_limit(300) (5分鐘) 防止無限循環;Shell 層用 timeout 300 強制殺掉。echo '$json' > /tmp/params.json,PHP 讀取 json_decode(file_get_contents(...), true)。現代 Linux (CentOS 7+, Ubuntu 16.04+) 建議改用 systemd timer,優點:精準到秒、支援隨機延遲、依賴管理、日誌整合 journald。範例單元檔:
# /etc/systemd/system/php-task@.service
[Unit]
Description=PHP Task %i
[Service]
Type=oneshot
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php task.php %i
User=www-data
# /etc/systemd/system/php-task@.timer
[Unit]
Description=Run PHP Task %i periodically
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=15m
[Install]
WantedBy=timers.target
# 啟用
systemctl daemon-reload
systemctl enable --now php-task@sync_orders.timer