0Pricing
DevOps Bootcamp · Lekcja

Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem

Projektuj operacje bezpieczne do ponownego uruchamiania i dodawaj wykładnicze opóźnienie między próbami w przypadku zawodnych wywołań zewnętrznych.

Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem to bezpłatna lekcja DevOps Bootcamp na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DevOps Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.

Czym jest idempotencja i dlaczego ma znaczenie

Idempotencja oznacza, że wielokrotne wykonanie tej samej operacji daje taki sam wynik jak wykonanie jej jeden raz. W skryptach Bash ma to kluczowe znaczenie, ponieważ skrypty ulegają awariom, sieć może zostać rozłączona, a użytkownicy mogą przypadkowo uruchomić polecenia ponownie.

  • Skrypt nieidempotentny, który dwukrotnie tworzy użytkownika, może zakończyć się błędem lub utworzyć zduplikowane dane.
  • Skrypt idempotentny najpierw sprawdza: czy ten element już istnieje?
  • Skryptów idempotentnych można bezpiecznie używać w zadaniach cron, potokach CI i pętlach ponawiania.

Złota zasada brzmi: proszę sprawdzić przed wykonaniem działania. Każda operacja destrukcyjna lub tworząca zasoby powinna być zabezpieczona testem warunku wstępnego.

Zabezpieczanie tworzenia plików i katalogów

Najczęstszy wzorzec idempotencji polega na sprawdzeniu, czy zasób już istnieje, przed jego utworzeniem. Bash udostępnia zwięzłe konstrukcje jednolinijkowe do tego celu.

  • [ -d dir ] — prawda, jeśli katalog istnieje
  • [ -f file ] — prawda, jeśli istnieje zwykły plik
  • mkdir -p — tworzy katalog tylko wtedy, gdy go nie ma (wbudowana idempotencja)

Jeśli są dostępne, należy preferować wbudowane opcje, takie jak -p i --no-clobber, zamiast ręcznych sprawdzeń — są atomowe i odporne na warunki wyścigu.

#!/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

Idempotentne zarządzanie użytkownikami i grupami

Zadania administracji systemem, takie jak dodawanie użytkowników lub grup, muszą być idempotentne — ponowne uruchomienie skryptu na tym samym komputerze nie powinno kończyć się błędem ani tworzyć duplikatów.

  • id username zwraca 0, jeśli użytkownik istnieje
  • getent group groupname sprawdza, czy istnieje grupa
  • Każdą operację należy opakować warunkiem ochronnym, aby skrypt można było bezpiecznie uruchamiać ponownie

Ten wzorzec stanowi podstawę narzędzi do zarządzania konfiguracją, takich jak Ansible — każde zadanie jest operacją zabezpieczoną warunkiem i idempotentną.

#!/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

Używanie plików blokad do zapobiegania równoczesnym uruchomieniom

Nawet skrypt idempotentny może powodować problemy, jeśli dwie jego instancje działają jednocześnie. Plik blokady gwarantuje, że w danym momencie działa tylko jedna instancja.

  • Należy utworzyć plik blokady podczas uruchamiania i usunąć go przy zakończeniu.
  • Należy użyć trap, aby wyczyścić blokadę nawet wtedy, gdy skrypt zostanie przerwany.
  • Wywołanie mkdir dla pojedynczej ścieżki jest atomowe w większości systemów plików Linux — jest bezpieczniejsze niż touch w przypadku blokowania.

Bez blokady wolno działające zadanie cron i ręczne ponowne uruchomienie mogą na siebie nachodzić i uszkodzić współdzielony stan.

#!/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."

Śledzenie ukończonych kroków za pomocą pliku stanu

W przypadku skryptów wieloetapowych (migracji, instalacji, aprowizacji) można śledzić ukończone kroki za pomocą pliku stanu. Każdy krok sprawdza plik stanu przed wykonaniem i zapisuje w nim informację po zakończeniu.

  • Rozwiązanie jest tanie i przenośne — nie wymaga bazy danych.
  • Umożliwia wznowienie działania skryptu od miejsca, w którym został przerwany.
  • Stan należy przechowywać w przewidywalnej lokalizacji, takiej jak /var/lib/myapp/ lub ~/.myapp/state/.

Ten wzorzec jest używany przez ważne narzędzia, takie jak apt, cloud-init i platformy migracji baz danych.

#!/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...'"

Wprowadzenie do logiki ponawiania

Wywołania zewnętrzne — żądania HTTP, zapytania DNS i wywołania API chmury — są z natury zawodne. Pojedyncza awaria nie powinna przerywać całego skryptu. Logika ponawiania automatycznie wykonuje nieudaną operację ponownie.

  • Proste ponawianie: pętla wykonuje polecenie N razy, aż zakończy się ono powodzeniem.
  • Należy zawsze ustawić maksymalną liczbę prób, aby uniknąć nieskończonych pętli.
  • Należy rejestrować każdą próbę, aby można było zdiagnozować awarie.

Najprostsza funkcja ponawiania opakowuje dowolne polecenie i wykonuje je ponownie określoną liczbę razy, ze stałym opóźnieniem. W wielu przypadkach to wystarcza, ale przy dużym obciążeniu ma poważną wadę — omówioną w następnej części.

#!/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."

Problem szturmującego tłumu

Gdy wielu klientów jednocześnie ponawia próbę po awarii, tworzą szturmujący tłum — wszyscy ponawiają wywołania w tych samych stałych odstępach, uderzając w serwer dokładnie w tej samej chwili i uniemożliwiając odzyskanie sprawności.

  • 100 skryptów ponawia próbę co 5 sekund → 100 jednoczesnych żądań co 5 sekund.
  • Serwer już ma problemy; zsynchronizowane obciążenie jeszcze je pogarsza.
  • Rozwiązaniem jest wykładnicze wydłużanie odstępów: po każdej awarii należy podwoić czas oczekiwania.
  • Należy dodać losowe odchylenie (jitter), aby desynchronizować ponowienia u poszczególnych klientów.

Wykładnicze wydłużanie odstępów z losowym odchyleniem to standard branżowy używany przez zestawy SDK AWS, klientów Google Cloud i wszystkie najważniejsze systemy rozproszone.

Implementowanie wykładniczego backoffu

Wykładniczy backoff wykładniczo zwiększa czas oczekiwania po każdej awarii: 1 s, 2 s, 4 s, 8 s, 16 s... Daje to systemowi zdalnemu czas na odzyskanie sprawności, jednocześnie zmniejszając całkowite obciążenie.

Wzór: delay = base * (2 ^ attempt)

  • Ustaw limit (maksymalne opóźnienie), aby czas oczekiwania nie rósł bez ograniczeń.
  • Parametry do dostrojenia: base_delay, max_delay, max_attempts.

Ta funkcja nadaje się do ponownego użycia — przekaż do niej dowolne polecenie jako argument.

#!/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"

Dodawanie jittera do backoffu

Jitter dodaje losowość do opóźnienia backoffu. Nawet przy wykładniczym backoffie wszystkie klienty, które rozpoczną działanie w tym samym czasie, nadal będą ponawiać próby synchronicznie. Jitter przerywa tę synchronizację.

Dwie popularne strategie jittera:

  • Pełny jitter: sleep random(0, cap) — maksymalnie rozprasza żądania i zapewnia najmniejsze szczytowe obciążenie.
  • Równy jitter: sleep cap/2 + random(0, cap/2) — gwarantuje minimalny czas oczekiwania i zapobiega natychmiastowemu przeciążeniu systemu.

W Bashu użyj $RANDOM (0–32767) do generowania liczb losowych. Przeskaluj je do zakresu opóźnienia za pomocą arytmetyki modulo.

#!/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."

Łączenie idempotencji i ponawiania prób w rzeczywistym przepływie pracy

W skryptach produkcyjnych idempotencja i logika ponawiania prób współdziałają ze sobą. Typowy przepływ wdrażania może wyglądać tak:

  1. Uzyskanie blokady (zapobieganie równoczesnym uruchomieniom)
  2. Sprawdzenie plików stanu (pomijanie ukończonych kroków)
  3. Użycie ponawiania prób z backoffem w przypadku wywołań zewnętrznych (pobieranie, API, DNS)
  4. Oznaczenie kroków jako ukończonych dopiero po potwierdzeniu powodzenia
  5. Zwolnienie blokady za pomocą trap

Takie połączenie sprawia, że skrypty są bezpieczne do ponownego uruchomienia w dowolnym momencie — po awarii, przekroczeniu limitu czasu lub ręcznym przerwaniu — bez pozostawiania systemu w niespójnym stanie.

#!/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."

Obsługa błędów, których nie należy ponawiać

Nie wszystkie błędy należy obsługiwać przez ponawianie prób. Ponawianie próby w przypadku odpowiedzi 404 Not Found lub 403 Forbidden jest nieefektywne — bez ingerencji człowieka nigdy nie zakończą się powodzeniem. Logika ponawiania prób powinna rozróżniać:

  • Błędy przejściowe — przekroczenie limitu czasu sieci, 503 Service Unavailable, błąd DNS → ponów próbę
  • Błędy trwałe — 401 Unauthorized, 404 Not Found, nieprawidłowe dane wejściowe → zakończ natychmiast niepowodzeniem

W przypadku curl sprawdzaj kod stanu HTTP i pomijaj ponawianie prób dla odpowiedzi 4xx. Użyj --write-out '%{http_code}', aby przechwycić stan niezależnie od treści odpowiedzi.

#!/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"

Sprawdzenie wiedzy: wybór strategii backoffu

Skrypt wdrożeniowy pobiera artefakt wydania z bucketa S3. Podczas niedawnego incydentu 80 instancji potoku zakończyło działanie niepowodzeniem jednocześnie z powodu krótkiej awarii S3. Gdy S3 odzyskało sprawność po 10 sekundach, wszystkie 80 instancji ponowiły próbę w tej samej chwili, powodując kolejne przeciążenie i przedłużając awarię o 3 minuty.

Jaka strategia ponawiania prób najlepiej zapobiegłaby takiej kaskadzie typu „lawina żądań” podczas przyszłych incydentów?

Podsumowanie: idempotencja i ponawianie prób z backoffem

W tej lekcji nauczyli się Państwo projektować skrypty Bash, które są bezpieczne do ponownego uruchomienia i odporne na przejściowe awarie.

Wzorce idempotencji:

  • Zabezpieczaj każdą operację sprawdzeniem istnienia ([ -f ], [ -d ], id, getent).
  • Używaj mkdir -p i innych dostępnych wbudowanych flag zapewniających idempotencję.
  • Używaj plików blokad (atomowego mkdir), aby zapobiegać równoczesnym uruchomieniom.
  • Używaj plików stanu (plików znaczników dla poszczególnych kroków), aby umożliwić wznowienie działania po awarii.

Wzorce ponawiania prób z backoffem:

  • Zawsze ustawiaj maksymalną liczbę prób — nigdy nie ponawiaj prób bez końca.
  • Używaj wykładniczego backoffu: po każdej awarii podwajaj czas oczekiwania.
  • Dodawaj jitter (losowość), aby zapobiegać synchronizacji typu „lawina żądań”.
  • Rozróżniaj błędy przejściowe (ponawiaj próbę) od trwałych (kończ działanie natychmiast).

Połączenie tych wzorców pozwala tworzyć skrypty gotowe do użycia produkcyjnego: bezpieczne, obserwowalne i samonaprawiające się.

Często zadawane pytania

Czy lekcja „Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem” jest bezpłatna?

Tak — pełny tekst „Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DevOps Bootcamp, przejdź na CoddyKit PRO. Kurs DevOps Bootcamp zawiera 4 lekcji w sumie.

Co nauczysz się w „Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem”?

Projektuj operacje bezpieczne do ponownego uruchamiania i dodawaj wykładnicze opóźnienie między próbami w przypadku zawodnych wywołań zewnętrznych. Ćwiczysz DevOps Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DevOps Bootcamp?

Nie wymagamy żadnego doświadczenia. DevOps Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DevOps Bootcamp?

Tak. Każda lekcja DevOps Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Tryb ścisły z set -euo pipefail
  2. Procedury trap do sprzątania i obsługi sygnałów
  3. Bezpieczne pliki tymczasowe i katalogi blokad
  4. Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem
← Powrót do DevOps Bootcamp