在生產環境中,Nginx、PHP-FPM、Worker 等長期運行的進程可能因程式錯誤、資源耗盡、收到信號等原因意外退出。傳統做法依賴 systemd、supervisord 或 monit 等外部工具,但若環境受限(如無 root 權限、不想引入額外依賴),一個純 PHP 實作的守護腳本同樣能勝任。
status > 0)立即拉起,無延遲。/proc/$pid/stat 確認進程存活,掛掉 1 秒內重啟。php supervisor.php /usr/local/nginx/sbin/nginx nobody &
參數說明:
supervisor.php)建議: 被守護程式**務必以前景(非 daemon)模式運行**,否則父進程無法正確捕獲退出狀態。
$pid = pcntl_fork();
if ($pid == 0) {
// 子進程:替換映像執行目標程式
pcntl_exec($path, $args, $_SERVER);
write_log("execute file failed");
exit(0);
}
使用 pcntl_fork() 建立子進程,子進程呼叫 pcntl_exec() 將自身替換為目標程式,保留環境變數。
$endpid = pcntl_waitpid($pid, $status, WUNTRACED);
if ($status == 0) {
// 可能是 daemon 化,檢查 /proc/$pid/stat 是否存在
if (file_exists("/proc/" . ($pid+1) . "/stat")) {
// 進程存活,進入心跳檢測迴圈
for (;;) {
if (file_exists("/proc/" . ($pid+1) . "/stat")) {
usleep(1000000); // 1 秒
} else {
write_log("program die");
break; // 跳出心跳迴圈,觸發重啟邏輯
}
}
} else {
write_log("program start failed");
exit(0);
}
} elseif ($status > 0) {
// 非 daemon 異常退出,直接記錄並準備重啟
write_log("program die");
}
function chk_need_start(&$times_arr, $duration, $max_times) {
// 清理超過時間窗口的記錄
foreach ($times_arr as $k => $v) {
if ($k < time() - $duration) unset($times_arr[$k]);
}
// 判斷是否超過閾值
if (count($times_arr) >= $max_times) return false;
$times_arr[time()] = 1;
return true;
}
以時間戳為鍵記錄每次重啟,滑動窗口算法實現「N 秒內最多 M 次」限制。
<?php
// author: tipboy@qq.com
$_retry_times = 5;
$_retry_times_duration = 60;
$_times_arr = array();
$args = $_SERVER['argv'];
if (count($args) < 3) {
write_log("args num error!");
write_log("usage: php supervisor.php /path/to/bin user [args...]");
exit();
}
$path = $args[1];
$username = $args[2];
// 移除前三個參數(腳本名、程式路徑、用戶)
array_shift($args); array_shift($args); array_shift($args);
while (true) {
$pid = pcntl_fork();
if ($pid == 0) {
pcntl_exec($path, $args, $_SERVER);
write_log("execute file failed");
exit(0);
} elseif ($pid > 0) {
$endpid = pcntl_waitpid($pid, $status, WUNTRACED);
if ($status == 0) {
if (file_exists("/proc/" . ($pid+1) . "/stat")) {
write_log("program start success");
for (;;) {
if (file_exists("/proc/" . ($pid+1) . "/stat")) {
usleep(1000000);
} else {
write_log("program die");
break;
}
}
} else {
write_log("program start failed");
exit(0);
}
} elseif ($status > 0) {
write_log("program die");
} else {
exit(0);
}
if (chk_need_start($_times_arr, $_retry_times_duration, $_retry_times)) {
continue;
} else {
write_log("retry limit exceeded, stop guardian");
break;
}
}
}
function write_log($msg) {
echo date('Y-m-d H:i:s') . ' ' . $msg . "\n";
}
function chk_need_start(&$times_arr, $duration, $max_times) {
foreach ($times_arr as $k => $v) {
if ($k < time() - $duration) unset($times_arr[$k]);
}
if (count($times_arr) >= $max_times) return false;
$times_arr[time()] = 1;
return true;
}
?>
/usr/local/bin/supervisor.php,賦予執行權限。nohup php supervisor.php ... > /var/log/supervisor.log 2>&1 & 後台運行並記錄日誌。crontab 或 systemd 實現開機自啟。logrotate 切割日誌。這個約 100 行的 PHP 腳本,利用 pcntl 擴展實現了企業級進程守護的核心功能:即時重啟、心跳監控、頻率限流。它零依賴、輕量級、易於定製,極適合中小型專案或受限環境下的服務穩定性保障。