首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP-FPM 過載後無法恢復根因分析:eAccelerator 自旋鎖在共享記憶體中被孤立持有導致全進程阻塞

     編輯:PHP 相關知識

生產環境 PHP-FPM 叢集頻繁出現「過載後永久不可用」現象:一旦負載飆升觸發 request_terminate_timeout 強制終止進程,後續所有請求 (甚至空 PHP 檔案) 導致 CPU 100% 狂轉,負載均衡降權策略完全失效,必須重啟 PHP-FPM 才能恢復。經深度排查,根因為 eAccelerator 共享記憶體自旋鎖在進程被強殺時未釋放,導致所有工作進程永久阻塞於鎖獲取迴圈。

現象與環境

  • PHP-FPM request_terminate_timeout = 5s (請求超時強殺)。
  • 高併發下大量請求觸發 5 秒超時,錯誤日誌大量 WARNING: [pool www] child 12345, script '...' executing too slow。
  • 過載後,任何請求 (含 ) 導致 PHP-FPM 子進程 CPU 100%。
  • 重啟 PHP-FPM 即恢復正常。

核心證據:eAccelerator 源碼中的自旋鎖實現

eAccelerator 使用共享記憶體實現進程間鎖 (mm_mutex),採用 x86 原子指令自旋鎖:

// 簡化版 eAccelerator 共享記憶體自旋鎖
#define spinlock_try_lock(rw) \
    asm volatile("lock ; decl %0" : "=m" ((rw)->lock) : : "memory")
#define _spinlock_unlock(rw) \
    asm volatile("lock ; incl %0" : "=m" ((rw)->lock) : : "memory")

static int mm_do_lock(mm_mutex* lock, int kind) {
    while (1) {
        spinlock_try_lock(lock);           // 原子遞減嘗試獲取
        if (lock->lock == 0) {             // 獲取成功 (lock 變為 0)
            lock->pid = getpid();
            lock->locked = 1;
            return 1;
        }
        _spinlock_unlock(lock);            // 獲取失敗,釋放並讓出 CPU
        sched_yield();
    }
    return 1;
}

static int mm_do_unlock(mm_mutex* lock) {
    if (lock->locked && (lock->pid == getpid())) {
        lock->pid = 0;
        lock->locked = 0;
        _spinlock_unlock(lock);            // 原子遞增釋放
    }
    return 1;
}

致命場景重現

  1. 進程 A 進入 mm_do_lock,成功獲取鎖 (lock->lock == 0,設定 pid=A, locked=1)。
  2. 進程 A 執行 require_once 載入腳本,編譯緩存操作耗時較長。
  3. 超過 5 秒,PHP-FPM Master 發送 SIGTERM 強制殺死進程 A。
  4. 關鍵點:進程 A 被強殺,**無機會執行 mm_do_unlock**,鎖狀態永久保持 locked=1, pid=A。
  5. 進程 B、C、D... 陸續接收請求,嘗試 require_once,進入 mm_do_lock。
  6. 發現 lock->lock != 0 (實際為負數,因原子遞減未配對遞增),進入無限 while(1) 自旋 + sched_yield()。
  7. 所有工作進程 100% CPU 空轉,無法處理任何請求。

GDB 現場鑑證確認

(gdb) p *mm->lock
$8 = {lock = 4294966693, pid = 21775, locked = 1}
  • lock = 4294966693 (即 -2903,證明多次遞減未遞增)。
  • pid = 21775:持有鎖的進程 ID,早已不存在 (被強殺)。
  • locked = 1:鎖定狀態。

觸發條件總結

  1. 請求執行時間極長,觸發 request_terminate_timeout 強殺。
  2. 強殺發生在 **eAccelerator 持有鎖期間** (通常為 require/include 編譯緩存階段)。
  3. 使用 autoload 或運行時 require 增加持有鎖機率。

解決方案與最佳實踐

方案一:調大或移除 request_terminate_timeout (推薦)

; php-fpm.conf
request_terminate_timeout = 300s ; 或 0 (關閉,交由應用層控制)
  • 超時判斷下沉至應用層 (如 Laravel max_execution_time、自定義中間件)。
  • PHP-FPM 僅作「最後防線」(如 5 分鐘),避免頻繁強殺。

方案二:棄用 eAccelerator,改用 Zend OPcache (PHP 5.5+ 內建)

  • OPcache 為 PHP 核心擴展,鎖機制更健壯,無共享記憶體自旋鎖孤立問題。
  • 效能更優、維護更活躍、支援 JIT (PHP 8+)。

方案三:Autoload 閾值保護 (臨時緩解)

function __autoload($class) {
    if (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'] > 4.5) { // 接近 5 秒
        exit('Autoload timeout protection'); // 主動退出而非被強殺
    }
    // 正常載入邏輯
}

方案四:預先載入所有類別 (生產環境)

  • 使用 Composer classmap 或 optimize-autoloader。
  • 請求啟動階段 (啟動腳本) 完成所有 require,運行期零載入,零鎖爭用。

深度教訓

「在共享記憶體中實現的進程間鎖,若持有者可能被外部非協作式終止,**必須**有健壯的鎖恢復機制 (如 Robust Mutex / pthread_mutex_robust)。eAccelerator 缺乏此機制,是其架構缺陷。」

  • 現代 PHP (5.5+) 應全面遷移至 OPcache。
  • 超時控制應由應用層負責,基礎設施層僅提供兜底保護。
  • 監控指標:request_terminate_timeout 觸發率、PHP-FPM 子進程存活時間分布。

此案例是典型的「基礎設施層鎖機制與進程管理策略不匹配」導致的災難性故障,值得所有 PHP 架構師深思。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved