PHP 腳本受 max_execution_time (默認 30 秒) 限制,超時會觸發 E_ERROR 並終止執行。無法在代碼中 try/catch 捕獲此致命錯誤,但可通過 register_shutdown_function 在腳本關閉階段檢測超時並執行清理/記錄/告警邏輯。本文提供標準實現模式與進階應用。
// 註冊關閉處理器 (可多次註冊,按順序執行)
register_shutdown_function('handleShutdown');
function handleShutdown(): void {
// 獲取最後發生的錯誤
$error = error_get_last();
// 判斷是否為最大執行時間超時
// 錯誤信息: "Maximum execution time of 30 seconds exceeded"
if ($error !== null && strpos($error['message'], 'Maximum execution time') !== false) {
// 超時處理邏輯
error_log("[TIMEOUT] Script exceeded max_execution_time at {$error['file']}:{$error['line']}");
// 可選:發送告警、寫入審計日誌、釋放鎖、回滾業務狀態
// 注意:此處無法輸出響應給客戶端 (連接已斷開),只能做側邊效應操作
}
// 其他致命錯誤也會觸發
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
error_log("[FATAL] {$error['message']} in {$error['file']}:{$error['line']}");
}
}
// 設置執行時間限制 (僅影響當前腳本)
set_time_limit(2); // 2秒
// 模擬長時間運行
sleep(3); // 將觸發超時
class TimeoutHandler {
private static $instance;
private $startTime;
private $timeoutThreshold;
private $callbacks = [];
private function __construct(int $thresholdSeconds = 30) {
$this->startTime = microtime(true);
$this->timeoutThreshold = $thresholdSeconds;
register_shutdown_function([$this, 'onShutdown']);
}
public static function init(int $threshold = 30): self {
self::$instance ??= new self($threshold);
return self::$instance;
}
// 註冊超時回調
public function onTimeout(callable $callback): self {
$this->callbacks[] = $callback;
return $this;
}
public function onShutdown(): void {
$error = error_get_last();
$elapsed = microtime(true) - $this->startTime;
// 判斷超時:錯誤信息匹配 OR 實際耗時超過閾值 (防止錯誤信息本地化差異)
$isTimeout = ($error !== null && stripos($error['message'], 'maximum execution time') !== false)
|| $elapsed >= $this->timeoutThreshold;
if ($isTimeout) {
foreach ($this->callbacks as $cb) {
try {
$cb($elapsed, $error);
} catch (\Throwable $e) {
error_log("[TimeoutHandler] Callback failed: " . $e->getMessage());
}
}
}
}
// 手動檢查並拋出異常 (用於循環中主動檢測)
public function check(): void {
if (microtime(true) - $this->startTime >= $this->timeoutThreshold) {
throw new \RuntimeException("Execution timeout threshold reached");
}
}
}
// 使用
TimeoutHandler::init(25) // 25秒閾值 (留 5秒緩衝給 shutdown)
->onTimeout(function($elapsed, $error) {
// 記錄到監控系統
Metrics::gauge('script.timeout', 1, ['script' => basename($_SERVER['SCRIPT_FILENAME'])]);
// 釋放分佈式鎖
RedisLock::release('critical_section');
// 發送告警
Alert::send("Script timeout after {$elapsed}s");
});
// 長時間循環中主動檢查
for ($i = 0; $i < 10000; $i++) {
TimeoutHandler::init()->check(); // 超時拋出異常,可被上層捕獲
processItem($i);
}
| 限制 | 說明 | 應對 |
|---|---|---|
| 無法向客戶端輸出 | 關閉階段連接已斷,echo 無效 | 只能寫日誌、發隊列、調 API、更新 DB |
| 執行時間仍受限 | shutdown 函數本身也受 max_execution_time 限制 | 邏輯極簡單、快速;避免網絡調用或設置 set_time_limit(0) 在 shutdown 內 |
| 無法獲取完整堆疊 | error_get_last() 只返回最後一個錯誤 | 配合 xdebug 或自定義錯誤處理器記錄堆疊 |
| CLI vs Web 差異 | CLI 無 max_execution_time 默認限制 (0=無限) | CLI 手動 set_time_limit() 設置 |
| FastCGI 進程管理器 | Nginx + PHP-FPM 有 request_terminate_timeout | 設置 request_terminate_timeout > max_execution_time 讓 PHP 先觸發 |
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
register_shutdown_function | 腳本結束鉤子 | 原生、無依賴、捕獲所有致命錯誤 | 無法輸出響應、邏輯受時限 | 日誌記錄、資源清理、監控上報 |
pcntl_alarm + 信號 | SIGALRM 定時器 | 可中斷阻塞系統調用、精確控制 | 需 pcntl 擴展、僅 CLI、信號處理複雜 | CLI 長運行任務、守護進程 |
| Swoole/Workerman 任務超時 | 事件循環定時器 | 毫秒級精度、可取消、異步非阻塞 | 需擴展/框架、學習成本 | 高性能異步服務、API 網關 |
| 異步隊列 + TTL | 消息隊列過期機制 | 解耦、可重試、可觀測 | 架構複雜、延遲高 | 耗時業務異步化 (視頻轉碼、報表生成) |
| 外部監控殺進程 | Supervisor/Systemd/k8s Probe | 徹底隔離、不污染業務代碼 | 無法優雅關閉、數據可能不一致 | 最後防線、不可控第三方庫調用 |
max_execution_time = 30,CLI 任務根據業務設置 (300-3600s)。register_shutdown_function 是 PHP 處理致命錯誤與超時的「最後一道防線」,配合合理的架構分層 (同步快速響應 + 異步重任務),構建健壯的 PHP 應用體系。