Мониторинг планировщика задач PHP: диагностика и отслеживание

Раздел: Администрирование сервера -> Планировщик задач

Основное решение: блокировка с файлом и отметками времени

Как с помощью PHP надёжно определить, выполняется ли cron задача прямо сейчас и когда она запускалась в последний раз?

Наиболее эффективный метод - использование файла блокировки (flock) в сочетании с записью временных меток начала и окончания. При старте скрипт проверяет существование блокировки: если она уже установлена, задача считается выполняющейся (или зависшей). Если блокировку удаётся получить, скрипт записывает PID и текущий timestamp в файл, а при завершении - обновляет время последнего успешного выполнения и снимает блокировку.


// lock.php
$lockFile = '/tmp/my_cron.lock';
$fp = fopen($lockFile, 'c+');

if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
    // Не удалось получить блокировку - задача уже выполняется
    echo "Задача уже запущена или зависла.\n";
    exit(1);
}

// PID и время старта
$pid = getmypid();
$startTime = time();
ftruncate($fp, 0);
fwrite($fp, json_encode(['pid' => $pid, 'start' => $startTime]));
fflush($fp);

// Долгая работа
echo "Задача запущена (PID $pid)...\n";
sleep(30);

// Обновляем время последнего завершения
$lastRunFile = '/tmp/my_cron_last_run';
file_put_contents($lastRunFile, time());

// Снимаем блокировку
flock($fp, LOCK_UN);
fclose($fp);
echo "Задача завершена.\n";

Cron php определить (определение/проверка выполнения cron задач в php)

Для проверки статуса из другого скрипта или веб-интерфейса:


// check.php
$lockFile = '/tmp/my_cron.lock';
$lastRunFile = '/tmp/my_cron_last_run';

$status = [];
if (file_exists($lockFile)) {
    $data = json_decode(file_get_contents($lockFile), true);
    $pid = $data['pid'] ?? null;
    // Дополнительно можно проверить, жив ли процесс (см. варианты)
    $status['running'] = true;
    $status['started_at'] = $data['start'] ?? null;
} else {
    $status['running'] = false;
}
$status['last_finished'] = file_exists($lastRunFile) ? (int)file_get_contents($lastRunFile) : null;
echo json_encode($status, JSON_PRETTY_PRINT);

Cron events php (события cron в php)

{
    "running": true,
    "started_at": 1712345678,
    "last_finished": 1712345600
}

Типичные ошибки и их решение

  • Блокировка не снимается при kill -9: файл остаётся, а процесс пропадает. Решение - проверять PID по /proc или через posix_kill(p, 0); если процесс не существует - удалять блокировку.
  • Гонка при одновременном старте: flock атомарен, но если использовать несколько файлов - возможны проблемы. Рекомендуется всегда открывать один и тот же файл.
  • Неверные права доступа: файл должен быть доступен тому же пользователю, что и cron. Создавайте его в /tmp или в каталоге проекта с chmod.

Цели использования

Подходит для простых сценариев, когда нужно предотвратить повторный запуск и узнать время последнего выполнения. Не требует внешних сервисов, минимальная нагрузка.

Альтернативные подходы

Как организовать мониторинг через базу данных?

В таблицу cron_tasks добавляются записи с полями: task_name, start_time, end_time, status. При запуске скрипт обновляет строку, при завершении - меняет статус. Проверка выполняется запросом к БД.


// db_cron.php
$db = new PDO('mysql:host=localhost;dbname=cron', 'user', 'pass');
$taskName = 'send_emails';

// Старт
$stmt = $db->prepare('INSERT INTO cron_tasks (task_name, start_time, status) VALUES (?, NOW(), "running") ON DUPLICATE KEY UPDATE start_time=NOW(), status="running"');
$stmt->execute([$taskName]);

// Работа
// ...

// Завершение
$db->prepare('UPDATE cron_tasks SET end_time=NOW(), status="done" WHERE task_name=?')->execute([$taskName]);

Проблемы

При аварийном завершении записи остаются в статусе running. Нужен дополнительный механизм (heartbeat или тайм-аут). Также повышается нагрузка на БД при частых запросах.

Как использовать Redis для лёгкой проверки активности?

