Простой и удобный шаблон для bash-скриптов выполняемых по расписанию

от автора

Хочу поделиться с сообществом простым и полезным шаблоном скрипта-обёртки на bash для запуска заданий по cron (а сейчас и systemd timers), который моя команда повсеместно использует много лет.

Сначала пара слов о том зачем это нужно, какие проблемы решает. С самого начала моей работы системным администратором linux, я обнаружил, что cron не очень удобный планировщик задач. При этом практически безальтернативный. Чем больше становился мой парк серверов и виртуальных машин, тем больше я получал абсолютно бесполезных почтовых сообщений «From: Cron Daemon». Задание завершилось с ошибкой — cron напишет об этом. Задание выполнено успешно, но напечатало что-нибудь в STDOUT/STDERR — cron всё равно напишет об этом. При этом даже нельзя отформатировать тему почтового сообщения для удобной автосортировки. Сначала были годы борьбы с использованием разных вариаций из > /dev/null, 2> /dev/null, > /dev/null 2>&1, | mail -E -s '<Subject>' root@. Потом я нашёл Cronic — обёртку на bash, которая скрывает вывод запускаемой задачи, если она завершена успешно. Стало полегче, но обнаружилось, что от некоторых заданий всё же лучше получать сообщение «Task OK», чтобы не столкнуться в самый неподходящий момент с тем, что выполнение задания тихо сломано месяц назад. Постепенно копились и другие хотелки:

  1. иногда требуется, чтобы задание было автоматически принудительно остановлено, если выполняется больше определённого времени;

  2. иногда нужны гарантии, что в каждый момент времени запущена только одна копия задания;

  3. бывает так, что запускать задачу нужно с рандомной задержкой по времени (такая дисперсия иногда нужна, чтобы не положить какой-нибудь сервис одновременными запросами с большого количества машин).

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

  1. сохраняет в лог-файлы STDERR и STDOUT выполняемых команд;

  2. если задание завершилось ошибкой, то отправляет на заданный электронный адрес последние 10000 (можно настроить любое) строк STDOUT и STDERR;

  3. опционально может отправить метрику в Zabbix, если задача выполнена успешно (удобно для сброса времени срабатывания триггера);

  4. гарантирует, что одновременно будет запущена только одна копия задания;

  5. опционально может запускать задачу с рандомной (в заданном диапазоне) задержкой по времени;

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

Вот как выглядит пример сообщения, которое присылает скрипт

Вот как выглядит пример сообщения, которое присылает скрипт
Давайте посмотрим на сам шаблон
#!/usr/bin/env bash  ## # bash options ##  set -eu -o pipefail  export LC_ALL="C" export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"  ## # Variables ##  SET_EXECUTION_LOCK="${SET_EXECUTION_LOCK:-}" MAX_EXECUTION_TIME="${MAX_EXECUTION_TIME:-10}" START_DELAY_RANGE="${START_DELAY_RANGE:-0-0}" LOGS="/var/log/$(basename "$0")" HOSTNAME="${HOSTNAME:-$(hostname -f)}" REPORT_MAIL="monitoring@example.com" REPORT_SUBJ="$0 fail on $HOSTNAME" #ZABBIX_ITEM="example.task.ok"  ## # Execution lock and timeout ##  if [[ -n "$MAX_EXECUTION_TIME" ]]; then   command="timeout -v -k 60 $MAX_EXECUTION_TIME" fi  if [[ -n "$SET_EXECUTION_LOCK" ]]; then   command="flock -E 0 -n $0 ${command:-}"  fi  if [[ -z "${_run_:-}" ]]; then   sleep "$(shuf -i "$START_DELAY_RANGE" -n 1)"   export _run_=1   exec ${command:-} "$0" "$@" fi  ## # Functions ##  print_logs() {   cd "$LOGS"    echo "Trace of $HOSTNAME:$0"   for log in stderr stdout; do     if [[ -s "$log" ]]; then       echo "----- $(basename "$log")"       tail -n 10000 "$log"     fi   done }  send_to_zabbix() {   local item="${1:-}"   local value="${2:-1}"    zabbix_sender -c /etc/zabbix/zabbix_agentd.conf -s "$HOSTNAME" -k "$item" -o "$value" }  on_exit() {   return }  on_error() {   print_logs 2>&1 | mail -E -s "$REPORT_SUBJ" "$REPORT_MAIL"   on_exit    # Нам не нужно лишнее письмо от crond   exit 0 }  main() {   set -x    "$@"    if [[ -n "${ZABBIX_ITEM:-}" ]]; then     send_to_zabbix "${ZABBIX_ITEM:-}" 1   fi }  ## # Main ##  trap on_error ERR trap on_exit EXIT  [[ -d "$LOGS" ]] || mkdir -p "$LOGS"  (main "$@" > "$LOGS/stdout" 2> "$LOGS/stderr") 

В целом скрипт тривиален, но некоторые пояснения, думаю, требуются.

Переменные которые определяют параметры запуска. Их значения могут быть установлены напрямую или через одноимённые переменные окружения.

# Максимальное время после которого задача будет принудительно завершена. MAX_EXECUTION_TIME="${MAX_EXECUTION_TIME:-8h}"  # Не пустое значение запрещает запуск нескольких копий задачи. SET_EXECUTION_LOCK="${SET_EXECUTION_LOCK:-}"  # Случайное число из этого диапазона определяет количество секунд задержки # перед стартом. START_DELAY_RANGE="${START_DELAY_RANGE:-0-0}"

Точка входа, запускаем главную функцию в subshell. Это нужно для того, чтобы в случае ошибки не была завершена сама обёртка.

(main "$@" > "$LOGS/stdout" 2> "$LOGS/stderr")

Основная функция.

main() {   # Включаем вывод в STDERR выполняемых команд с аргументами   set -x    # Здесь помещаем вызов или логику задачи на bash. Если использовать   # "$@", то здесь будет выполнена команда переданная обёртке в качестве   # аргумента командной строки   "$@"    # Если определена переменная ZABBIX_ITEM, то отправляем метрику в сервер Zabbix   if [[ -n "${ZABBIX_ITEM:-}" ]]; then     send_to_zabbix "${ZABBIX_ITEM:-}" 1   fi } 

on_error() — функция, которая будет вызвана в случае ошибки.

on_error() {   # Отправляем трейсы   print_logs 2>&1 | mail -E -s "$REPORT_SUBJ" "$REPORT_MAIL"   on_exit    # Завершаем работу с rc=0, нам не нужно лишнее письмо от crond   exit 0 }

Функция on_exit() в шаблоне пуста. В неё можно добавить команды, которые будут выполнены перед завершением скрипта. Например, команды очистки временных файлов.

Что можно улучшить

  1. Если вы используете Sentry для трекинга ошибок, то при помощи sentry-cli можно заменить отправку трейсов по электронной почте на отправку их в Sentry.

  2. Можно отправлять метрики успешного завершения задачи в Prometheus/VictoriaMetrics, при помощи curl (нужен pushgateway) или, что проще, использовать prometheus node_exporter textfile collector.


ссылка на оригинал статьи https://habr.com/ru/articles/778922/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *