Introduction
La planification de capacité Bash consiste à estimer et gérer le CPU, la mémoire, les E/S, les descripteurs de fichiers (FD) et le nombre de processus afin que vos scripts s'exécutent de façon prévisible sous charge. Ce guide vous fournit :
- Un flux de travail étape par étape pour dimensionner et faire évoluer en toute sécurité
- Des sondes et formules pratiques exécutables localement
- Un petit plan pilote pour réduire les risques au déploiement
Public cible : développeurs, consultants DevOps et équipes de démarrage qui livrent des tâches basées sur Bash (ETL, étapes CI, traitement de données, tâches de publication) sur ordinateurs portables, serveurs, Docker ou Kubernetes.
Vue d'ensemble du flux de travail
Utilisez une séquence simple et reproductible. Garder les étapes distinctes réduit le retravail et clarifie les décisions.
1. Définir la charge de travail et les SLO
- Profil de travail : nombre d'entrées par unité de temps, taille moyenne, taille de pointe et critères d'acceptation.
- Exemples de SLO : latence P95 par élément, débit horaire et budget d'erreur.
- Modes de défaillance à éviter : OOM kills, forks incontrôlés, épuisement des FD et latences à queue longue.
2. Sonder l'environnement
Créez un inventaire rapide des capacités et limites.
#!/usr/bin/env bash
set -Eeuo pipefail
printf "Sonde environnement\n"
cores=$(getconf _NPROCESSORS_ONLN || echo 1)
mem_kb=$(grep -i MemTotal /proc/meminfo | awk '{print $2}')
mem_mb=$((mem_kb/1024))
fd_limit=$(ulimit -n || true)
proc_limit=$(ulimit -u || true)
avail_disk=$(df -h . | awk 'NR==2{print $4}')
printf "cores=%s\nmem_mb=%s\nfd_limit=%s\nproc_limit=%s\navail_disk=%s\n" \
"$cores" "$mem_mb" "$fd_limit" "$proc_limit" "$avail_disk"
# Utilisation FD actuelle (utilise $$ pour le PID du shell courant)
printf "fds_now=%s\n" "$(ls -1 /proc/$$/fd | wc -l)"
Note : Dans les conteneurs, les valeurs ulimit peuvent différer de l'hôte. Définissez-les explicitement au besoin (Docker : --ulimit, Kubernetes : ulimit au niveau conteneur via securityContext si votre distribution le supporte).
3. Mesurer le coût unitaire en ressources
Choisissez un travail représentatif et mesurez le temps CPU, le RSS (mémoire résidente) et l'usage des FD.
#!/usr/bin/env bash
# sample_job.sh : remplacez le corps par votre véritable unité de travail
set -Eeuo pipefail
in="$1"
# Charge exemple : hachage puis compression pour simuler CPU + E/S
sha256sum "$in" >/dev/null
gzip -c "$in" >/dev/null
#!/usr/bin/env bash
# measure_cost.sh : exécute le travail et capture CPU et mémoire approximatifs
set -Eeuo pipefail
item="$1"
# Préférez /usr/bin/time -v si disponible pour le Max RSS
if command -v /usr/bin/time >/dev/null; then
/usr/bin/time -v bash sample_job.sh "$item" 2>_time.txt 1>/dev/null || true
cpu_user=$(awk -F: '/User time/ {gsub(/ /,""); print $2}' _time.txt)
cpu_sys=$(awk -F: '/System time/ {gsub(/ /,""); print $2}' _time.txt)
rss_kb=$(awk -F: '/Maximum resident set size/ {gsub(/ /,""); print $2}' _time.txt)
printf "cpu_user=%s cpu_sys=%s rss_kb=%s\n" "$cpu_user" "$cpu_sys" "$rss_kb"
else
# Repli : time grossier et instantané RSS pour le shell courant
t0=$(date +%s)
bash sample_job.sh "$item"
t1=$(date +%s)
rss_kb=$(awk '/VmRSS/ {print $2}' /proc/$$/status 2>/dev/null || echo 0)
printf "elapsed_s=%s rss_kb=%s\n" "$((t1-t0))" "$rss_kb"
fi
Exécutez le script de mesure sur 20 à 50 éléments représentatifs et calculez médianes, P95 et maximums. Utilisez des nombres conservateurs (P95 ou max) plus marges de sécurité.
4. Construire un modèle de capacité simple
Dimensionnement mémoire
- Soit
rss_per_job_kble RSS P95 en KB. - Soit
avail_mem_kble total moins réserve OS/applications. - Marge de sécurité : gardez 25 % de mémoire libre.
concurrency_mem = floor((avail_mem_kb * 0.75) / rss_per_job_kb)
Exemple :
- Mémoire totale : 8 Go → 8 × 1024 × 1024 = 8 388 608 KB
- Réserve 25 % → 6 291 456 KB utilisables
rss_per_job_kb= 80 000concurrency_mem = floor(6 291 456 / 80 000) = 78
Dimensionnement CPU
- Soit
cores= cœurs logiques. - Soit
cpu_sec_per_item= secondes utilisateur + système par élément à concurrence 1×. - Budget 70 % CPU pour laisser de la marge.
max_items_per_sec_cpu = (cores * 0.7) / cpu_sec_per_itemconcurrency_cpu = floor(cores * 0.7)si chaque travail occupe un cœur.
Exemple :
cores = 8,cpu_sec_per_item = 0,2 smax_items_per_sec_cpu = (8 * 0,7) / 0,2 = 28 éléments/s
Dimensionnement descripteurs de fichiers
fd_limitdepuisulimit -n.- Chaque étape de pipeline peut consommer des FD (stdin/stdout/stderr, pipes, fichiers ouverts).
- Mesure : avant et pendant le travail, lancez
ls /proc/$$/fd | wc -l. concurrency_fd = floor((fd_limit * 0.8) / fds_per_job)
Limites processus et threads
proc_limitdepuisulimit -u. Comptez les enfants avecps --ppid.- Gardez 30 % de marge :
enfants <= proc_limit * 0.7.
E/S disque et réseau
dd if=/dev/zero of=./_io_test.bin bs=1M count=512 oflag=direct 2>&1 | tail -1
Utilisez dd pour vérifier grossièrement la vitesse d'écriture. Pour les lectures, chronométrez cat ou sha256sum sur un gros fichier. Assurez-vous que votre débit cible reste sous 70 % de la bande passante observée en régime permanent.
5. Décider le parallélisme et la montée en charge
- Pour le parallélisme local : utilisez
xargs -Pou GNU parallel pour plafonner la concurrence. - Pour le partitionnement : divisez l'entrée en N parties et traitez avec N workers.
- Préférez le parallélisme à gros grain (niveau processus) au lancement d'un sous-shell pour chaque jeton.
Exemple de harnais avec xargs :
#!/usr/bin/env bash
set -Eeuo pipefail
P="${P:-8}" # plafond de parallélisme
LIST="${1:-worklist.txt}
export -f sample_job.sh || true
# Chaque ligne dans worklist.txt est un chemin à traiter
cat "$LIST" | xargs -r -P "$P" -n 1 -I{} bash sample_job.sh {}
6. Ajouter garde-fous et marges de sécurité
set -Eeuo pipefailpour échouer vite et réduire le travail partiel.- Réglages ulimit (valeurs d'exemple, ajustez pour votre hôte) :
# Plafonds souples pour éviter l'impact sur l'hôte partagé
ulimit -S -n 4096 # FD
ulimit -S -u 2048 # processus
ulimit -S -m $((6*1024*1024)) || true # mémoire KB, si supporté
- Contre-pression : plafonnez la profondeur de file et les workers parallèles.
- Politique de reprise : backoff exponentiel pour erreurs E/S transitoires.
7. Observer les signaux qui pilotent les décisions de mise à l'échelle
- Latence : moyenne mobile et P95 par élément qui augmentent dans le temps.
- Profondeur de file : la liste de travail grossit plus vite que la completion.
- Saturation ressources :
vmstat r > coressur longues périodes, CPU > 90 %, attente E/S > 20 % en continu. - Erreurs : sorties non-nulles, OOM kills, pipes cassés.
Traceur de latence simple :
#!/usr/bin/env bash
set -Eeuo pipefail
log=_latency.csv
echo "ts_s, elapsed_ms" > "$log"
while read -r item; do
t0=$(date +%s%3N)
bash sample_job.sh "$item"
t1=$(date +%s%3N)
echo "$((t0/1000)),$((t1 - t0))" >> "$log"
done < worklist.txt
8. Notes conteneurs et clusters (Docker, Compose, Kubernetes)
- Docker : définissez plafonds CPU et mémoire pour que vos calculs correspondent à la réalité, ex.
--cpus,--memory,--ulimit nofile=4096. - Compose : répliquez ces limites dans la définition du service.
- Kubernetes : requests et limits doivent refléter vos budgets mesurés ; surveillez le throttling CPU si les limites sont serrées.
- Kubernetes Ingress et Secrets sont sans lien avec les limites de calcul Bash, mais vos travaux peuvent récupérer des entrées via HTTP ou lire des secrets ; évitez de récupérer des secrets dans des boucles serrées.
Exemples pratiques : formules et extraits
Calculateur de concurrence lié mémoire
#!/usr/bin/env bash
set -Eeuo pipefail
rss_kb=${1:?"rss_kb par travail requis"}
avail_kb=$(grep -i MemAvailable /proc/meminfo | awk '{print $2}')
# Marge de sécurité 25 %
usable_kb=$(( (avail_kb * 75) / 100 ))
conc=$(( usable_kb / rss_kb ))
printf "avail_kb=%s usable_kb=%s rss_kb=%s concurrency=%s\n" \
"$avail_kb" "$usable_kb" "$rss_kb" "$conc"
Estimateur de débit CPU
#!/usr/bin/env bash
set -Eeuo pipefail
cores=$(getconf _NPROCESSORS_ONLN || echo 1)
cpu_sec_per_item=${1:?"secondes CPU par élément"}
budget=$(python3 - <<EOF
cores=$cores
cpi=$cpu_sec_per_item
print(round((cores*0.7)/cpi,2))
EOF
)
printf "cores=%s items_per_sec_cpu_budget=%s\n" "$cores" "$budget"
Si Python n'est pas disponible, calculez à la main ou avec bc.
Vérification FD de sécurité dans un pipeline
#!/usr/bin/env bash
set -Eeuo pipefail
before=$(ls /proc/$$/fd | wc -l)
# Simule un petit fanout
{ find . -maxdepth 1 -type f -print0 | xargs -0 -P 8 -n 1 -I{} bash -c 'cat "{}" >/dev/null'; }
after=$(ls /proc/$$/fd | wc -l)
limit=$(ulimit -n)
printf "fd_before=%s fd_after=%s fd_limit=%s\n" "$before" "$after" "$limit"
Motif xargs sécurisé
find input/ -type f -print0 | xargs -0 -P "$P" -n 1 -I{} bash -c '
set -Eeuo pipefail
f="$1"
# Évite de lire tout le fichier en mémoire
sha256sum "$f" | awk "{print \$1}" >> hashes.txt
' _ {}
Liste de contrôle des risques de croissance
- Évitez les boucles O(n²) sur des répertoires grandissants ; préférez l'indexation et les scans en un seul passage.
- N'engloutissez pas les fichiers entiers en mémoire ; streamez avec
while read -rouawk. - Évitez le globbing non borné ; utilisez
findavec limites de taille et profondeur. - Utilisez
-print0avecxargs -0pour gérer les espaces sans risque et réduire les bogues d'analyse. - Plafonnez les reprises et le parallélisme pour éviter les effets de meute.
Plan pilote local
Rendez le premier pilote étroit, mesurable et facile à inspecter localement avant tout déploiement.
Périmètre
- Un type de travail critique, 200 éléments représentatifs, taille max 64 Mo.
- Parallélisme fixe P dans {1, 4, 8}.
Critères de réussite ou échec
- Latence P95 par élément ≤ 300 ms à P=4.
- Zéro OOM, zéro épuisement FD, sorties non-nulles ≤ 0,5 %.
- CPU régime permanent ≤ 70 %, mémoire libre ≥ 25 %.
Fichiers pilotes
worklist.txt : liste des chemins d'entrée.
pilot.sh : orchestre l'exécution et collecte les métriques.
#!/usr/bin/env bash
set -Eeuo pipefail
P="${P:-4}
LIST="${1:-worklist.txt}
mkdir -p _pilot
: > _pilot/latency.csv
: > _pilot/errors.log
run_item() {
local item="$1"
local t0 t1 rc
t0=$(date +%s%3N)
if bash sample_job.sh "$item\); then
t1=$(date +%s%3N)
echo "$((t0/1000)),$((t1-t0))" >> _pilot/latency.csv
else
rc=$?
echo "$(date -Is) item=$item rc=$rc" >> _pilot/errors.log
fi
}
export -f run_item
cp "$LIST" _pilot/worklist.run
cat _pilot/worklist.run | xargs -r -P "$P" -n 1 -I{} bash -c 'run_item "$@"' _ {}
# Résumés
items=$(wc -l < _pilot/worklist.run)
errs=$(wc -l < _pilot/errors.log 2>/dev/null || echo 0)
lat_p95=$(awk -F, 'NR>1{a[NR-1]=$2} END{n=asort(a); idx=int(0.95*n); if(idx<1) idx=1; print a[idx]}' _pilot/latency.csv 2>/dev/null || echo 0)
free_mem_mb=$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo)
printf "items=%s errors=%s p95_ms=%s free_mem_mb=%s\n" "$items" "$errs" "$lat_p95" "$free_mem_mb"
Boucle d'ajustement
- Si p95_ms est élevé : diminuez P ou réduisez le CPU par élément (optimisez le travail).
- Si free_mem_mb est bas : réduisez P ou la mémoire par élément.
- Si les erreurs montent : inspectez
_pilot/errors.loget ajoutez reprises ou validation d'entrée.
Sécurité et retour arrière
- P=1 par défaut si un contrôle échoue.
- Gardez les ulimits souples dans le pilote pour borner l'impact sur les hôtes partagés.
Conclusion
Vous disposez maintenant d'une méthode concrète pour dimensionner les charges de travail Bash :
- Mesurez les coûts CPU, mémoire, FD et E/S par élément.
- Modélisez la concurrence avec formules simples et 25 % à 30 % de marge.
- Appliquez des garde-fous avec ulimit et parallélisme borné.
- Surveillez latence, profondeur de file, saturation et erreurs pour décider quand monter en charge.
Prochaines étapes
- Lancez le pilote local, enregistrez latences P95 et usage ressources, et définissez votre P par défaut.
- Documentez les budgets régime permanent et pointe pour CPU, mémoire, FD et processus.
- Si vous passez à Docker ou Kubernetes, reportez les mêmes limites et marges dans les spécifications des conteneurs.