Перейти к содержимому

Агенты изнутри - расписание, хиты, cron, блокировка

Разбираем агенты по слоям: где живёт очередь, как считается время следующего запуска и чем отличается выполнение на хите от выполнения по расписанию сервера. Заодно смотрим, почему однажды агент исчезает из очереди совсем сам.

Механика

Агент - это строка кода с расписанием, а не отдельный процесс. Платформа хранит её вместе с модулем, интервалом и временем следующего запуска в собственной таблице очереди.

Функция агента обязана вернуть платформе строку с командой своего следующего вызова. Возврат пустой строки означает осознанное самоудаление, а забытый возврат приводит к тому же результату молча.

Режимов расписания ровно два, и время следующего запуска они считают по-разному. Интервальный режим отсчитывает паузу от окончания прошлого запуска, а периодический держится жёсткого расписания и догоняет пропущенные слоты.

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

Периодические агенты на редко посещаемом сайте накапливают пропущенные запуски целыми пачками. Дождавшись посетителя, платформа отрабатывает несколько слотов подряд, и этот посетитель ждёт страницу дольше всех остальных.

Агенты платформы однопоточны по своей природе и выполняются строго друг за другом. Долгий агент блокируется от повторного запуска примерно на десять минут, а остальные агенты очереди ждут своей очереди.

Перевод очереди на расписание сервера снимает зависимость от посетителей сайта. Хитовый запуск выключают настройками ядра, а очередь начинает разбирать отдельный скрипт, запускаемый планировщиком.

Внутри агента нет ни пользовательского контекста, ни данных текущего запроса браузера. Ни текущего пользователя, ни адреса страницы там нет, и код, рассчитывающий на них, ведёт себя непредсказуемо.

Ошибка внутри одного агента останавливает разбор всей остальной очереди этого запуска. Следующие агенты в этом запуске не выполняются, и картина выглядит как «перестали работать все агенты сразу».

Фоновая задача - это не агент, а совсем другое средство отложенной работы. Она выполняется один раз сразу после ответа посетителю, тогда как агент возвращается по расписанию.

Шаги

  1. Прочитать очередь агентов и посмотреть время следующего запуска у каждого из них.
  2. Определить режим каждого агента: интервал от окончания работы или жёсткое расписание.
  3. Понять, где выполняется очередь: на хитах посетителей или по расписанию сервера.
  4. Найти агенты, которые исчезли, и проверить возврат строки в их функциях.
  5. Развести тяжёлые задачи по времени, чтобы очередь не упиралась в один агент.

Код

Читаем очередь агентов:

$res = CAgent::GetList(['NEXT_EXEC' => 'ASC'], []);
while ($row = $res->Fetch()) {
printf("%-40s %s интервал=%s период=%s активен=%s\n", $row['NAME'],
$row['NEXT_EXEC'], $row['AGENT_INTERVAL'], $row['IS_PERIOD'], $row['ACTIVE']);
}
// давнее время следующего запуска означает стоящую очередь, а не поломку агента
// ACTIVE = N встречается после ручного отключения агента в административной части

Имя агента в очереди - это буквально строка вызова его кода. По ней агент и удаляется, поэтому регистрация и удаление обязаны совпадать до последнего символа.

Время следующего запуска остаётся главным показателем здоровья всей очереди агентов проекта. Если оно в прошлом у половины строк, разбирают не сами агенты, а способ их выполнения на проекте.

Регистрируем агент с понятным расписанием:

CAgent::AddAgent('\Local\Job\Sync::run();', 'main', 'N', 3600, '', 'Y',
ConvertTimeStamp(time() + 60, 'FULL'));
// третий аргумент - периодичность: N считает паузу от окончания прошлого запуска
// пятый и шестой - проверка даты и активность, седьмой - время первого запуска

Возвращаем строку перезапуска:

public static function run(): string
{
self::doWork();
return '\Local\Job\Sync::run();'; // без этой строки агент исчезнет после первого раза
}
// пустая строка вместо вызова - это осознанное самоудаление агента
// исключение внутри метода оборвёт разбор очереди на этом же месте

Пересоздаём агент правильно:

CAgent::RemoveAgent('\Local\Job\Sync::run();', 'main'); // параметры совпадают с регистрацией
CAgent::AddAgent('\Local\Job\Sync::run();', 'main', 'N', 900);
// повторное добавление без удаления даёт два агента с одинаковым вызовом
// имя модуля в удалении обязано совпадать с именем при регистрации агента

