Каждый системный администратор знает это чувство: скрипт работает идеально при запуске из командной строки, но стоит добавить его в crontab - и начинается настоящее молчание. Никаких сообщений, никаких логов, словно задача растворилась в цифровом эфире. Что происходит за кулисами этой технической магии?
За годы работы я понял: cron-скрипты ведут себя как капризные актеры, которые блестяще играют на репетиции, но теряются на настоящей сцене. Разница лишь в том, что "сцена" cron кардинально отличается от привычной командной строки. Это как если бы артиста внезапно лишили декораций, света и даже сценария.
Среда обитания невидимок
Cron живет в спартанских условиях, которые многих застают врасплох. Если ваша интерактивная оболочка напоминает уютную квартиру со всеми удобствами, то среда cron больше похожа на пустую комнату с голыми стенами. Здесь нет привычных переменных окружения, загруженных из .bashrc
или .profile
, минимальный PATH содержит только /usr/bin:/bin
, а рабочая директория может оказаться совсем не той, которую вы ожидаете.
Помню историю коллеги, который три недели искал причину молчания backup-скрипта. Команда rsync
отлично работала в терминале, но через cron упорно не запускалась. Проблема оказалась банальной: полный путь к утилите был /usr/bin/rsync
, а в скрипте использовалось просто rsync
. В ограниченной среде cron программа просто не находилась. Это типичная ловушка - мы привыкаем, что система "знает", где искать команды, но cron работает почти с нуля.
Переменная PATH в cron обычно содержит всего /usr/bin:/bin
. Представьте, что вы привыкли ездить по городу с навигатором, а тут внезапно остались только с парой основных улиц на карте. Любая программа, установленная в /usr/local/bin
, /opt/bin
или пользовательские директории, становится невидимой для cron. Python может находиться в /usr/local/bin/python3
, Node.js в /usr/local/bin/node
, а ваши собственные утилиты и вовсе в домашней папке.
Какие еще переменные "исчезают" в cron? Практически все пользовательские настройки. Остаются лишь базовые: HOME
(домашняя директория), LOGNAME
(имя пользователя), PATH
(урезанный до минимума) и SHELL
(обычно /bin/sh
, а не ваш любимый bash). Исчезают настройки локали, часовых поясов, алиасы команд - все то, что делает работу в терминале комфортной.
Искусство чтения цифровых следов
Системные логи становятся детективными уликами в расследовании "тихих" ошибок. В Debian и Ubuntu основная информация попадает в /var/log/syslog
, в RHEL и CentOS - в /var/log/cron
. Но многие администраторы забывают включить детальное логирование cron, а потом удивляются отсутствию информации.
Базовый поиск записей cron выполняется командой, которая покажет последние 20 записей планировщика:
grep CRON /var/log/syslog | tail -n 20
Для систем семейства Red Hat путь к логам отличается, поэтому команда выглядит иначе:
grep CRON /var/log/cron | tail -n 20
Что показывают эти записи? Строки вида Apr 15 14:30:01 server CRON[12345]: (username) CMD (/path/to/script.sh)
подтверждают, что cron пытался запустить команду в указанное время. Если таких записей нет - проблема в самом задании или демоне cron не работает. Если записи есть, но результата нет - проблема глубже.
Записи об ошибках выглядят примерно так: CRON[12345]: (username) MAIL (mailed 1 byte of output; but got status 0x004b, #/bin/sh: command not found#)
. Здесь видно, что команда не найдена, что намекает на проблемы с путями.
Для мониторинга cron в реальном времени незаменима команда:
tail -f /var/log/syslog | grep CRON
Запустите её в отдельном терминальном окне перед тестированием задания, и вы увидите все попытки запуска немедленно. Это особенно удобно при отладке заданий с частым запуском.
Многие системы по умолчанию не создают отдельный лог для cron. Чтобы включить детальное логирование, добавьте в файл /etc/rsyslog.d/50-default.conf
строку:
cron.* /var/log/cron.log
После добавления перезапустите службу rsyslog:
sudo systemctl restart rsyslog
Теперь все события cron будут записываться в отдельный файл /var/log/cron.log
, что значительно упрощает диагностику.
Права доступа как фундамент стабильности
Файл может существовать, быть синтаксически верным, но без исполняемых прав cron его проигнорирует без единого слова. Это одна из самых частых причин молчания скриптов. Проверка прав доступа - первое, что нужно делать:
ls -l /path/to/script.sh
Результат должен показать что-то вроде -rwxr-xr-x
, где первый символ x
после rwx
означает право владельца на выполнение. Если вместо x
стоит дефис -
, файл не исполняемый. Исправляем это простой командой:
chmod +x /path/to/script.sh
Но права доступа - это не только исполняемость самого файла. Есть подводные камни, которые ловят даже опытных администраторов. Скрипт может лежать в директории, недоступной для cron. Например, если файл находится в /home/user/private/scripts/backup.sh
, а права на директорию private
установлены как 700
(только владелец), то cron, запущенный от имени другого пользователя или root, не сможет до него добраться.
Проверить полный путь доступа помогает команда:
namei -l /home/user/private/scripts/backup.sh
Она покажет права доступа для каждой части пути. Все директории в цепочке должны иметь право "прохода" (execute) для пользователя, от имени которого запускается cron.
Особое внимание требуют системные задания в /etc/cron.d/
и папках /etc/cron.daily/
, /etc/cron.hourly/
. Эти файлы должны принадлежать пользователю root и группе root:
sudo chown root:root /etc/cron.d/my-backup-job
Если владелец другой, система проигнорирует файл из соображений безопасности, не выдав при этом никакого предупреждения.
Перенаправление как мост между мирами
Молчание cron часто объясняется просто: весь вывод команд по умолчанию отправляется на почту владельцу задания. Если почтовая система не настроена (а в большинстве серверных установок она отключена), сообщения теряются безвозвратно. Это как кричать в пустоту - звук есть, но его никто не слышит.
Базовое решение - явное перенаправление всего вывода в файл:
* * * * * /path/to/script.sh >> /var/log/myscript.log 2>&1
Разберем эту конструкцию по частям. Символы >>
означают добавление в конец файла (append), в отличие от >
, которое перезаписывает файл полностью. Конструкция 2>&1
критически важна: цифра 2
обозначает поток ошибок (stderr), а 1
- стандартный вывод (stdout). Запись 2>&1
направляет ошибки туда же, куда идет обычный вывод. Без этого вы увидите результат работы скрипта, но пропустите все сообщения об ошибках.
Иногда полезно разделить потоки для более детальной диагностики:
* * * * * /path/to/script.sh > /var/log/script_output.log 2> /var/log/script_errors.log
Тогда штатная работа записывается в файл script_output.log
, а все проблемы и ошибки - в отдельный script_errors.log
. Это упрощает анализ: в одном файле видите, что скрипт делает правильно, в другом - что идет не так.
Альтернативный подход - отправка в системный журнал через утилиту logger:
* * * * * /path/to/script.sh | logger -t "backup-script"
Здесь все сообщения скрипта попадут в системный лог с тегом "backup-script", что позволяет легко их найти:
grep "backup-script" /var/log/syslog
Для критически важных заданий можно настроить email-уведомления, добавив в начало crontab переменную:
MAILTO=Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
* * * * * /path/to/important-script.sh
Но помните: это работает только при настроенном почтовом сервере на машине.
Симуляция спартанской среды
Как понять, сработает ли скрипт в условиях cron, не дожидаясь планового запуска? Нужно воссоздать аналогичную среду выполнения. Это как репетиция в тех же декорациях, где будет проходить настоящий спектакль.
Самый простой способ очистить окружение и запустить команду в "голой" среде:
env -i /bin/sh -c '/path/to/script.sh'
Команда env -i
полностью очищает все переменные окружения, создавая практически пустую среду. Параметр /bin/sh
использует базовую оболочку, точно как это делает cron по умолчанию. Если ваш скрипт падает в таких условиях с ошибкой "command not found" или "file not found", то он точно не сработает и в планировщике.
Более точная имитация учитывает конкретного пользователя и рабочую директорию:
sudo -u username env -i /bin/bash -c 'cd ~ && /path/to/script.sh'
Эта команда запускается от имени нужного пользователя (username
), очищает окружение (env -i
), переходит в домашнюю директорию (cd ~
) и только потом выполняет скрипт. Такой подход максимально близко воспроизводит условия выполнения cron-задания.
Продвинутый метод - временное сохранение реальной среды cron. Создайте тестовое задание:
* * * * * env > ~/cron-env.txt
Дождитесь его срабатывания (максимум минуту), затем изучите, какие именно переменные доступны в cron:
cat ~/cron-env.txt
Теперь можете загрузить эту среду в интерактивную сессию:
env -i $(cat ~/cron-env.txt | xargs) /bin/sh
Вы окажетесь в оболочке с точно теми же переменными, что использует cron. Здесь можете тестировать команды и скрипты в идентичных условиях.
Абсолютность
В мире cron относительные пути - источник 90% всех проблем. Это как давать указания "поверни направо у того дерева", не уточняя, от какого места начинать путь. Команда ./backup.sh
может не найти скрипт, потому что рабочая директория не та, которую вы предполагаете. Вызов python script.py
может запустить совершенно неожиданную версию интерпретатора или вообще ничего не найти.
Золотое правило cron: всегда используйте полные абсолютные пути ко всему - программам, скриптам, файлам данных, логам:
/usr/bin/python3 /home/user/scripts/backup.py --config /etc/backup/config.ini --output /var/backups/
Команда which
станет вашим лучшим другом для поиска точного расположения программ:
which python3
which mysql
which tar
which rsync
Каждая из этих команд покажет полный путь к исполняемому файлу. Например, which python3
может выдать /usr/bin/python3
или /usr/local/bin/python3
- и эта разница критична для cron.
Особое внимание заслуживает строка shebang в начале скрипта. Многие используют универсальную запись:
#!/usr/bin/env python3
Но в среде cron это может не сработать, потому что env
может находиться не в стандартном пути, или переменная PATH не содержит путь к python3. Надежнее указать прямой путь:
#!/usr/bin/python3
Предварительно проверив его существование командой:
ls -l /usr/bin/python3
То же самое касается bash-скриптов. Вместо #!/bin/bash
иногда путь может быть /usr/bin/bash
. Проверьте:
which bash
Для файлов конфигурации, логов, временных файлов - везде нужны абсолютные пути. Вместо config.ini
используйте /etc/myapp/config.ini
, вместо backup.log
- /var/log/backup.log
.
Диагностика по нарастающей
Отладка cron требует систематического подхода, начиная с простейших тестов и постепенно усложняя задачу. Это как настройка музыкального инструмента - сначала проверяете, издает ли он вообще звуки, потом настраиваете каждую струну.
Начните с элементарного теста - добавьте задание, которое просто записывает текущее время:
* * * * * date >> /tmp/cron-test.log
В отдельном терминале следите за файлом:
tail -f /tmp/cron-test.log
Если время обновляется каждую минуту - значит, демон cron работает исправно, и проблема не в нем. Это исключает самые базовые причины молчания.
Следующий шаг - проверка доступности вашего скрипта. Замените date
на простой вызов:
* * * * * /path/to/your/script.sh >> /tmp/cron-test.log 2>&1
Если в логе появились сообщения об ошибках вроде "Permission denied" или "No such file or directory", вы на правильном пути - проблема найдена.
Внутри самих скриптов добавляйте подробную отладочную информацию:
#!/bin/bash
echo "$(date): Script started by user $(whoami)" >> /var/log/debug.log
echo "Working directory: $(pwd)" >> /var/log/debug.log
echo "PATH: $PATH" >> /var/log/debug.log
echo "HOME: $HOME" >> /var/log/debug.log
# Проверка доступности внешних команд
which python3 >> /var/log/debug.log 2>&1
which mysql >> /var/log/debug.log 2>&1
# Основная логика скрипта здесь
echo "$(date): Script finished successfully" >> /var/log/debug.log
Такой подход покажет не только факт запуска скрипта, но и условия его выполнения. Часто проблема становится очевидной уже на этапе вывода переменных окружения.
Для скриптов, которые обрабатывают файлы, добавьте проверки существования:
if [ ! -f "/path/to/input/file.txt" ]; then
echo "$(date): Input file not found!" >> /var/log/debug.log
exit 1
fi
Это поможет отличить проблемы с правами от проблем с отсутствующими файлами.
Продвинутые техники укрощения
Некоторые проблемы требуют более изощренных решений. Блокировки предотвращают конфликты, когда предыдущий запуск скрипта еще не завершился, а новый уже начался. Представьте ситуацию: backup-скрипт должен запускаться каждые 30 минут, но большая база данных архивируется 45 минут. Без защиты получится несколько одновременных процессов, которые могут конфликтовать.
Утилита flock
элегантно решает эту проблему:
* * * * * /usr/bin/flock -n /tmp/script.lock /path/to/script.sh
Опция -n
означает "не ждать" - если блокировка уже существует (предыдущий процесс еще работает), новый запуск просто завершится без выполнения. Файл блокировки /tmp/script.lock
создается автоматически и удаляется по завершении процесса.
Более сложная блокировка с логированием попыток:
* * * * * /usr/bin/flock -n /tmp/script.lock -c '/path/to/script.sh' || echo "$(date): Previous instance still running" >> /var/log/cron-conflicts.log
Переменные окружения можно задать прямо в crontab, что часто проще, чем модифицировать каждый скрипт:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
SHELL=/bin/bash
LANG=en_US.UTF-8
TZ=America/New_York
MAILTO=Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
0 2 * * * /path/to/backup-script.sh
*/15 * * * * /path/to/monitoring-script.sh
Такой подход централизует настройки окружения для всех заданий пользователя.
Временные ограничения предотвращают "зависание" процессов, которые могут работать бесконечно из-за сетевых проблем или других внешних факторов:
* * * * * timeout 300 /path/to/potentially-long-script.sh
Скрипт будет принудительно завершен через 5 минут (300 секунд). Для критичных заданий это может спасти от накопления "зомби"-процессов.
Проверка перекрывающихся процессов внутри самого скрипта:
#!/bin/bash
SCRIPT_NAME=$(basename $0)
PID_FILE="/var/run/${SCRIPT_NAME}.pid"
# Проверка, не запущен ли уже процесс
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "$(date): Script already running with PID $OLD_PID"
exit 1
fi
fi
# Записываем свой PID
echo $$ > "$PID_FILE"
# Убираем PID-файл при завершении
trap "rm -f $PID_FILE" EXIT
# Основная логика скрипта здесь
Такая защита работает надежнее внешних блокировок, особенно при системных сбоях.
Отладка cron превращается из темного искусства в точную науку, когда вы знаете правильные инструменты и методы. Каждая "тихая" ошибка оставляет следы - главное научиться их читать и интерпретировать. Систематический подход, внимание к деталям окружения и терпеливая пошаговая диагностика помогут вам подчинить даже самые своенравные автоматизированные задачи. Помните: в мире системного администрирования молчание редко означает согласие - чаще всего оно скрывает проблему, которая ждет своего часа и грамотного решения.