Script idempotenti e logica di retry con backoff
Progetti operazioni sicure da rieseguire e aggiunga un backoff esponenziale per le chiamate esterne instabili.
Script idempotenti e logica di retry con backoff è una lezione DevOps Bootcamp gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DevOps Bootcamp, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DevOps Bootcamp include 4 lezioni in totale.
Che cos’è l’idempotenza e perché è importante
Idempotenza significa che l’esecuzione ripetuta della stessa operazione produce lo stesso risultato di una singola esecuzione. Negli script Bash è fondamentale perché gli script possono arrestarsi, le reti possono interrompersi e gli utenti possono ripetere accidentalmente un’operazione.
- Uno script non idempotente che crea due volte un utente può fallire o duplicare i dati.
- Uno script idempotente esegue prima un controllo: esiste già?
- Gli script idempotenti possono essere utilizzati senza rischi nei cron job, nelle pipeline CI e nei cicli di retry.
La regola d’oro è: controlli prima di agire. Ogni operazione distruttiva o di creazione deve essere protetta da un controllo della precondizione.
Proteggere la creazione di file e directory
Il pattern di idempotenza più comune consiste nel verificare se una risorsa esiste già prima di crearla. Bash offre comandi concisi per farlo.
[ -d dir ]— vero se la directory esiste[ -f file ]— vero se il file normale esistemkdir -p— crea la directory solo se non esiste (idempotenza integrata)
Quando disponibili, preferisca i flag integrati come -p e --no-clobber ai controlli manuali: sono atomici e sicuri rispetto alle race condition.
#!/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."
fiGestione idempotente di utenti e gruppi
Le attività di amministrazione del sistema, come l’aggiunta di utenti o gruppi, devono essere idempotenti: rieseguire lo script sulla stessa macchina non deve generare errori né creare duplicati.
id usernamerestituisce 0 se l’utente esistegetent group groupnameverifica l’esistenza di un gruppo- Racchiuda ogni operazione in un controllo, così lo script può essere rieseguito senza rischi
Questo pattern è alla base di strumenti di gestione della configurazione come Ansible: ogni attività è un’operazione protetta e idempotente.
#!/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."
fiUtilizzare file di lock per impedire le esecuzioni simultanee
Anche uno script idempotente può causare problemi se due istanze vengono eseguite contemporaneamente. Un file di lock garantisce che venga eseguita una sola istanza alla volta.
- Crei un file di lock all’avvio e lo rimuova alla terminazione.
- Utilizzi un
trapper pulire il lock anche se lo script viene interrotto. mkdirsu un singolo percorso è atomico nella maggior parte dei filesystem Linux: è più sicuro ditouchper il locking.
Senza un lock, un cron job lento e una riesecuzione manuale possono entrare in conflitto e corrompere lo stato condiviso.
#!/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."Tracciare i passaggi completati con un file di stato
Per gli script composti da più passaggi, come migrazioni, installazioni e provisioning, è possibile tenere traccia dei passaggi già completati utilizzando un file di stato. Ogni passaggio controlla il file di stato prima dell’esecuzione e vi scrive al termine.
- È economico e portabile: non richiede un database.
- Consente a uno script fallito di riprendere dal punto in cui si era interrotto.
- Memorizzi lo stato in un percorso prevedibile, come
/var/lib/myapp/o~/.myapp/state/.
Questo pattern è utilizzato da strumenti importanti come apt, cloud-init e i framework per le migrazioni dei database.
#!/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...'"Introduzione alla logica di retry
Le chiamate esterne, come richieste HTTP, ricerche DNS e chiamate alle API cloud, sono intrinsecamente inaffidabili. Un singolo errore non dovrebbe interrompere l’intero script. La logica di retry riprova automaticamente un comando fallito.
- Retry ingenuo: ripetere il comando per N volte finché non riesce.
- Imposti sempre un numero massimo di tentativi per evitare cicli infiniti.
- Registri ogni tentativo, così gli errori possono essere diagnosticati.
La funzione di retry più semplice avvolge un comando qualsiasi e lo riprova fino a un numero fisso di volte, con un ritardo costante. È sufficiente per molti casi d’uso, ma presenta un difetto critico sotto carico, che verrà illustrato nel prossimo argomento.
#!/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."Il problema del thundering herd
Quando molti client effettuano contemporaneamente un retry dopo un errore, creano un thundering herd: eseguono tutti il retry allo stesso intervallo fisso, sovraccaricando il server esattamente nello stesso momento e rendendo impossibile il recupero.
- 100 script eseguono il retry ogni 5 secondi → 100 richieste simultanee ogni 5 secondi.
- Il server è già in difficoltà; il carico sincronizzato peggiora la situazione.
- La soluzione è il backoff esponenziale: raddoppiare il tempo di attesa dopo ogni errore.
- Aggiunga il jitter (una variazione casuale) per desincronizzare i retry tra i client.
Il backoff esponenziale con jitter è lo standard del settore, utilizzato dagli SDK AWS, dai client Google Cloud e dai principali sistemi distribuiti.
Implementazione del backoff esponenziale
Il backoff esponenziale aumenta esponenzialmente il tempo di attesa dopo ogni errore: 1 s, 2 s, 4 s, 8 s, 16 s... In questo modo il sistema remoto ha il tempo di riprendersi, riducendo al contempo il carico complessivo.
La formula: delay = base * (2 ^ attempt)
- Impostate un limite massimo (ritardo massimo), in modo che i tempi di attesa non crescano senza limiti.
- Parametri da regolare:
base_delay,max_delay,max_attempts.
Questa funzione è riutilizzabile: passatele qualsiasi comando come argomento.
#!/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"Aggiunta del jitter al backoff
Il jitter aggiunge casualità al ritardo del backoff. Anche con il backoff esponenziale, se tutti i client iniziano nello stesso momento, continueranno a riprovare sincronizzati. Il jitter interrompe questa sincronizzazione.
Due strategie comuni per il jitter:
- Jitter completo:
sleep random(0, cap)— distribuzione massima, picco di carico minimo. - Jitter uniforme:
sleep cap/2 + random(0, cap/2)— garantisce un'attesa minima ed evita di sovraccaricare immediatamente il sistema.
In Bash, usate $RANDOM (0–32767) per generare numeri casuali. Ridimensionateli in base all'intervallo del ritardo usando l'aritmetica modulare.
#!/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."Combinazione di idempotenza e retry in un flusso di lavoro reale
Negli script di produzione, l'idempotenza e la logica di retry lavorano insieme. Un tipico flusso di distribuzione potrebbe:
- Acquisire un lock (per impedire esecuzioni simultanee)
- Controllare i file di stato (per saltare i passaggi completati)
- Usare il retry con backoff per le chiamate esterne (download, API, DNS)
- Contrassegnare i passaggi come completati solo dopo averne confermato il successo
- Rilasciare il lock tramite
trap
Questa combinazione rende gli script sicuri da eseguire nuovamente in qualsiasi momento, dopo un arresto anomalo, un timeout o un'interruzione manuale, senza lasciare il sistema in uno stato non funzionante.
#!/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."Gestione degli errori non ripetibili
Non tutti gli errori devono essere sottoposti a retry. Riprovare in caso di 404 Not Found o 403 Forbidden è inutile: senza l'intervento umano, queste richieste non avranno mai successo. La logica di retry deve distinguere tra:
- Errori transitori — timeout di rete, 503 Service Unavailable, errore DNS → riprovare
- Errori permanenti — 401 Unauthorized, 404 Not Found, input non valido → fallire immediatamente
Con curl, controllate il codice di stato HTTP e ignorate i retry per le risposte 4xx. Usate --write-out '%{http_code}' per acquisire lo stato separatamente dal corpo della risposta.
#!/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"Verifica delle conoscenze: scelta della strategia di backoff
Uno script di distribuzione scarica un artefatto di release da un bucket S3. Durante un incidente recente, 80 istanze della pipeline hanno avuto un errore simultaneamente a causa di una breve interruzione di S3. Quando S3 si è ripristinato dopo 10 secondi, tutte le 80 istanze hanno riprovato nello stesso momento, causando un altro sovraccarico e prolungando l'interruzione di 3 minuti.
Quale strategia di retry sarebbe più efficace per evitare questa cascata da effetto gregge in incidenti futuri?
Riepilogo: idempotenza e retry con backoff
In questa lezione avete imparato a progettare script Bash sicuri da eseguire nuovamente e resilienti agli errori transitori.
Pattern di idempotenza:
- Proteggete ogni operazione con un controllo di esistenza (
[ -f ],[ -d ],id,getent). - Usate
mkdir -pe gli altri flag idempotenti integrati, quando disponibili. - Usate file di lock (
mkdiratomico) per impedire esecuzioni simultanee. - Usate file di stato (file indicatore per ogni passaggio) per consentire la ripresa dopo un errore.
Pattern di retry con backoff:
- Impostate sempre un numero massimo di tentativi: non riprovate mai all'infinito.
- Usate il backoff esponenziale: raddoppiate il ritardo dopo ogni errore.
- Aggiungete il jitter (casualità) per impedire la sincronizzazione da effetto gregge.
- Distinguete gli errori transitori (riprovare) da quelli permanenti (fallire rapidamente).
La combinazione di questi pattern produce script pronti per la produzione: sicuri, osservabili e capaci di autoripararsi.
Domande Frequenti
La lezione «Script idempotenti e logica di retry con backoff» è gratuita?
Sì — il testo completo di «Script idempotenti e logica di retry con backoff» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DevOps Bootcamp, passa a CoddyKit PRO. Il corso DevOps Bootcamp include 4 lezioni in totale.
Cosa imparerò in «Script idempotenti e logica di retry con backoff»?
Progetti operazioni sicure da rieseguire e aggiunga un backoff esponenziale per le chiamate esterne instabili. Eserciti DevOps Bootcamp con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare DevOps Bootcamp?
Non è richiesta alcuna esperienza precedente. DevOps Bootcamp su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Script idempotenti e logica di retry con backoff»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione DevOps Bootcamp?
Sì. Ogni lezione DevOps Bootcamp include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Modalità rigorosa con set -euo pipefail
- Gestori trap per pulizia e segnali
- File temporanei sicuri e directory di lock
- Script idempotenti e logica di retry con backoff