SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Чому не запускається cron job: 5 причин

П’ять причин збою cron job: мінімальний PATH, неекранований символ %, неправильний crontab, вивід у пошті та скрипт, що очікує вашу оболонку.

Чому cron job не запускається

Cron job, який «ніколи не запускається», майже завжди вже запускався. Він виконувався в середовищі, відмінному від вашої оболонки, завершувався з помилкою в першу секунду, а повідомлення надходило туди, де ви його не читали. Майже кожен такий випадок пояснюється однією з п’яти причин: шляхом пошуку, символом відсотка, неправильним файлом crontab, виводом, який надіслано електронною поштою, або скриптом, що очікує сеанс входу.

cron — це daemon (фоновий сервіс), який читає файли crontab і запускає команди за розкладом. Він не читає ваш .bashrc, не відкриває термінал, не запускає login shell і не повідомляє, коли команда завершується з помилкою. Кожна наведена нижче причина випливає з цих чотирьох фактів.

Розгляньте їх по черзі та почніть із головного питання: чи cron взагалі запустив job? «cron не запустив job» і «job запустився та завершився» — це різні проблеми, які не мають спільної причини. Спочатку з’ясуйте саме це.

Чи запустився cron взагалі?

Назва unit daemon відрізняється в різних сімействах дистрибутивів. Перевірте обидва варіанти, а потім прочитайте журнал.

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

У Debian і Ubuntu unit має назву cron. У Fedora, Rocky та Alma він має назву crond. На конкретній машині існує лише одна з цих назв, тому повідомлення про невідомий unit в одній із двох команд є нормальним і не свідчить про помилку.

Прочитайте записи, які створила саме ваша система. Не шукайте рядок, скопійований із посібника, оскільки формулювання відрізняються між реалізаціями cron і конфігураціями журналювання. Потрібно перевірити лише дві речі: чи є запис на хвилині, вказаній у розкладі, і чи містить цей запис назву вашої команди. Запис із назвою вашої команди означає, що cron виконав свою частину, а помилка виникла всередині команди. Відсутність запису означає, що cron не отримав ваш розклад. Це причина 3 нижче.

Деякі образи передають повідомлення cron через rsyslog у файл, а не до журналу. Перевірте /var/log на наявність файлу з назвою на кшталт cron або syslog, а потім прочитайте його кінець.

ls -l /var/log
sudo tail -n 50 /var/log/syslog

Якщо немає ні unit, ні журналу, можливо, cron просто не встановлений. У мінімальних хмарних образах і контейнерах його часто немає.

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

Причина 1: cron не має вашої змінної PATH

Інтерактивна оболонка формує PATH з /etc/profile, ~/.profile, ~/.bashrc і всього, що підключають ці файли. Для завдання cron нічого з цього не виконується. cron запускає команду у власному скороченому середовищі, тому програму поза стандартними системними каталогами не знайдено. Це може бути будь-що в /usr/local/bin, /opt, менеджері версій мови програмування, віртуальному середовищі Python або робочому просторі Go. Завдання завершується з помилкою на першому рядку, а оболонка записує повідомлення на кшталт "not found". Точне формулювання залежить від оболонки, яка його виконала.

Знайдіть фактичний шлях до кожної команди, яку використовує ваше завдання.

command -v docker
command -v node
readlink -f "$(command -v node)"

Потім або вкажіть ці абсолютні шляхи в завданні, або один раз задайте PATH на початку crontab.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

Отримайте цей список на власній машині за допомогою echo "$PATH" і видаліть усе, що існує лише в інтерактивному сеансі. Тут важливе одне правило: cron не розгортає змінні в цих рядках призначень. PATH=$PATH:/usr/local/bin зберігає буквальний текст $PATH:/usr/local/bin, тому завдання отримує змінну пошуку, яка не містить жодного придатного каталогу. Запишіть увесь список явно.

Для менеджера версій одного шляху недостатньо. nvm, pyenv, rbenv і asdf встановлюють функцію оболонки або каталог shims із вашого .bashrc, а завдання cron ніколи не читає цей файл. Викликайте бінарний файл потрібної версії за абсолютним шляхом або підключіть init-скрипт менеджера першим рядком власного скрипту.

Причина 2: символ відсотка завершує команду

У полі команди crontab % не є звичайним символом. Перший неекранований % завершує команду. Усе, що стоїть після нього, передається команді через стандартний ввід, а кожен наступний % перетворюється на символ нового рядка. Це реальна функція cron для передавання коротких вхідних даних програмі. Вона ж пояснює, чому ім’я файлу з датою є класичним прикладом помилкового запису в crontab.

Запишіть 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site, і tar ніколи не побачить відформатовану дату. cron обрізає рядок на першому %, тому shell отримує незавершену підстановку команди, а решта рядка надходить через стандартний ввід. Екрануйте кожен символ відсотка зворотною скісною рискою.

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

Один рядок читають два рівні, послідовно. \% — це правило cron, яке cron застосовує до запуску будь-яких процесів. $(date +\%F) — це підстановка команди, яку пізніше застосовує shell, запущений cron. Уся суть полягає в тому, щоб розуміти, якому рівню належить кожен символ.

Надійніше взагалі не розміщувати логіку в crontab. Винесіть її у скрипт. У скрипті символ відсотка не має спеціального значення.

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

Тоді рядок crontab містить лише шлях і перенаправлення. Запис у crontab, який можна швидко прочитати, легше налагоджувати.

Причина 3: який crontab ви редагували?

Є не один crontab. Це кілька файлів із різними власниками та різною кількістю полів. Завдання, записане не в той файл, залишається непомітним.

  • crontab -e редагує crontab користувача, від імені якого виконано команду. sudo crontab -e редагує crontab root. Під час налагодження на одному сервері двоє людей часто читають два різні файли.
  • sudo crontab -l -u deploy виводить crontab іншого користувача. Так можна перевірити, що саме встановлено для облікового запису, від імені якого має запускатися завдання.
  • /etc/crontab і кожен файл у /etc/cron.d містять додаткове поле між розкладом і командою: ім’я користувача, від імені якого потрібно виконати команду. Якщо вставити рядок із п’ятьма полями з user crontab у /etc/cron.d, перше слово команди буде прочитано як ім’я користувача.
  • Файли в /etc/cron.d повинні мати імена, що містять лише літери, цифри, символи підкреслення та дефіси. Файл із назвою backup.sh або site.conf буде пропущено лише через неправильне ім’я. Перейменуйте його на backup і знову перевірте журнал.
  • Власником файлів у /etc/cron.d має бути root. Вони також не повинні бути доступними для запису групі або іншим користувачам. ls -l /etc/cron.d одразу показує обидва ці параметри.
  • Скрипти, додані до /etc/cron.daily та інших подібних каталогів, мають відповідати тому самому правилу іменування та мати біт виконання. Відсутність біта виконання призводить до непомітного пропуску скрипту.
  • /etc/cron.allow і /etc/cron.deny визначають, хто взагалі може встановлювати crontab. Якщо один із цих файлів є на сервері, прочитайте його, перш ніж припускати, що вашому користувачеві дозволено мати crontab.

Встановлюйте user crontab командою crontab, а не редагуйте файл spool вручну, оскільки crontab перевіряє файл перед встановленням. Після збереження прочитайте вивід цієї команди. Якщо команда відхиляє файл, попередня версія залишається активною, а ваша зміна не застосовується. Це виглядає так, ніби cron ігнорує вашу конфігурацію.

Власник також визначає дозволи. Завдання в crontab root створює файли, власником яких є root, і застосунок, що їх читає, може не мати права на запис. Завдання у crontab звичайного користувача не може читати каталог, доступний лише root. Власник має відповідати призначенню завдання: обслуговування застосунку слід виконувати від імені його власного облікового запису. Саме це є підставою для заміни WordPress wp-cron системним завданням cron. Режим доступу до файлів, які створює завдання, визначається успадкованим umask. Це значення може відрізнятися від umask вашої оболонки. Тому, якщо результат завдання створюється без права читання, варто прочитати як umask визначає дозволи для файлів.

Причина 4: вивід надходив на пошту, яку ніхто не читає

cron збирає все, що завдання записує до стандартного виводу та стандартного потоку помилок. Якщо завдання записало хоча б щось, cron передає цей текст локальній поштовій системі. Повідомлення надходить власнику crontab або за адресою, яку задає MAILTO. На мінімально налаштованому VPS зазвичай немає MTA (агента передавання пошти), тому доставити повідомлення нікому. Помилка існувала лише деякий час, а потім зникла без сліду. Саме тому несправне завдання виглядає так, ніби воно мовчить.

Натомість перенаправте вивід до файла, яким ви керуєте.

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> додає стандартний вивід до файла. 2>&1 спрямовує стандартний потік помилок туди, куди в цей момент спрямований стандартний вивід, тому цей оператор має бути після перенаправлення. Якщо записати їх у зворотному порядку, як 2>&1 >> file, стандартний потік помилок збереже початкове призначення, і помилка, яку ви шукаєте, не потрапить до файла.

Журнал — ще одне зручне місце для виводу. logger записує його до syslog під заданою вами міткою.

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

