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 plikmkdir -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."
fiIdempotentne 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 usernamezwraca 0, jeśli użytkownik istniejegetent group groupnamesprawdza, 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."
fiUż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
mkdirdla pojedynczej ścieżki jest atomowe w większości systemów plików Linux — jest bezpieczniejsze niżtouchw 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:
- Uzyskanie blokady (zapobieganie równoczesnym uruchomieniom)
- Sprawdzenie plików stanu (pomijanie ukończonych kroków)
- Użycie ponawiania prób z backoffem w przypadku wywołań zewnętrznych (pobieranie, API, DNS)
- Oznaczenie kroków jako ukończonych dopiero po potwierdzeniu powodzenia
- 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 -pi 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
- Tryb ścisły z set -euo pipefail
- Procedury trap do sprzątania i obsługi sygnałów
- Bezpieczne pliki tymczasowe i katalogi blokad
- Skrypty idempotentne i logika ponawiania z narastającym opóźnieniem