機制原理
PHP 為防止腳本無限運行佔用資源,內建執行時間限制機制:
- 默認值: 30 秒(
php.ini: max_execution_time = 30)
- 計時範圍: 僅計算 PHP 解析器執行 CPU 時間,**不包含**:系統調用(睡眠、文件 I/O、網絡請求、數據庫查詢)、
sleep()、stream_select() 等阻塞操作
- 超時處理: 觸發
E_ERROR,腳本強制終止,無法被 try-catch 捕獲
- CLI 模式: 默認 0(無限制),受
max_execution_time 影響需顯式設置
修改方案優先級對比
| 方案 | 作用域 | 優先級 | 適用場景 | 權限需求 |
php.ini |
全局 |
最低 |
專用服務器、所有項目統一 |
root / 管理員 |
.user.ini (PHP-FPM) |
目錄及子目錄 |
中 |
共享主機、單項目配置 |
文件寫權限 |
| Nginx/Apache 配置 |
虛擬主機/路徑 |
高 |
反向代理層面超時控制 |
Web 服務器配置權限 |
ini_set() |
當前腳本 |
最高 |
單腳本動態調整、無服務器權限 |
無 |
set_time_limit() |
當前腳本 |
最高 |
同上,函數別名 |
無 |
動態修改代碼模式
1. 基礎用法
<?php
// 設置無限制(慎用!僅 CLI 或受控任務)
ini_set('max_execution_time', '0');
set_time_limit(0); // 等價
// 設置具體秒數(推薦)
ini_set('max_execution_time', '300'); // 5 分鐘
set_time_limit(300);
2. 安全模式:保存原值、執行、恢復
<?php
function withExtendedTimeLimit(int $seconds, callable $task): mixed
{
$original = ini_get('max_execution_time');
$originalSet = set_time_limit($seconds); // 返回舊限制秒數
try {
return $task();
} finally {
// 恢復原限制
set_time_limit($originalSet);
// ini_set('max_execution_time', $original); // 同步 ini 值
}
}
// 使用
$result = withExtendedTimeLimit(600, function() {
// 發送 10 萬郵件、生成大報表、視頻轉碼等
foreach ($users as $user) {
sendNewsletter($user);
// 定期重置計時器(防止單次循環過長被誤殺)
set_time_limit(600);
}
});
3. 長任務進度輸出與緩存刷新
<?php
ini_set('max_execution_time', '0'); // CLI 或後台任務
set_time_limit(0);
$total = 100000;
for ($i = 1; $i <= $total; $i++) {
// 業務邏輯
processItem($i);
// 每 1000 次輸出進度並刷新緩存
if ($i % 1000 === 0) {
echo "已處理: $i / $total (" . round($i/$total*100, 1) . "%)\n";
// 關鍵:刷新輸出緩存,讓瀏覽器/終端實時看到
if (ob_get_level()) ob_flush();
flush();
// 可選:重置計時器防止單次迴路超時
set_time_limit(300);
}
}
Web 環境長任務最佳架構:異步化
核心原則:HTTP 請求不應超過 30 秒。 長任務必須異步化:
| 架構 | 實現方式 | 優點 | 缺點 |
| 消息隊列 |
Redis Lists / RabbitMQ / Kafka + Worker 進程 |
解耦、可擴展、重試、優先級、監控 |
基礎設施複雜度高 |
| 數據庫任務表 |
tasks 表 + Cron/定時器輪詢執行 |
簡單、無額外依賴、事務一致性 |
輪詢延遲、擴展性弱 |
| PHP 內建 |
pcntl_fork() 子進程 / exec() 後台命令 |
無外部依賴 |
進程管理困難、內存洩漏風險、Windows 不支援 pcntl |
| Serverless |
AWS Lambda / 阿里雲函數 / Cloudflare Workers |
零維護、按量付費、自動擴縮容 |
冷啟動、執行時間限制(通常 15min)、Vendor Lock-in |
最小化異步示例(數據庫任務表)
<?php
// 控制器:創建任務,立即返回
public function exportReport(Request $request)
{
$task = Task::create([
'type' => 'export_report',
'payload' => ['user_id' => auth()->id(), 'format' => 'xlsx'],
'status' => 'pending',
]);
return response()->json(['task_id' => $task->id, 'message' => '任務已排隊,稍後可下載']);
}
// Worker(由 Supervisor 管理常駐進程)
while (true) {
$task = Task::where('status', 'pending')->oldest()->first();
if ($task) {
$task->update(['status' => 'running', 'started_at' => now()]);
try {
$file = generateReport($task->payload);
$task->update(['status' => 'completed', 'result' => $file, 'completed_at' => now()]);
} catch (\Throwable $e) {
$task->update(['status' => 'failed', 'error' => $e->getMessage()]);
}
} else {
sleep(5); // 無任務休眠
}
}
常見誤區與排查
| 現象 | 原因 | 解決 |
| ini_set(0) 不生效 |
PHP 以安全模式運行 / 禁用函數 / Web 服務器有更嚴格超時 |
檢查 disable_functions、Nginx fastcgi_read_timeout、Apache Timeout |
| sleep() 不計入時間 |
機制設計:僅計 CPU 時間 |
正常行為;長輪詢用 set_time_limit() 重置 |
| CLI 默認無限制但報超時 |
系統 ulimit -t / systemd TimeoutSec / 容器限制 |
檢查運行環境硬性限制 |
| set_time_limit() 在循環中無效 |
每次調用重置計時器而非累加 |
在循環內定期調用以「續命」 |
生產環境配置清單
; php.ini
max_execution_time = 30 ; Web 默認
max_input_time = 60 ; 接收數據時間
memory_limit = 256M ; 內存限制
; Nginx + PHP-FPM
fastcgi_read_timeout 300s; ; 代理層超時 ≥ PHP 設置
fastcgi_send_timeout 300s;
fastcgi_buffers 16 16k;
; PHP-FPM 池配置
request_terminate_timeout = 300 ; FPM 硬性終止(秒),0=關閉
request_slowlog_timeout = 10s ; 慢日誌閾值
slowlog = /var/log/php-fpm/$pool.slow.log