cron и systemd-таймеры
Завёл на этой машине задачу по расписанию, которая просто печатает своё окружение. Через минуту прочитал результат: переменных в окружении восемь против сорока в моём обычном сеансе, путь поиска программ короче, а оболочка по умолчанию другая. Скрипт, который у меня в терминале работает, в такой задаче может не найти половину своих инструментов.
Стержень: задача по расписанию запускается в урезанном окружении и ничего не знает о предыдущем запуске.
// Формулировки: «в терминале работает, по расписанию нет», «что если предыдущий запуск не успел?», «чем таймер лучше классического планировщика?»
Урезанное окружение
Замер конкретный. В моём сеансе 40 переменных окружения, в задаче по расписанию - 8. Путь поиска программ в задаче содержит только системные каталоги, без личных. Оболочка задаче назначена простая, не та, в которой я работаю, а значит, часть привычного синтаксиса просто не сработает.
Отсюда три правила. Первое: в скрипте для расписания пути к программам и файлам пишут полностью либо задают путь поиска в самом скрипте. Второе: всё, что нужно из окружения (адреса, ключи, язык, часовой пояс), задают явно, а не надеются, что оно унаследуется. Третье: явно указывают, какой оболочкой запускать.
// Проверять такие вещи надо не глазами, а запуском в тех же условиях: у классического планировщика это делается запуском через окружение с очищенными переменными. Разница между «у меня работает» и «в проде не работает» в девяти случаях из десяти именно тут.
- окружение задачи
- набор переменных, который получает запущенная по расписанию программа
- путь поиска программ
- список каталогов, где ищется команда, запущенная по имени
Наложение запусков
Планировщик - служба, которая запускает задачи по расписанию, - смотрит только на время и не проверяет, закончился ли предыдущий запуск. Если задача стоит каждые пять минут, а однажды отработала семь, то в системе окажутся две копии сразу. Дальше по нарастающей: если задача тяжёлая, копии начнут мешать друг другу и в конце концов положат машину.
Воспроизвёл на стенде: два запуска подряд без защиты честно пошли оба и работали одновременно. Тот же скрипт с замком на файле: первый запустился и работал, второй увидел занятый замок и вышел с сообщением «предыдущий запуск ещё идёт». Три строки в начале скрипта - и класс аварий закрыт.
// Замок ставят именно на файл, а не на проверку «есть ли такой процесс». Проверка процесса ненадёжна: между проверкой и запуском проходит время, а после аварийного завершения остаётся файл с номером процесса, который уже никому не принадлежит. Замок на файле снимается операционной системой сам, когда процесс умирает.
- наложение запусков
- новый запуск начинается, пока предыдущий ещё работает
- замок на файле
- механизм ядра: второй процесс видит, что файл занят, и не начинает работу
Чем таймеры удобнее классического планировщика
Таймеры менеджера служб (это программа, которая запускает и следит за всеми службами системы) решают несколько вещей сразу. Пропущенный запуск можно догнать: если машина была выключена в момент срабатывания, задача выполнится сразу после включения - у классического планировщика такого нет вовсе. Есть разброс запуска, чтобы сотня машин не пошла в одну секунду в один и тот же сервис. Есть предел времени выполнения, после которого задачу прибивают. И запуск можно привязать не к часам, а к времени с прошлого выполнения.
Ещё одно отличие в том, куда девается вывод. Классический планировщик пытается отправить вывод письмом и обычно теряет его, а служба пишет в общий журнал, где он лежит рядом со всеми остальными записями и ищется по имени задачи.
// Цена - многословность: вместо одной строки расписания получается два файла, описание задачи и описание таймера. На одной машине с тремя задачами это лишняя возня, на парке машин - экономия нервов. Проверить, как понимается запись расписания, можно отдельной командой: она печатает, когда именно сработает следующий запуск.
- догоняющий запуск
- выполнить пропущенное сразу после включения машины
- разброс запуска
- случайная задержка, чтобы машины не стартовали одновременно
Как отвечать: «Скрипт работает в терминале и не работает по расписанию. Почему?»
Почти всегда дело в окружении. Я специально это мерил: у задачи по расписанию восемь переменных окружения против сорока в моём сеансе, путь поиска программ короче и оболочка назначена другая. Поэтому программа, которая в терминале находится по имени, в задаче не находится вовсе, а переменные с адресами и ключами просто отсутствуют. Лечу тремя вещами: полные пути или заданный путь поиска прямо в скрипте, явное указание всего нужного окружения и явная оболочка. Проверяю не глазами, а запуском в тех же условиях, с очищенными переменными. И заодно смотрю на две соседние вещи: куда девается вывод задачи, потому что по умолчанию он теряется, и что будет, если предыдущий запуск не успел закончиться.
Ответ называет причину, даёт способ проверки и переходит к двум соседним ловушкам. Про потерянный вывод и про наложение вспоминают редко, а страдают от них постоянно.
На чём валятся
- −Полагаются на переменные окружения, которых у задачи по расписанию нет.
- −Пишут команды по имени, а не полным путём, и получают «команда не найдена».
- −Не думают про наложение: тяжёлая задача копится в несколько копий.
- −Ставят замок через файл с номером процесса и получают залипший замок после аварии.
- −Теряют вывод задачи и потом не могут понять, почему она «вроде отработала».
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Задача в cron стоит раз в 5 минут, но иногда идёт 12. Через час на хосте десяток её копий. Чем защититься?A)Уменьшить период до минуты, чтобы очередь не копиласьB)Взять блокировку: flock -n на файле, иначе выйтиC)Поставить nice, чтобы копии не мешали друг другуD)Добавить sleep со случайной задержкой в начало скрипта
показать ответ и разбор
+B)Взять блокировку: flock -n на файле, иначе выйти// разбор: cron запускает задачу по расписанию и не смотрит, доработала ли прошлая. Классическая защита — flock: flock -n /var/lock/job.lock -c script, и опоздавший запуск сразу выходит. Блокировку берут на весь скрипт, а не внутри, и следят, чтобы файл лока не жил на tmpfs, которая исчезает при ребуте вместе со следами зависшего запуска.
- Ночной бэкап переносят с cron на systemd-таймер. Что даёт Persistent=true?A)Пропущенный из-за простоя запуск догонится после стартаB)Таймер переживёт перезагрузку и не потребует enableC)Сервис будет перезапущен, если упал во время работыD)Состояние задачи сохранится между запусками в /var/lib
показать ответ и разбор
+A)Пропущенный из-за простоя запуск догонится после старта// разбор: Persistent=true заставляет systemd запомнить время последнего срабатывания на диске: если в 3 ночи машина была выключена, задача стартует сразу после загрузки, а не через сутки. Плюс таймера в другом: он не поднимет второй экземпляр, пока связанный сервис активен, а логи и код возврата видны в journalctl и systemctl status — с cron это отдельная возня.
- Что означает строка cron: */15 9-18 * * 1-5 ?A)Каждые 15 часов в дни месяца с 9 по 18 числоB)В 15 минут каждого часа, без ограничения по дням неделиC)15-го числа в 9 и в 18 часов только по буднямD)Каждые 15 минут, с 9 до 18 часов, по будням (пн–пт)
показать ответ и разбор
+D)Каждые 15 минут, с 9 до 18 часов, по будням (пн–пт)// разбор: Поля cron: минута, час, день месяца, месяц, день недели. */15 в минутах = каждые 15 минут (0,15,30,45); 9-18 — диапазон часов; * * — любой день месяца и месяц; 1-5 — дни недели пн–пт (0 и 7 — воскресенье). Итого: каждые 15 минут в рабочие часы по будням.
- cron-задача «иногда не отрабатывает», в логах приложения пусто, следов нет. Как поймать причину?A)cron сам пишет ошибки задачи в /var/log/syslogB)Перенаправить stdout и stderr в файл: cmd >>log 2>&1C)Добавить set -x — cron покажет трейс выполнения на экранеD)Участить запуск задачи, чтобы поймать сбой почаще
показать ответ и разбор
+B)Перенаправить stdout и stderr в файл: cmd >>log 2>&1// разбор: cron перехватывает stdout/stderr задачи и отправляет их локальной почтой пользователю; если почта не настроена (обычный случай), вывод и ошибки просто исчезают. Чтобы увидеть причину — перенаправляй в лог:
cmd >>/var/log/job.log 2>&1. В syslog/journal видно, что cron запускал задачу (строки CRON[...]), но не её собственные ошибки. - На 200 хостах одна и та же cron-джоба в 0:00 дёргает общий backend — тот ложится ровно в полночь. Как размазать нагрузку?A)Перенести всех на 0:05 — в новой минуте нагрузки не будетB)Поднять таймаут у backend, чтобы он переварил залпC)Добавить джиттер: RandomizedDelaySec или sleep $((RANDOM%600))D)Запускать джобу заметно чаще — тогда пик как-нибудь размажется сам
показать ответ и разбор
+C)Добавить джиттер: RandomizedDelaySec или sleep $((RANDOM%600))// разбор: 200 хостов, стартующих ровно в 0:00, — классический thundering herd: синхронный залп кладёт общий ресурс. Лечится джиттером: у systemd-таймера RandomizedDelaySec=10m, либо
sleep $((RANDOM % 600))в начале скрипта, либо сдвиг по хешу хоста. Простой перенос всех на другую точную минуту только переносит пик, но не устраняет синхронность.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.