Планировщик задач cron(8) существует с 7 версии Unix, а его синтаксис crontab(5) знаком даже тем, кто нечасто сталкивается с системным администрированием Unix. Это стандартизированный, довольно гибкий, простой в настройке и надёжно работающий планировщик, которому пользователи и системные пакеты доверяют управление важными задачами.
У простоты cron(8), как и многих старых Unix-инструментов, есть недочёт: программа полагается на то, что пользователь хотя бы примерно знает, как всё работает, и в состоянии правильно реализовать в нём какую-то проверку безопасности.
По сути, единственное, что делает планировщик, — это пытается запустить задачу в определённое время и прислать результат на электронную почту. Для простых и незначительных задач пользователей таких возможностей вполне достаточно.
Но для более важных системных задач стоит обернуть вокруг cron(8) и вызываемых им задач дополнительную инфраструктуру. Если вам хочется отслеживать выполняемые задачи, то существует несколько способов повысить надёжность работы с cron(8).
Шестой столбец в системном файле crontab(5) — это имя пользователя, который должен запускать задачу:
0 * * * * root cron-task
По возможности вы должны запускать задачу от имени пользователя, привилегий которого должно хватать только на это действие и никакое другое. Иногда имеет смысл создать отдельного системного пользователя только для запуска запланированных задач, связанных с вашим приложением.
0 * * * * myappcron cron-task
Это делается не только из соображений безопасности (хотя их тоже следует учитывать); данная мера защищает вас от таких неприятностей, как ошибки сценариев, которые пытаются удалить все системные директории.
Также старайтесь не запускать задачи с системами баз данных (как, например, MySQL) через пользователя root с правами администратора. Лучше сделайте это под другим пользователем…
Или даже создайте специального пользователя с ограниченными разрешениями и уникальным случайным паролем, который будет храниться в защищённом файле ~/.my.cnf. К примеру, для задачи резервного копирования MySQL требуется всего лишь несколько разрешений: SELECT, SHOW VIEW и LOCK TABLES.
Конечно же, иногда действительно нужно заходить под root. В особо конфиденциальных случаях можно даже воспользоваться sudo(8) с соответствующими опциями NOPASSWD, чтобы назначенный пользователь мог запускать только определённые задачи под root и ничего более.
Перед тем как добавить задачу в файл crontab(5), необходимо протестировать её в командной строке в роли пользователя, который будет запускать задачу, и с теми же параметрами среды. Если вы будете запускать задачу как root, то воспользуйтесь чем-то вроде su или sudo -i , чтобы сначала получить root-оболочку с ожидаемой средой пользователя:
$ sudo -i -u cronuser
$ cron-task
Как только задача «отработала» в командной строке, добавьте её в файл crontab(5). Измените настройки времени так, чтобы задача запустилась через несколько минут, а затем просмотрите /var/log/syslog с помощью команды tail -f и убедитесь, что задача запускается без ошибок и выполняется правильно:
May 7 13:30:01 yourhost CRON[20249]: (you) CMD (cron-task)
Поначалу такая процедура кажется педантичной, но очень скоро становится привычной и избавляет от множества хлопот, ведь так просто предположить что-то, что есть в вашей среде, но отсутствует в среде использования cron(8).
А ещё это необходимая проверка того, что ваш файл crontab(5) имеет правильную структуру — некоторые реализации cron(8) не загружают целый файл, если одна из строк имеет неправильный формат.
При необходимости в начале файла вы можете задать произвольные переменные среды для задач:
MYVAR=myvalue
0 * * * * you cron-task
Скорее всего, вы уже встречали руководства в сети, в которых для того чтобы crontab(5) не рассылал стандартные результаты и/или типовые письма с ошибками каждые 5 минут, в конце описания задания добавляются операторы перенаправления оболочки. Этот кустарный способ очень популярен для запуска задач по веб-разработке путём автоматизации запроса к URL через curl(1) или wget(1):
*/5 * * * root curl https://example.com/cron.php >/dev/null 2>&1
Полное игнорирование результатов выполнения — это всегда плохо. Ведь если вам выдают только нужные результаты или ошибки и у вас нет других задач или средств мониторинга выполнения заданий, то вы не заметите проблем (и не узнаете, в чём они заключаются).
В случае с curl(1) столько всего могло пойти не так, а вы заметили бы это слишком поздно:
Скрипт мог сломаться и вернуть 500 ошибок.
URL задачи cron.php мог измениться, а кто-то забыл добавить перенаправление HTTP 301.
Но даже с настроенным редиректом HTTP 301 вы не сможете на него перейти, если не воспользуетесь в curl(1) командами -L или --location.
Клиент мог попасть в чёрный список, на запросе мог сработать брандмауэр… его могли заблокировать автоматические или ручные процессы, которые ошибочно пометили запрос как спам.
При несоответствии шифра или протокола в HTTPS могло сломаться подключение.
Автор встречал всё из вышеперечисленного, и даже очень часто.
Лучше потратить какое-то время на чтение справочной страницы вызываемой задачи и поискать, как правильно управлять результатами выполнения, чтобы получать действительно нужные вам данные. Например, в случае с curl(1) я заметил, что хорошо работает следующая формула:
curl -fLsS -o /dev/null http://example.com/
-f: если код ответа HTTP — это ошибка, то сгенерировать сообщение об ошибке, а не страницу 404.
-L: если указано перенаправление HTTP 301, то попробовать перейти на него.
-sS: не показывать индикатор выполнения (-S также не позволяет -s блокировать сообщения об ошибках).
-o /dev/null: отправить стандартный результат выполнения (фактически возвращённую страницу) в /dev/null.
Таким образом, в соответствии со старой Unix-философией Правила тишины ваш запрос curl(1) будет молчать, пока всё работает как надо.
Вы можете не согласиться с некоторыми из перечисленных вариантов; можете посчитать, что, например, нужно сохранять полный вывод возвращённой страницы или выдавать ошибку, а не тихо переходить на редирект 301, или же вообще предпочтёте пользоваться wget(1).
Суть в том, что вы пробуете разобраться, чего и при каких обстоятельствах ждать от вызываемой программы, а вместо бездумного удаления всех результатов и (что ещё хуже) ошибок пытаетесь как можно лучше подстроить программу под ваши требования. Поработайте с законом Мерфи; исходите из того, что всё, что могло пойти не так, со временем пойдёт не так.
Ещё одна распространённая ошибка — не прописывать вверху файла crontab(5) правильный MAILTO , куда будут отправляться все ошибки и результаты выполнения задач.
Для отправки своих сообщений cron(8) использует реализацию системной почты, а стандартные конфигурации для почтовых агентов чаще всего отправляют сообщение в файл mbox в /var/mail/$USER, который могут никогда и не читать, что сводит на нет всю концепцию рассылки результатов и ошибок.
Но это легко исправить. Сделайте так, чтобы сообщения отправлялись на адрес, который вы действительно проверяете с сервера… возможно, даже с помощью mail(1):
$ printf '%s\n' 'Test message' | mail -s 'Test subject' you@example.com
Как только вы проверили корректную настройку почтового агента и убедились, что на почтовый ящик доставляются письма, пропишите этот адрес в переменной MAILTO в начале файла:
MAILTO=you@example.com
0 * * * * you cron-task-1
*/5 * * * * you cron-task-2
Если же вы не хотите получать результаты выполнения задачи на электронную почту, то имеется и другой способ. С помощью такого инструмента, как logger(1), вы можете отправлять эти данные в syslog:
0 * * * * you cron-task | logger -it cron-task
Ещё можно настроить псевдонимы в системе, чтобы перенаправлять ваши системные письма на тот адрес, которым пользуетесь. Для постфикса воспользуйтесь файлом aliases(5).
Иногда я пользуюсь этим способом, если задача может завершиться несколькими строками выходных значений, которые будут полезны для последующего анализа, но обычные результаты stderr отправляю через MAILTO. Если вы не хотите пользоваться syslog (возможно, из-за большого объёма и/или частоты отправляемых данных), то всегда можно настроить файл журнала /var/log/cron-task.log… но не забудьте добавить для этого правило logrotate(8)!
В идеале команды в определениях crontab(5) должны состоять из нескольких слов, а сам файл содержать 1–2 команды. Если команда выполняется за экраном, то, скорее всего, она слишком длинная для crontab(5), поэтому вам следует добавить её в собственный сценарий. Эта идея особенно хороша, если для своих команд вы хотите эффективно пользоваться возможностями bash или других оболочек (помимо POSIX/Bourne /bin/sh)… или даже языком сценариев (например, Awk или Perl). Для разбора команд cron(8) по умолчанию использует системную реализацию /bin/sh.
Файлы crontab(5) не допускают многострочных команд и имеют ряд других недостатков (например, знаки процента (%) нужно экранировать обратной косой чертой). Поэтому хранить как можно больше конфигураций вне crontab(5) — это, в общем-то, и неплохая идея.
Если вы запускаете задачи cron(8) от имени несистемного пользователя и не можете добавить сценарии в системный bindir в виде /usr/local/bin, то правильнее всего будет прописать свой собственный скрипт и добавить ссылку на него в PATH. Я предпочитаю ~/.local/bin, но встречал ссылки и на ~/bin. Сохраните сценарий в ~/.local/bin/cron-task, сделайте его исполняемым с помощью chmod +x и включите эту директорию в определение среды PATH в верхушке файла:
PATH=/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin
MAILTO=you@example.com
0 * * * * you cron-task
Собственная директория с пользовательскими сценариями под личные нужды имеет массу преимуществ, но это тема для другой статьи.
Если ваша реализация cron(8) это поддерживает, то вместо бесконечно длинного файла /etc/crontab попробуйте вынести задачи в отдельный файл /etc/cron.d:
$ ls /etc/cron.d
system-a
system-b
raid-maint
Такая структура позволит логически сгруппировать файлы конфигураций так, что вы (и другие администраторы) сможете быстрее находить нужные задачи. Кроме того, вы сможете сделать какие-то файлы доступными для редактирования только определённым пользователям, чтобы снизить вероятность конфликтов изменений. Здесь, кстати, пригодится и sudoedit(8).
Ещё один плюс в том, что он лучше работает с контролем версий. Так что, если собирается изрядное количество таких файлов задач либо они обновляются чаще раза за пару месяцев, то я запускаю Git-репозиторий для их отслеживания:
$ cd /etc/cron.d
$ sudo git init
$ sudo git add --all
$ sudo git commit -m "First commit"
Если вы редактируете файл crontab(5) для задач только определённого пользователя, то воспользуйтесь инструментом crontab(1). Для изменения crontab(5) напечатайте crontab -e. Так ваш $EDITOR сможет внести изменения во временный файл, который будет установлен при выходе. Файлы сохранятся в специально выделенной директории, в моей системе она называется /var/spool/cron/crontabs.
В системах, которые я поддерживал, готовый шаблон /etc/crontab мог никогда не меняться. Это нормально.
Обычно cron(8) разрешает задаче выполняться бесконечно, так что если вам этого не нужно, то следует прописать время ожидания (тайм-аут) в опциях вызываемой программы либо включить его в сценарий. Если в самой команде нужных параметров нет, то одним из возможных способов реализации станет обёртка команды timeout(1) в coreutils:
0 * * * * you timeout 10s cron-task
В Википедии Грега (Greg’s Wiki) можно найти целый ряд дополнительных предложений по реализации тайм-аутов.
cron(8) запускает новый процесс вне зависимости от того, завершились предыдущие запуски или нет. Так что если вы хотите избежать блокировки задач с длительным временем выполнения, то в GNU/Linux можно воспользоваться обёрткой flock(1) для системного вызова flock(2) — она настроит особый файл блокировки, который предотвращает параллельное выполнение задач в более чем одном экземпляре cron.
0 * * * * you flock -nx /var/lock/cron-task cron-task
В Википедии Грега (Greg’s Wiki) можно найти подробное обсуждение типовой блокировки файла для сценариев, включая важные нюансы о «развёртывании собственных файлов», если flock(1) недоступен.
Если вам важно запускать какие-то действия в определённом порядке, то подумайте, нужно ли разводить их в отдельные задачи? Возможно, если собрать задачи в один сценарий оболочки, вам будет проще организовать их последовательное выполнение.
Если ваша задача в cron(8) (или команды внутри её сценария) завершается ненулевыми значениями, то лучше запустить команды, которые смогли бы правильно обработать ошибки. Например, очистить соответствующие ресурсы и отправить информацию о текущем статусе задания в средства мониторинга. Если вы пользуетесь Nagios Core или его вариациями, то присмотритесь к send_nsca — он может передавать пассивную проверку статуса задачи на сервер мониторинга. Я написал простой сценарий nscaw, который делает это за меня:
0 * * * * you nscaw CRON_TASK -- cron-task
Если ваша машина работает не круглые сутки, а задачу не нужно запускать в опредёленное время (допустим, она запускается раз в день или раз в неделю), то лучше установите anacron и перекиньте сценарии в /etc соответствующих директорий cron.hourly, cron.daily, cron.monthly и cron.weekly. Обратите внимание, что в /etc/crontab по умолчанию для Debian и Ubuntu GNU/Linux есть хуки для запуска, но задачи запускаются, только если не установлен anacron(8).
Если вы используете cron(8), чтобы «спросить» директорию об изменениях и запустить сценарий только при наличии каких-то изменений, то в GNU/Linux лучше прописать демона на основе inotifywait(1).
И, наконец, если вам нужен больший контроль над тем, когда и как запускаются задачи, а возможности cron(8) в этом плане вас не устраивают, то, возможно, стоит написать демона, который будет последовательно работать на сервере и разветвлять процессы для своей задачи.
Например, это позволит запускать задачу чаще одного раза в минуту. Не зацикливайтесь на cron(8) как на единственном варианте для асинхронного управления задачами!
на главную сниппетов