Хранить ключ с TTL, например cron:task:send_emails со значением 1 и временем жизни, равным интервалу запуска. Если ключ истёк - задача не выполнялась вовремя.


// redis_cron.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$taskKey = 'cron:send_emails';

// Каждую минуту обновляем ключ
$redis->setex($taskKey, 90, 1);  // TTL 90 секунд (интервал + запас)
// ...

Проверка:


// check.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
if ($redis->exists('cron:send_emails')) {
    echo "Задача активна или недавно выполнялась.\n";
} else {
    echo "Возможен пропуск выполнения.\n";
}

Проблемы

Если задача запускается раз в час, TTL должен быть больше часа - это увеличивает время неопределённости. Нет точной информации о длительности выполнения.

Как проверить выполнение через системные вызовы (ps aux)?

С помощью exec('ps aux | grep [cron_script]') определяется, запущен ли процесс. Фильтр по имени скрипта или аргументу.


// ps_check.php
$scriptName = 'my_cron_script.php';
$output = [];
exec("ps aux | grep '{$scriptName}' | grep -v grep", $output, $exitCode);

if (count($output) > 0) {
    echo "Процесс найден:\n";
    print_r($output);
} else {
    echo "Процесс не выполняется.\n";
}

Проблемы

  • Безопасность: передача пользовательских данных в команду может привести к инъекции.
  • Зависимость от ОС (Linux). На Windows команда другая.
  • Разные процессы с похожим именем. Лучше добавлять уникальный аргумент.

Как отследить выполнение по логам cron (syslog)?

Если на сервере включено журналирование cron (rsyslog), можно читать строки из /var/log/syslog или /var/log/cron с помощью PHP. Фильтр по PID или имени скрипта.


// log_check.php
$logFile = '/var/log/syslog';
$handle = fopen($logFile, 'r');
if (!$handle) die("Нет доступа к логам cron");

$pattern = '/CRON.*my_cron_script/';
while (($line = fgets($handle)) !== false) {
    if (preg_match($pattern, $line)) {
        echo "Найдено: $line";
    }
}
fclose($handle);

Проблемы

Лог-файл может быть огромным, потребуется оптимизация (чтение только последних строк). Права на чтение логов обычно ограничены.

Расширенные примеры проверки выполнения cron задач

1. Комбинированная блокировка с проверкой PID через /proc

Пример

class CronLock {
    private string $lockFile;
    private $handle;

    public function __construct(string $taskName) {
        $this->lockFile = sys_get_temp_dir() . '/' . md5($taskName) . '.lock';
    }

    public function acquire(): bool {
        $this->handle = fopen($this->lockFile, 'c+');
        if (!$this->handle) return false;
        
        if (!flock($this->handle, LOCK_EX | LOCK_NB)) {
            // Проверяем, жив ли процесс, который держит блокировку
            $data = json_decode(fread($this->handle, 1024), true);
            $pid = $data['pid'] ?? 0;
            if ($pid && !$this->isProcessAlive($pid)) {
                // Процесс мёртв, принудительно снимаем блокировку
                flock($this->handle, LOCK_UN);
                fclose($this->handle);
                $this->handle = fopen($this->lockFile, 'w+');
                return flock($this->handle, LOCK_EX | LOCK_NB);
            }
            return false;
        }
        $pid = getmypid();
        ftruncate($this->handle, 0);
        fwrite($this->handle, json_encode(['pid' => $pid, 'time' => time()]));
        fflush($this->handle);
        return true;
    }

    private function isProcessAlive(int $pid): bool {
        // Проверка существования процесса в /proc
        return file_exists("/proc/{$pid}");
    }

    public function release(): void {
        if ($this->handle) {
            // Обновляем время последнего выполнения
            file_put_contents($this->lockFile . '.last', time());
            flock($this->handle, LOCK_UN);
            fclose($this->handle);
        }
    }

    public function getLastRun(): ?int {
        $file = $this->lockFile . '.last';
        return file_exists($file) ? (int)file_get_contents($file) : null;
    }
}

// Использование в cron
$lock = new CronLock('send_emails');
if (!$lock->acquire()) {
    die("Задача уже выполняется или зависла.");
}
// ... работа ...
$lock->release();
echo "Выполнение завершено.";
Выполнение завершено.

