0Pricing
DevOps Bootcamp · Урок

Идемпотентные скрипты и логика повторных попыток с задержкой

Проектируйте операции, безопасные при повторном запуске, и добавляйте экспоненциальную задержку для нестабильных внешних вызовов

«Идемпотентные скрипты и логика повторных попыток с задержкой» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.

Что такое идемпотентность и почему она важна

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

  • Неидемпотентный скрипт, который дважды создаёт пользователя, может завершиться с ошибкой или создать дубликаты данных.
  • Идемпотентный скрипт сначала проверяет: существует ли это уже?
  • Идемпотентные скрипты безопасно использовать в заданиях cron, конвейерах CI и циклах повторных попыток.

Золотое правило: проверяйте перед выполнением. Каждую разрушающую или создающую операцию следует защищать предварительной проверкой условия.

Защита создания файлов и каталогов

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

  • [ -d dir ] — истина, если каталог существует
  • [ -f file ] — истина, если обычный файл существует
  • mkdir -p — создаёт каталог, только если его нет (встроенная идемпотентность)

Если это возможно, предпочитайте встроенные флаги, такие как -p и --no-clobber, ручным проверкам — они выполняются атомарно и безопасны относительно состязаний.

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

Идемпотентное управление пользователями и группами

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

  • id username возвращает 0, если пользователь существует
  • getent group groupname проверяет наличие группы
  • Заключайте каждую операцию в условную проверку, чтобы скрипт можно было безопасно повторно выполнить

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

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

Использование файлов блокировки для предотвращения одновременных запусков

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

  • Создайте файл блокировки при запуске и удалите его при завершении.
  • Используйте trap, чтобы очистить блокировку, даже если выполнение скрипта было прервано.
  • Команда mkdir для одного пути атомарна в большинстве файловых систем Linux — для блокировок это безопаснее, чем touch.

Без блокировки медленное задание cron и ручной повторный запуск могут столкнуться и повредить общее состояние.

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

Отслеживание выполненных шагов с помощью файла состояния

Для многошаговых скриптов (миграций, установок, подготовки среды) можно отслеживать уже выполненные шаги с помощью файла состояния. Перед выполнением каждый шаг проверяет файл состояния и записывает в него информацию после завершения.

  • Дёшево и переносимо — база данных не требуется.
  • Позволяет сбойному скрипту продолжить работу с места остановки.
  • Храните состояние в предсказуемом месте, например в /var/lib/myapp/ или ~/.myapp/state/.

Этот шаблон используют такие известные инструменты, как apt, cloud-init и платформы миграции баз данных.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

Введение в логику повторных попыток

Внешние вызовы — HTTP-запросы, обращения к DNS, вызовы облачных API — по своей природе ненадёжны. Одиночный сбой не должен прерывать весь скрипт. Логика повторных попыток автоматически повторяет неудачную команду.

  • Наивный вариант: повторять цикл N раз, пока команда не завершится успешно.
  • Всегда задавайте максимальное число повторных попыток, чтобы избежать бесконечных циклов.
  • Записывайте в журнал каждую попытку, чтобы сбои можно было диагностировать.

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

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

Проблема стада

Когда после сбоя многие клиенты одновременно повторяют попытку, возникает эффект стада: все повторяют запросы через один и тот же интервал, обращаясь к серверу в один и тот же момент и делая восстановление невозможным.

  • 100 скриптов повторяют попытку каждые 5 секунд → 100 одновременных запросов каждые 5 секунд.
  • Сервер уже испытывает трудности, а синхронная нагрузка усугубляет ситуацию.
  • Решение — экспоненциальное увеличение интервала: удваивайте время ожидания после каждого сбоя.
  • Добавляйте случайное отклонение, чтобы рассинхронизировать повторные попытки разных клиентов.

Экспоненциальное увеличение интервала со случайным отклонением — отраслевой стандарт, используемый пакетами AWS SDK, клиентами Google Cloud и всеми основными распределёнными системами.

Реализация экспоненциальной задержки

Экспоненциальная задержка увеличивает время ожидания после каждого сбоя по экспоненте: 1 с, 2 с, 4 с, 8 с, 16 с… Это даёт удалённой системе время на восстановление и одновременно снижает общую нагрузку.

Формула: delay = base * (2 ^ attempt)

  • Установите ограничение (максимальную задержку), чтобы время ожидания не росло бесконечно.
  • Параметры для настройки: base_delay, max_delay, max_attempts.

