生產環境 PHP-FPM 叢集頻繁出現「過載後永久不可用」現象:一旦負載飆升觸發 request_terminate_timeout 強制終止進程,後續所有請求 (甚至空 PHP 檔案) 導致 CPU 100% 狂轉,負載均衡降權策略完全失效,必須重啟 PHP-FPM 才能恢復。經深度排查,根因為 eAccelerator 共享記憶體自旋鎖在進程被強殺時未釋放,導致所有工作進程永久阻塞於鎖獲取迴圈。
request_terminate_timeout = 5s (請求超時強殺)。WARNING: [pool www] child 12345, script '...' executing too slow。) 導致 PHP-FPM 子進程 CPU 100%。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;
}
mm_do_lock,成功獲取鎖 (lock->lock == 0,設定 pid=A, locked=1)。require_once 載入腳本,編譯緩存操作耗時較長。mm_do_unlock**,鎖狀態永久保持 locked=1, pid=A。require_once,進入 mm_do_lock。lock->lock != 0 (實際為負數,因原子遞減未配對遞增),進入無限 while(1) 自旋 + sched_yield()。(gdb) p *mm->lock
$8 = {lock = 4294966693, pid = 21775, locked = 1}
lock = 4294966693 (即 -2903,證明多次遞減未遞增)。pid = 21775:持有鎖的進程 ID,早已不存在 (被強殺)。locked = 1:鎖定狀態。request_terminate_timeout 強殺。require/include 編譯緩存階段)。autoload 或運行時 require 增加持有鎖機率。; php-fpm.conf
request_terminate_timeout = 300s ; 或 0 (關閉,交由應用層控制)
max_execution_time、自定義中間件)。function __autoload($class) {
if (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'] > 4.5) { // 接近 5 秒
exit('Autoload timeout protection'); // 主動退出而非被強殺
}
// 正常載入邏輯
}
classmap 或 optimize-autoloader。require,運行期零載入,零鎖爭用。「在共享記憶體中實現的進程間鎖,若持有者可能被外部非協作式終止,**必須**有健壯的鎖恢復機制 (如 Robust Mutex / pthread_mutex_robust)。eAccelerator 缺乏此機制,是其架構缺陷。」
request_terminate_timeout 觸發率、PHP-FPM 子進程存活時間分布。此案例是典型的「基礎設施層鎖機制與進程管理策略不匹配」導致的災難性故障,值得所有 PHP 架構師深思。