2. Мониторинг с помощью очередей (RabbitMQ) и heartbeat

Каждая задача при запуске публикует сообщение в очередь cron:heartbeats. Отдельный потребитель отслеживает, приходят ли сообщения вовремя. Если heartbeat отсутствует дольше заданного интервала - генерируется alert.

Пример

// heartbeat_producer.php (запускается из cron каждую минуту)
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('cron:heartbeats', false, true, false, false);

$taskName = 'send_emails';
$timestamp = time();
$data = json_encode(['task' => $taskName, 'time' => $timestamp]);
$msg = new AMQPMessage($data, ['delivery_mode' => 2]);
$channel->basic_publish($msg, '', 'cron:heartbeats');

$channel->close();
$connection->close();
echo "Heartbeat отправлен для {$taskName}.";
Пример

// heart_checker.php (демон, запускается постоянно)
// Потребитель анализирует время последнего heartbeat
// Если задача не отзывается более 2 минут (120 сек) - предупреждение

// ... consumer logic ...
$lastHeartbeat = getLastHeartbeatFromDB('send_emails'); // предполагаем хранение
if ($lastHeartbeat && (time() - $lastHeartbeat > 120)) {
    echo "Пропуск heartbeat: задача send_emails не отзывается!";
    // Отправить уведомление
}

3. Проверка через INOTIFY или аналоги для файловых блокировок

Использование расширения inotify для немедленного получения уведомлений об изменении статуса блокировочного файла. Не подходит для проверки текущего состояния, но полезно для логов.

Пример

// inotify_cron.php
$inotify = inotify_init();
stream_set_blocking($inotify, true);
$watch = inotify_add_watch($inotify, '/tmp/my_cron.lock', IN_CLOSE_WRITE | IN_MODIFY);

echo "Ожидание изменений файла блокировки...\n";
$events = inotify_read($inotify);
foreach ($events as $event) {
    echo "Событие: " . $event['mask'] . "\n";
    if ($event['mask'] & IN_CLOSE_WRITE) {
        echo "Файл записан: возможно, задача завершилась.\n";
    }
}

4. Отправка уведомлений при пропуске выполнения

Сравнение последнего времени завершения с текущим. Если разница превышает допустимый интервал - отправляется письмо или сообщение в Telegram.

Пример

// notify_if_missed.php
$taskName = 'send_emails';
$lockFile = '/tmp/' . $taskName . '_last_run';
$interval = 120; // ожидаемый интервал в секундах

if (file_exists($lockFile)) {
    $lastRun = (int)file_get_contents($lockFile);
    $diff = time() - $lastRun;
    if ($diff > $interval + 30) { // +30 секунд запаса
        $msg = "Задача {$taskName} не выполнялась уже {$diff} секунд. Последний запуск: " . date('Y-m-d H:i:s', $lastRun);
        // mail() или Telegram API
        echo $msg . "\n";
    } else {
        echo "Всё в порядке. Последний запуск: " . date('Y-m-d H:i:s', $lastRun) . "\n";
    }
} else {
    echo "Нет информации о последнем запуске.\n";
}
Всё в порядке. Последний запуск: 2024-08-05 12:34:56

5. Использование systemd timers и journalctl для мониторинга

Если cron задачи перенесены в systemd таймеры, проверять статус можно через systemctl is-active и читать логи journalctl.

Пример

// systemd_check.php
$serviceName = 'my-cron-task.service';
exec("systemctl is-active {$serviceName}", $output, $code);
echo "Статус service: " . trim(implode('', $output)) . "\n";
// Вывод: active или inactive

// Получение времени последнего успешного запуска
$days = 1;
exec("journalctl -u {$serviceName} --since '{$days} day ago' --output=json --no-pager 2>/dev/null | tail -1", $jsonLines);
$lastLog = json_decode(implode('', $jsonLines), true);
if ($lastLog) {
    echo "Последний запуск (лог): " . ($lastLog['__REALTIME_TIMESTAMP'] ?? 'неизвестно') . "\n";
}

Определение/проверка выполнения cron задач в PHP - comments

En
Cron php определить (php)