Идемпотентные скрипты и логика повторных попыток с задержкой
Проектируйте операции, безопасные при повторном запуске, и добавляйте экспоненциальную задержку для нестабильных внешних вызовов
«Идемпотентные скрипты и логика повторных попыток с задержкой» — бесплатный урок Linux Command Line & Bash Scripting Mastery на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Linux Command Line & Bash Scripting Mastery, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Linux Command Line & Bash Scripting Mastery содержит 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."Объединение идемпотентности и повторных попыток в рабочем процессе
В рабочих скриптах идемпотентность и логика повторных попыток работают вместе. Типичный рабочий процесс развёртывания может выглядеть так:
- Получить блокировку (предотвратить параллельные запуски)
- Проверить файлы состояния (пропустить завершённые шаги)
- Использовать повторные попытки с задержкой для внешних вызовов (загрузка, API, DNS)
- Отметить шаги как выполненные только после подтверждённого успеха
- Освободить блокировку с помощью
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) и разблокировать остальной курс Linux Command Line & Bash Scripting Mastery, подпишись на CoddyKit PRO. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.
Чему я научусь в уроке «Идемпотентные скрипты и логика повторных попыток с задержкой»?
Проектируйте операции, безопасные при повторном запуске, и добавляйте экспоненциальную задержку для нестабильных внешних вызовов Ты практикуешь Linux Command Line & Bash Scripting Mastery с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Linux Command Line & Bash Scripting Mastery?
Предыдущий опыт не требуется. Linux Command Line & Bash Scripting Mastery на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Идемпотентные скрипты и логика повторных попыток с задержкой»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Linux Command Line & Bash Scripting Mastery?
Да. Каждый урок Linux Command Line & Bash Scripting Mastery включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Строгий режим с set -euo pipefail
- Обработчики trap для очистки и сигналов
- Безопасные временные файлы и каталоги блокировок
- Идемпотентные скрипты и логика повторных попыток с задержкой