Задачи по расписанию: почему они молчат
Задача по расписанию — это единственный вид рабочей нагрузки, которая может перестать выполняться, и об этом никто не узнает. Пользовательский запрос завершится с ошибкой, API вернёт код 500, сервис упадёт — и кто-то это заметит. Задача в кроне молчит. Она запускается в пустоте: нет наблюдателя, нет ожидания результата, нет немедленной обратной связи. Если она перестала работать, об этом узнают не сразу, а когда узнают — окажется, что данных для разбора недостаточно.
Типичный сценарий: инженер добавляет задачу в крон, проверяет, что она запустилась, и забывает о ней. Через несколько месяцев задача перестаёт выполняться. Никто этого не замечает, пока не случится что-то заметное: не обновится справочник, не очистится кэш, не отправится отчёт. К этому моменту уже непонятно, когда задача перестала работать, почему, и что происходило в промежутке.
Первое, что пробуют — и почему это не помогает
Когда задача перестаёт выполняться, первое, что делают — запускают её вручную. Это кажется логичным: если задача работает вручную, значит, проблема в кроне. Но это тупик.
Запуск вручную работает в окружении пользователя. В нём есть все переменные среды, текущая директория, права доступа, которые задаёт оболочка пользователя. Крон запускает задачу в другом окружении: без переменных из .bashrc, без текущей директории, с правами пользователя, от имени которого запущен крон. Поэтому задача, которая работает вручную, может не работать в кроне — и наоборот.
Кроме того, запуск вручную не проверяет отработку задачи. Он проверяет только запуск. Задача может запуститься, но не завершиться, или завершиться с ошибкой, или завершиться успешно, но не сделать то, что от неё ожидалось. Запуск вручную не покажет этого.
Что значит «работает»
«Запустилось» и «отработало» — разные утверждения. Запуск задачи — это старт процесса. Отработка — это завершение с нужным результатом. Между ними может быть многое: ошибка в коде, нехватка прав, отсутствие файла, переполнение диска, взаимная блокировка процессов.
Проверить запуск легко: достаточно посмотреть в лог крона или запустить команду вручную. Проверить отработку сложнее: нужно убедиться, что задача не только завершилась, но и сделала то, что от неё ожидалось. Например, если задача должна отправить письмо, то проверка запуска покажет, что процесс стартовал, но не покажет, что письмо ушло.
Окружение задачи: что отличается от пользовательского
Задача по расписанию выполняется в окружении, которое отличается от окружения пользователя. Это приводит к типичным ошибкам:
- Переменные среды. Задача может зависеть от переменных, которые заданы в
.bashrcили.zshrc, но не заданы в окружении крона. Например,PATHможет быть короче, и команда не найдётся. Или задача может использовать переменнуюJAVA_HOME, которая не задана в окружении крона. - Относительные пути. Задача может использовать относительные пути, которые работают в текущей директории пользователя, но не работают в окружении крона. Например,
./script.shне найдётся, если крон запускает задачу из корневой директории. - Права на файлы. Задача может пытаться записать в файл, на который у неё нет прав. Например, если задача запускается от имени пользователя, но файл принадлежит другому пользователю или имеет права
644. - Вывод. Задача может выводить данные в стандартный вывод или стандартный поток ошибок, но крон не сохраняет этот вывод, если его не перенаправить в файл. Если задача падает, вывод теряется.
- Часовой пояс. Задача может зависеть от часового пояса, который отличается от часового пояса сервера. Например, если задача должна запускаться в полночь по местному времени, но сервер находится в другом часовом поясе, она запустится не вовремя.
Перекрытие запусков: почему задачи мешают друг другу
Задача по расписанию может выполняться дольше, чем интервал между запусками. Например, если задача запускается каждую минуту, но выполняется две минуты, то следующая задача запустится, пока предыдущая ещё работает. Это может привести к конфликтам: задачи могут мешать друг другу, перезаписывать файлы, блокировать ресурсы.
Чтобы избежать этого, задача должна использовать замок — файл или семафор, который указывает, что задача уже выполняется. Замок должен освобождаться при завершении задачи, даже если она завершилась с ошибкой. Если замок не освободится, задача перестанет запускаться, пока замок не удалят вручную.
Типичная ошибка: замок не освобождается при падении задачи. Например, если задача завершилась с ошибкой, но не удалила замок, следующая задача не запустится. Чтобы этого избежать, замок должен освобождаться в блоке finally или с помощью обработчика сигналов.
Отчёт о выполнении: как понять, что задача отработала
Задача по расписанию должна оставлять след о своём выполнении. Это может быть запись в логе, файл с результатом, запись в базе данных. Проверка должна смотреть на этот след, а не на ошибки. Если задачи нет в логе, значит, она не выполнилась — даже если ошибок не было.
Оповещение должно срабатывать по отсутствию следа, а не по наличию ошибки. Если задача не выполнилась, но не оставила следа, оповещение должно сработать. Если задача выполнилась с ошибкой, но оставила след, оповещение не должно срабатывать — ошибка может быть ожидаемой.
Например, задача может завершаться с ошибкой, если нет данных для обработки. Это нормально, и оповещение не должно срабатывать. Но если задача не выполнилась вообще, оповещение должно сработать.
Что писать в журнал: как сделать разбор быстрым
Журнал задачи должен содержать достаточно информации для быстрого разбора. Если в журнале только факт запуска и завершения, разбор может занять вечер. Если в журнале есть подробности, разбор займёт минуту.
Вот что стоит писать в журнал:
- Время запуска и завершения. Это позволяет понять, сколько времени заняла задача. Если задача выполняется дольше обычного, это может указывать на проблему.
- Аргументы и параметры. Это позволяет воспроизвести выполнение задачи. Если задача запускается с разными параметрами, это должно быть видно в журнале.
- Результат выполнения. Это позволяет понять, что сделала задача. Например, если задача должна отправить письмо, в журнале должно быть указано, сколько писем отправлено.
- Ошибки и предупреждения. Это позволяет понять, что пошло не так. Если задача завершилась с ошибкой, в журнале должна быть указана причина.
- Контекст выполнения. Например, текущая директория, переменные среды, права доступа. Это позволяет понять, в каком окружении выполнялась задача.
Позиция редакции
Задачи по расписанию — это чёрные ящики, которые работают в пустоте. Их отказ не виден сразу, и когда он становится заметным, данных для разбора уже недостаточно. Чтобы избежать этого, нужно:
- Проверять не запуск, а отработку задачи. Запуск вручную не показывает, что задача отработала.
- Убедиться, что окружение задачи содержит всё необходимое: переменные среды, пути, права доступа.
- Использовать замок, чтобы избежать перекрытия запусков. Замок должен освобождаться при падении задачи.
- Оставлять след о выполнении задачи и оповещать по отсутствию этого следа. Отсутствие следа — это отказ.
- Писать в журнал достаточно информации для быстрого разбора: время, аргументы, результат, ошибки, контекст.
Эти меры не гарантируют, что задача не перестанет работать, но они гарантируют, что об этом узнают быстро и смогут быстро разобраться.
Позиция неверна, если задачи по расписанию не критичны для системы, и их отказ не приводит к заметным последствиям. В этом случае можно не тратить время на мониторинг и разбор, а просто периодически проверять их работу вручную.
Как проверить, что задача работает
Чтобы проверить, что задача по расписанию работает, нужно:
- Запустить задачу вручную и убедиться, что она отработала. Это не заменяет проверку в окружении крона, но показывает, что задача в принципе работает.
- Проверить, что задача оставляет след о своём выполнении. Это может быть запись в логе, файл с результатом, запись в базе данных.
- Проверить, что оповещение срабатывает по отсутствию этого следа. Если задачи нет в логе, оповещение должно сработать.
- Проверить, что журнал задачи содержит достаточно информации для разбора: время, аргументы, результат, ошибки, контекст.
Если всё это работает, задача по расписанию будет заметна, даже если она перестанет выполняться.