Переводим очередь на расписание сервера:

\Bitrix\Main\Config\Option::set('main', 'agents_use_crontab', 'N');
\Bitrix\Main\Config\Option::set('main', 'check_agents', 'N'); // хитовый запуск выключен
// дальше очередь разбирает отдельный скрипт, который зовёт CAgent::CheckAgents()
// в файле dbconn.php при этом объявляют константу поддержки запуска из консоли

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

Смотрим строку планировщика:

Окно терминала
crontab -l | grep -i cron_events
*/1 * * * * /usr/bin/php -f /home/bitrix/www/bitrix/php_interface/cron_events.php >/dev/null 2>&1
# консольный PHP берут тот же, что и у веб-сервера, иначе разойдутся права на кэш

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

Отделяем разовую работу от расписания:

\Bitrix\Main\Application::getInstance()->addBackgroundJob(
[\Local\Job\Report::class, 'build'], [$orderId]);
// фоновая задача выполнится один раз после ответа и расписания не имеет

Разделение простое и работает почти всегда. Разовая доработка после ответа принадлежит фоновой задаче, а всё повторяющееся по времени остаётся за агентом с понятным интервалом.

Ограничения

Агент не имеет ни пользовательского контекста, ни привычного окружения запроса. Проверки прав и обращения к текущему пользователю внутри него бессмысленны и дают разный результат от запуска к запуску.

Долгий агент держит всю очередь платформы на себе до своего завершения. Пока он работает, остальные ждут, а через десять минут платформа считает его зависшим и блокирует повторный запуск.

Периодический режим расписания догоняет все пропущенные слоты подряд без пауз. На редко посещаемом сайте это оборачивается пачкой запусков подряд, а не аккуратным расписанием.

Хитовый запуск очереди полностью зависит от текущей посещаемости конкретного сайта. Ночная выгрузка, поставленная агентом на три часа, выполнится в момент первого утреннего визита.

Фоновая задача выполняется без всяких гарантий: аварийное завершение запроса её теряет. При аварийном завершении запроса она пропадает, поэтому критичные операции ставят агентом или очередью.

Типичные проблемы

Агент выполнился один раз и пропал из списка.

Функция не вернула строку своего следующего вызова, и платформа удалила агента. Возврат строки вызова обязателен для любого агента, который должен повторяться дальше.

В очереди два одинаковых агента.

Агент добавили повторно, не удалив прежнюю регистрацию с теми же параметрами. Пересоздание агента всегда идёт парой: сначала удаление прежнего, а потом добавление нового.

После перевода на расписание агенты встали совсем.

Хитовый запуск выключен настройками, а задание планировщика не создано или зовёт другой файл. Проверяют строку планировщика и путь к тому скрипту, который разбирает очередь.

Один посетитель ждёт страницу несколько секунд.

На его хите пролог страницы отрабатывает подряд все накопившиеся периодические агенты. Тяжёлые задачи переводят на расписание сервера, а периодичность у них отключают.

Часть агентов не выполняется без видимой причины.

Первый в очереди агент падает с ошибкой и обрывает разбор остальных. Ошибку ищут в журнале, а тяжёлую логику оборачивают в перехват исключений.

Агент работает не от того пользователя.

В агенте нет пользовательского контекста, а код рассчитывает на текущего пользователя. Идентификатор нужного пользователя передают прямо в параметрах вызова самого агента.

Частые вопросы

Чем интервал отличается от периодичности?

Интервал отсчитывается от окончания прошлого запуска, периодичность держится жёсткого расписания. Второй режим догоняет пропущенные слоты и на редких хитах даёт пачку запусков.

Почему агент исчез из списка?

Его функция не вернула строку следующего вызова, и платформа считает работу законченной. Возврат пустой строки означает то же самое, но осознанно.

Обязательно ли переводить агенты на расписание?

Для тяжёлых и чувствительных ко времени задач - да, иначе они зависят от посещаемости. Лёгкие агенты спокойно живут на хитах и ничего не ломают.

Можно ли запускать агенты параллельно?

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

Когда брать фоновую задачу вместо агента?

Когда работу нужно доделать один раз сразу после ответа посетителю. Регулярные задачи по расписанию остаются за агентами.

Смежное

Первоисточник