**在標準 Nginx + PHP-FPM (FastCGI) 架構下,瀏覽器關閉/斷開連線後,PHP 腳本預設會**繼續執行**直到正常結束或達到 max_execution_time。** 只有當腳本嘗試向已斷開的連線**輸出資料** (觸發 SIGPIPE / EPIPE) 時,PHP 才會偵測到客戶端中斷,並根據 ignore_user_abort 設定決定是否終止。
<?php
// test_sleep.php
ignore_user_abort(false); // 預設值
file_put_contents('/tmp/test.log', '1 start ' . microtime(true) . PHP_EOL, FILE_APPEND);
sleep(10);
file_put_contents('/tmp/test.log', '2 end ' . microtime(true) . PHP_EOL, FILE_APPEND);
?>
操作:瀏覽器請求 test_sleep.php → 立即關閉分頁/取消請求。
結果:/tmp/test.log 顯示兩行日誌,**腳本完整執行 10 秒後結束**,未提前中止。
ignore_user_abort(true); // 明確設定 // 其餘同實驗 1
結果:同樣完整執行,**ignore_user_abort 在 FastCGI 下對「無輸出腳本」無效**。
<?php
ignore_user_abort(false);
file_put_contents('/tmp/test.log', '1 start ' . connection_status() . PHP_EOL, FILE_APPEND);
sleep(3);
for ($i=0; $i<10; $i++) {
echo "chunk $i\n";
flush(); // 強制刷新輸出緩衝區到 FastCGI 通道
sleep(1);
file_put_contents('/tmp/test.log', '2 loop ' . $i . ' status=' . connection_status() . ' abort=' . connection_aborted() . PHP_EOL, FILE_APPEND);
}
?>
客戶端:Go 程式模擬 HTTP 請求,發送 Header 後 sleep(1) 主動 conn.Close() (發送 FIN/RST)。
結果:
1 start status=0 abort=0 2 loop 0 status=0 abort=0 # 第一次 flush 成功 (客戶端尚未處理 RST) 2 loop 1 status=1 abort=1 # 第二次 flush 觸發 SIGPIPE/EPIPE,PHP 偵測到中斷 2 loop 2 status=1 abort=1 ... 腳本在第 2 次迴圈後被終止 (因 ignore_user_abort=false)
proxy_ignore_client_abort off 預設)。EPIPE / 發送 SIGPIPE 信號。// PHP 內核簡化邏輯 (main/php_main.c / sapi/fpm/fpm/fpm_main.c)
if (php_handle_aborted_connection()) { // 檢測到 SIGPIPE/EPIPE
if (!PG(ignore_user_abort)) { // ignore_user_abort = false
zend_bailout(); // 直接跳出執行 (類似 exit)
}
// ignore_user_abort = true 時,僅標記 PG(connection_status) = PHP_CONNECTION_ABORTED
// 腳本繼續執行,後續輸出會再次觸發 SIGPIPE 但被忽略
}
connection_status() 回傳位元遮罩:0=NORMAL, 1=ABORTED, 2=TIMEOUT, 4=ABORTED|TIMEOUTconnection_aborted() 回傳布林:是否為 ABORTED 狀態風險:用戶關閉頁面,任務仍佔用 Worker 進程,導致併發容量下降。
解決:
ignore_user_abort(true); // 允許繼續
for ($i=0; $i<100; $i++) {
do_heavy_work();
if (connection_aborted()) {
// 記錄進度、清理資源
exit; // 主動結束
}
}
proxy_ignore_client_abort on;:讓 Nginx 繼續等待 PHP 完成再回應 (需評估超時風險)。風險:用戶取消下載,PHP 繼續讀取檔案/推送資料浪費頻寬。
解決:依賴預設行為 (ignore_user_abort=false)。每次 echo + flush() 會觸發連線檢測,客戶端斷開即自動終止腳本。**無需額外代碼**。
風險:上游服務超時重試,導致重複處理。
解決:ignore_user_abort(true) 確保腳本執行完畢,**但必須實現冪等性** (唯一索引、狀態機、分佈式鎖)。
| 層級 | 參數 | 預設值 | 影響 |
|---|---|---|---|
| PHP 運行時 | ignore_user_abort() | false | 腳本層級控制 |
| php.ini | ignore_user_abort | Off | 全域預設值 |
| php.ini | max_execution_time | 30s | 硬性終止上限 (cli 為 0 無限制) |
| Nginx | fastcgi_read_timeout | 60s | Nginx 等待 FastCGI 回應上限 |
| Nginx | proxy_ignore_client_abort | off | 客戶端斷開時是否繼續代理請求 |
| PHP-FPM | request_terminate_timeout | 0 (關閉) | 強制終止超時請求 (優先於 max_execution_time) |
# 腳本執行到第二次 flush 時
sendto(4, "chunk 1\n", 8, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=1234} ---
# PHP 內核捕捉 SIGPIPE,檢查 ignore_user_abort,決定 zend_bailout()
# 後續迴圈不再執行
| 場景 | 預設行為 | 建議設定 | 關鍵點 |
|---|---|---|---|
| 純計算/睡眠無輸出 | 完整執行 | 改用異步佇列 | PHP 無法感知斷線 |
| 定期輸出/flush | 斷線後下次輸出終止 | 保持預設 (ignore_user_abort=false) | 依賴 SIGPIPE 機制 |
| 必須完成關鍵邏輯 | 可能被中斷 | ignore_user_abort(true) + 冪等設計 | 配合 connection_aborted() 記錄日誌 |
| 大檔下載/串流 | 用戶取消即停止 | 預設即最佳 | flush() 觸發檢測 |
**「瀏覽器關閉 = PHP 停止」是常見誤區。** 在 FastCGI 架構下,PHP 腳本生命週期由**輸出動作觸發的連線檢測**與 **ignore_user_abort 設定**共同決定。理解此機制,才能正確設計長任務、串流輸出、關鍵業務的可靠處理策略。**永遠優先考慮異步佇列將長任務移出 HTTP 請求週期**,這是架構層面的最優解。