сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Linux и bash

cron и systemd-таймеры

Задачи по расписанию

Завёл на этой машине задачу по расписанию, которая просто печатает своё окружение. Через минуту прочитал результат: переменных в окружении восемь против сорока в моём обычном сеансе, путь поиска программ короче, а оболочка по умолчанию другая. Скрипт, который у меня в терминале работает, в такой задаче может не найти половину своих инструментов.

Стержень: задача по расписанию запускается в урезанном окружении и ничего не знает о предыдущем запуске.

// Формулировки: «в терминале работает, по расписанию нет», «что если предыдущий запуск не успел?», «чем таймер лучше классического планировщика?»

Урезанное окружение

Замер конкретный. В моём сеансе 40 переменных окружения, в задаче по расписанию - 8. Путь поиска программ в задаче содержит только системные каталоги, без личных. Оболочка задаче назначена простая, не та, в которой я работаю, а значит, часть привычного синтаксиса просто не сработает.

Отсюда три правила. Первое: в скрипте для расписания пути к программам и файлам пишут полностью либо задают путь поиска в самом скрипте. Второе: всё, что нужно из окружения (адреса, ключи, язык, часовой пояс), задают явно, а не надеются, что оно унаследуется. Третье: явно указывают, какой оболочкой запускать.

// Проверять такие вещи надо не глазами, а запуском в тех же условиях: у классического планировщика это делается запуском через окружение с очищенными переменными. Разница между «у меня работает» и «в проде не работает» в девяти случаях из десяти именно тут.

окружение задачи
набор переменных, который получает запущенная по расписанию программа
путь поиска программ
список каталогов, где ищется команда, запущенная по имени

Наложение запусков

Планировщик - служба, которая запускает задачи по расписанию, - смотрит только на время и не проверяет, закончился ли предыдущий запуск. Если задача стоит каждые пять минут, а однажды отработала семь, то в системе окажутся две копии сразу. Дальше по нарастающей: если задача тяжёлая, копии начнут мешать друг другу и в конце концов положат машину.

Воспроизвёл на стенде: два запуска подряд без защиты честно пошли оба и работали одновременно. Тот же скрипт с замком на файле: первый запустился и работал, второй увидел занятый замок и вышел с сообщением «предыдущий запуск ещё идёт». Три строки в начале скрипта - и класс аварий закрыт.

// Замок ставят именно на файл, а не на проверку «есть ли такой процесс». Проверка процесса ненадёжна: между проверкой и запуском проходит время, а после аварийного завершения остаётся файл с номером процесса, который уже никому не принадлежит. Замок на файле снимается операционной системой сам, когда процесс умирает.

наложение запусков
новый запуск начинается, пока предыдущий ещё работает
замок на файле
механизм ядра: второй процесс видит, что файл занят, и не начинает работу

Чем таймеры удобнее классического планировщика

Таймеры менеджера служб (это программа, которая запускает и следит за всеми службами системы) решают несколько вещей сразу. Пропущенный запуск можно догнать: если машина была выключена в момент срабатывания, задача выполнится сразу после включения - у классического планировщика такого нет вовсе. Есть разброс запуска, чтобы сотня машин не пошла в одну секунду в один и тот же сервис. Есть предел времени выполнения, после которого задачу прибивают. И запуск можно привязать не к часам, а к времени с прошлого выполнения.

Ещё одно отличие в том, куда девается вывод. Классический планировщик пытается отправить вывод письмом и обычно теряет его, а служба пишет в общий журнал, где он лежит рядом со всеми остальными записями и ищется по имени задачи.

// Цена - многословность: вместо одной строки расписания получается два файла, описание задачи и описание таймера. На одной машине с тремя задачами это лишняя возня, на парке машин - экономия нервов. Проверить, как понимается запись расписания, можно отдельной командой: она печатает, когда именно сработает следующий запуск.

догоняющий запуск
выполнить пропущенное сразу после включения машины
разброс запуска
случайная задержка, чтобы машины не стартовали одновременно

Как отвечать: «Скрипт работает в терминале и не работает по расписанию. Почему?»

Почти всегда дело в окружении. Я специально это мерил: у задачи по расписанию восемь переменных окружения против сорока в моём сеансе, путь поиска программ короче и оболочка назначена другая. Поэтому программа, которая в терминале находится по имени, в задаче не находится вовсе, а переменные с адресами и ключами просто отсутствуют. Лечу тремя вещами: полные пути или заданный путь поиска прямо в скрипте, явное указание всего нужного окружения и явная оболочка. Проверяю не глазами, а запуском в тех же условиях, с очищенными переменными. И заодно смотрю на две соседние вещи: куда девается вывод задачи, потому что по умолчанию он теряется, и что будет, если предыдущий запуск не успел закончиться.

Ответ называет причину, даёт способ проверки и переходит к двум соседним ловушкам. Про потерянный вывод и про наложение вспоминают редко, а страдают от них постоянно.

На чём валятся

  • Полагаются на переменные окружения, которых у задачи по расписанию нет.
  • Пишут команды по имени, а не полным путём, и получают «команда не найдена».
  • Не думают про наложение: тяжёлая задача копится в несколько копий.
  • Ставят замок через файл с номером процесса и получают залипший замок после аварии.
  • Теряют вывод задачи и потом не могут понять, почему она «вроде отработала».

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.

  1. #dvo_scheduling1 / 5
    Задача в 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, которая исчезает при ребуте вместе со следами зависшего запуска.

  2. #dvo_scheduling2 / 5
    Ночной бэкап переносят с cron на systemd-таймер. Что даёт Persistent=true?
    A)Пропущенный из-за простоя запуск догонится после старта
    B)Таймер переживёт перезагрузку и не потребует enable
    C)Сервис будет перезапущен, если упал во время работы
    D)Состояние задачи сохранится между запусками в /var/lib
    показать ответ и разбор
    +A)Пропущенный из-за простоя запуск догонится после старта

    // разбор: Persistent=true заставляет systemd запомнить время последнего срабатывания на диске: если в 3 ночи машина была выключена, задача стартует сразу после загрузки, а не через сутки. Плюс таймера в другом: он не поднимет второй экземпляр, пока связанный сервис активен, а логи и код возврата видны в journalctl и systemctl status — с cron это отдельная возня.

  3. #dvo_scheduling3 / 5
    Что означает строка 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 минут в рабочие часы по будням.

  4. #dvo_scheduling4 / 5
    cron-задача «иногда не отрабатывает», в логах приложения пусто, следов нет. Как поймать причину?
    A)cron сам пишет ошибки задачи в /var/log/syslog
    B)Перенаправить stdout и stderr в файл: cmd >>log 2>&1
    C)Добавить 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[...]), но не её собственные ошибки.

  5. #dvo_scheduling5 / 5
    На 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)) в начале скрипта, либо сдвиг по хешу хоста. Простой перенос всех на другую точную минуту только переносит пик, но не устраняет синхронность.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.