Эта функция пригодна для повторного использования — передавайте ей любую команду в качестве аргументов.

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

Добавление случайного разброса к задержке

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

Две распространённые стратегии добавления случайного разброса:

  • Полный случайный разброс: sleep random(0, cap) — максимальное распределение нагрузки и наименьшая пиковая нагрузка.
  • Равномерный случайный разброс: sleep cap/2 + random(0, cap/2) — гарантирует минимальное ожидание и не позволяет сразу создавать чрезмерную нагрузку.

В Bash используйте $RANDOM (0–32767) для генерации случайных чисел. Приведите их к диапазону задержки с помощью арифметики по модулю.

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

Объединение идемпотентности и повторных попыток в рабочем процессе

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

  1. Получить блокировку (предотвратить параллельные запуски)
  2. Проверить файлы состояния (пропустить завершённые шаги)
  3. Использовать повторные попытки с задержкой для внешних вызовов (загрузка, API, DNS)
  4. Отметить шаги как выполненные только после подтверждённого успеха
  5. Освободить блокировку с помощью trap

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

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

Обработка ошибок, для которых повторные попытки невозможны

Не все ошибки следует устранять повторными попытками. Повторять запрос при ответах 404 Not Found или 403 Forbidden не имеет смысла: без вмешательства человека они никогда не завершатся успешно. Логика повторных попыток должна различать:

  • Временные ошибки — тайм-аут сети, 503 Service Unavailable, сбой DNS → повторить попытку
  • Постоянные ошибки — 401 Unauthorized, 404 Not Found, недопустимые входные данные → немедленно завершить работу с ошибкой

При использовании curl проверяйте код состояния HTTP и пропускайте повторные попытки для ответов 4xx. Используйте --write-out '%{http_code}', чтобы получить код состояния отдельно от тела ответа.

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

Проверка знаний: выбор стратегии задержки

Скрипт развёртывания загружает артефакт выпуска из корзины S3. Во время недавнего инцидента все 80 экземпляров конвейера одновременно завершились с ошибкой из-за кратковременного сбоя S3. Когда S3 восстановилась через 10 секунд, все 80 экземпляров повторили попытку в один и тот же момент, вызвав новую перегрузку и продлив сбой ещё на 3 минуты.

Какая стратегия повторных попыток лучше всего предотвратила бы в будущем такой каскадный эффект «стада»?

Итоги: идемпотентность и повторные попытки с задержкой

В этом уроке Вы научились проектировать скрипты Bash, которые безопасно запускать повторно и которые устойчивы к временным сбоям.

Шаблоны идемпотентности:

  • Предваряйте каждую операцию проверкой существования ([ -f ], [ -d ], id, getent).
  • Используйте mkdir -p и другие встроенные флаги идемпотентности, если они доступны.
  • Используйте файлы блокировки (атомарный mkdir), чтобы предотвращать параллельные запуски.
  • Используйте файлы состояния (файлы-маркеры для каждого шага), чтобы возобновлять работу после сбоя.

Шаблоны повторных попыток с задержкой:

  • Всегда задавайте максимальное число попыток — никогда не повторяйте попытки бесконечно.
  • Используйте экспоненциальную задержку: удваивайте задержку после каждого сбоя.
  • Добавляйте случайный разброс (случайность), чтобы предотвращать синхронизацию по принципу «стада».
  • Различайте временные ошибки (повторять попытку) и постоянные ошибки (немедленно завершать работу с ошибкой).

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

Часто задаваемые вопросы

Урок «Идемпотентные скрипты и логика повторных попыток с задержкой» бесплатный?

Да — полный текст урока «Идемпотентные скрипты и логика повторных попыток с задержкой» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.

Чему я научусь в уроке «Идемпотентные скрипты и логика повторных попыток с задержкой»?

Проектируйте операции, безопасные при повторном запуске, и добавляйте экспоненциальную задержку для нестабильных внешних вызовов Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DevOps Bootcamp?

Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Идемпотентные скрипты и логика повторных попыток с задержкой»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DevOps Bootcamp?

Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Строгий режим с set -euo pipefail
  2. Обработчики trap для очистки и сигналов
  3. Безопасные временные файлы и каталоги блокировок
  4. Идемпотентные скрипты и логика повторных попыток с задержкой
← Назад к DevOps Bootcamp