Прочитайте записи за допомогою journalctl -t backup-site. Так вивід самого завдання зберігається поруч із записами cron, і часову послідовність легко відстежувати. Якщо вам також потрібно фіксувати, хто саме виконав кожну команду на сервері, це окрема система. Її опис наведено в матеріалі аудит команд користувачів на сервері.

MAILTO="" на початку crontab вимикає пошту для завдань, розташованих нижче. Налаштування MAILTO на реальну адресу допоможе лише за наявності справного MTA, тому спочатку переконайтеся, що пошта справді виходить із сервера.

Під час налагодження діє одне правило: ніколи не додавайте > /dev/null 2>&1. Це найпоширеніший рядок у кожному crontab, і він викидає єдиний наявний доказ. Поверніть його пізніше, коли завдання запрацює.

Причина 5: скрипт очікує змінні середовища, яких cron не передає

Після того як команду знайдено, а її вивід перехоплено, залишається врахувати все інше, що ваша сесія надає автоматично.

  • Оболонка може бути не bash. Перевірте це за допомогою ls -l /bin/sh. У Debian і Ubuntu цей шлях вказує на dash, тому перевірка з подвійними квадратними дужками, масиви та source завершуються синтаксичною помилкою. Додайте до скрипту рядок #!/bin/bash і запускайте скрипт або вкажіть SHELL на початку crontab.
  • Робочий каталог не збігається з каталогом, у якому ви перебували. Усюди використовуйте абсолютні шляхи або додайте cd для переходу до потрібного каталогу в першому рядку скрипту. Відносний шлях — найпоширеніша причина, чому завдання «працює під час ручного запуску».
  • Локаль відрізняється від локалі вашої сесії. Будь-яка команда, яка форматує дату чи число або сортує текст, може повертати інший результат за іншого значення LANG. Якщо наступний крок обробляє цей результат, задайте локаль у скрипті, а не покладайтеся на поточне значення.
  • TTY (терміналу) немає. Команда, яка запитує підтвердження, відкриває редактор або відображає індикатор виконання, може зависнути чи завершитися. Додайте прапорець для неінтерактивного режиму, якщо відповідний інструмент його підтримує.
  • SSH agent недоступний. SSH_AUTH_SOCK немає в середовищі cron, тому команда ssh або rsync, яка працювала завдяки завантаженому ключу в агенті, тепер не може автентифікуватися. Надайте завданню окремий ключ, власником якого є користувач цього завдання.
  • Шини сеансу користувача немає, тому systemctl --user із завдання cron завершується помилкою, доки не буде задано XDG_RUNTIME_DIR. Кращим рішенням є systemd unit.

У Fedora, Rocky та Alma є ще одна можлива причина. SELinux обмежує завдання cron, тому доступ до шляху з неочікуваною міткою блокується, навіть якщо права доступу до файлу правильні. Перевірте відмови в доступі за допомогою sudo ausearch -m avc -ts recent і прочитайте Основи SELinux для сервера, перш ніж вимикати будь-які обмеження.

Однохвилинна перевірка, яка показує середовище cron

Не припускайте, які змінні містить середовище cron. Прочитайте їх. Створіть скрипт, який виводить усі змінні, заплануйте його запуск щохвилини, зачекайте, а потім прочитайте файл.

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

Додайте один рядок до crontab користувача, від імені якого фактично запускається потрібне завдання. Використайте абсолютні шляхи з обох боків.

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

Зачекайте хвилину, потім прочитайте /home/deploy/cron-probe.log і порівняйте його з тими самими командами, виконаними у вашій оболонці. Рядок PATH, робочий каталог і локаль зазвичай самі пояснюють причину збою. Зверніть увагу на дві деталі цього налаштування: символи відсотка містяться всередині скрипту, де правило cron на них не поширюється, а шлях до журналу доступний для запису користувачу, від імені якого запускається завдання.

Видаліть цей рядок із crontab одразу після отримання відповіді. Завдання, яке запускається щохвилини та дописує дані у файл, заповнить невеликий диск, причому зробить це непомітно.

Розклад відповідає тому, який ви мали на увазі?

Рядок у crontab користувача починається з п’яти полів: хвилина, година, день місяця, місяць, день тижня. Два з них взаємодіють так, що це часто дивує користувачів.

Якщо поля дня місяця та дня тижня обмежені, тобто жодне з них не дорівнює *, cron запускає завдання, коли збігається будь-яке з цих полів. 0 0 13 * 5 — це не «п’ятниця, 13-е». Завдання запускається опівночі 13-го числа кожного місяця та щоп’ятниці опівночі. Щоб вибрати один конкретний день, залиште одне з двох полів як *, а друге перевірте всередині скрипту.

cron використовує системний часовий пояс. Багато образів VPS налаштовано на UTC (координований всесвітній час), тому завдання, заплановане на 03:00, запускається о 03:00 UTC, що у вашому часовому поясі може припадати на середину дня. timedatectl показує, який часовий пояс фактично використовує ваш сервер. Перевірте це значення, а не припускайте, що воно збігається з часовим поясом вашого ноутбука.

Варто знати ще про дві проблеми з розкладом. @reboot спрацьовує під час запуску самого cron. Це не обов’язково збігається з моментом готовності мережі, тому завдання, якому потрібні DNS або віддалений хост, може завершитися помилкою під час завантаження системи, а під час кожного наступного ручного запуску працювати успішно. Крім того, ніщо не забороняє повільному завданню запуститися знову, поки попередній процес ще працює. Використайте блокування.

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n негайно завершує виконання, якщо блокування вже встановлено. Тому паралельний запуск припиняється, а не накопичується поверх першого.

Коли systemd timer є кращим інструментом

cron добре виконує одне завдання: запускає команду у визначений час. В інших аспектах його можливості обмежені. timer надає журнал без перенаправлення виводу, дає змогу пізніше перевірити код завершення, налаштувати порядок запуску відносно network-online.target і встановити випадкову затримку, щоб сотня серверів не запускала завдання в одну й ту саму секунду. Якщо завданню потрібна будь-яка з цих можливостей, systemd service і timer на VPS налаштувати простіше, ніж підтримувати рядок у crontab. Повторні спроби також налаштовують там, оскільки політики перезапуску systemd визначають, що відбувається після помилки, а cron взагалі не має відповіді на це питання.

Залишайте cron для невеликих завдань. Усе, що має залежності або політику повторних спроб, переносьте до timer. Обидва інструменти можуть працювати на одному сервері, тому не потрібно завершувати цю міграцію за один раз.

FAQ

Чому моє cron-завдання працює вручну, але завершується помилкою через cron?

Тому що середовища вашої оболонки та cron відрізняються. Оболонка входу читає /etc/profile і ~/.bashrc, які задають PATH, локаль і змінні агента. cron запускає команду без цього, з іншого робочого каталогу, а іноді й з іншою оболонкою. Використовуйте абсолютні шляхи для кожної команди, задайте потрібні змінні на початку crontab або всередині скрипту та заплануйте діагностичне завдання на одну хвилину, яке записує env | sort, pwd і id у файл журналу. Так ви побачите фактичне середовище cron, а не вгадуватимете його.

Як перевірити, чи cron справді запустив моє завдання?

Прочитайте журнал демона. Використовуйте journalctl -u cron у Debian та Ubuntu або journalctl -u crond у Fedora, Rocky та Alma. Деякі образи натомість передають ці повідомлення через rsyslog у файл під шляхом /var/log. Знайдіть запис за хвилину, зазначену в розкладі, і перевірте, чи містить він вашу команду. Відсутність запису означає, що cron не отримав цей розклад, тому переконайтеся, що ви редагували правильний crontab. Запис без результату означає, що команда запустилася та завершилася, тому перенаправте її вивід.

Чому date +%Y не працює всередині crontab?

cron обробляє % як спеціальний символ у полі команди. Перший неекранований % завершує команду, усе після нього передається цій команді через стандартне введення, а кожен наступний % перетворюється на новий рядок. Тому ім’я файлу з форматом дати не передається програмі, для якої його створено. Екрануйте кожен символ відсотка як \% або перенесіть команду в скрипт і викликайте цей скрипт із cron, оскільки всередині скрипту символ відсотка не має спеціального значення.

Куди потрапляє вивід мого cron-завдання?

У локальну поштову систему, адресату, визначеному власником crontab або значенням MAILTO. У більшості образів VPS немає встановленого агента передавання пошти, тому повідомлення відкидається, і завдання здається безшумним. Перенаправте вивід у файл за допомогою >> /path/to/log 2>&1, зберігаючи саме такий порядок, щоб стандартна помилка слідувала за стандартним виводом, або передайте вивід через logger -t myjob і прочитайте його за допомогою journalctl -t myjob. Не використовуйте > /dev/null 2>&1, доки триває діагностика.

Що вибрати: cron чи таймер systemd?

Використовуйте cron для простої команди у фіксований час, особливо якщо її потрібно буде перенести на машину без systemd. Використовуйте таймер, коли потрібен вивід у журналі без перенаправлення, доступний для перевірки код завершення, запуск після готовності мережі, випадкова затримка запуску або політика повторної спроби після помилки. Обидва варіанти можуть працювати на одному сервері, тому завдання можна переносити по одному, коли це буде виправдано.