Мониторинг планировщика задач 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";
}