Ejecución Paralela de Jobs con Bash
Ejecuta comandos y scripts de shell en paralelo de forma segura usando xargs, GNU parallel o jobs en segundo plano.
Visión General
Los servidores modernos tienen múltiples núcleos, pero un script de shell ingenuo ejecuta un comando a la vez. La ejecución paralela de jobs te permite procesar muchos archivos, URLs o tareas simultáneamente, reduciendo el tiempo de horas a minutos. Bash ofrece varias herramientas: jobs en background, xargs -P y GNU parallel. Cada opción equilibra control, portabilidad y facilidad de uso.
Cuándo Usar
Usa este recurso cuando:
- Necesites procesar muchos archivos o registros en un lote.
- Un loop secuencial sea demasiado lento para tu workflow.
- Quieras controlar el número máximo de jobs concurrentes.
- Necesites recolectar códigos de salida de cada proceso hijo.
Solución
Ejecución paralela de jobs en Bash
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS="${1:-4}"
INPUT_FILE="${2:-jobs.txt}"
# Opción 1: xargs con workers paralelos
process_task() {
local task="$1"
echo "Processing $task"
sleep "$((RANDOM % 3 + 1))"
echo "Done $task"
}
export -f process_task
cat "$INPUT_FILE" | xargs -P "$MAX_JOBS" -I {} bash -c 'process_task "{}"'
# Opción 2: GNU parallel
# parallel -j "$MAX_JOBS" process_task {} < "$INPUT_FILE"
# Opción 3: Background jobs con semáforo
SEMAPHORE=0
while IFS= read -r task; do
if [[ $SEMAPHORE -ge $MAX_JOBS ]]; then
wait -n
SEMAPHORE=$((SEMAPHORE - 1))
fi
process_task "$task" &
SEMAPHORE=$((SEMAPHORE + 1))
done < "$INPUT_FILE"
wait
Explicación
El script muestra tres enfoques comunes. xargs -P es portátil y está disponible en la mayoría de sistemas, pero menos flexible que GNU parallel. GNU parallel ofrece mejor manejo de salida, reanudación y progreso. El enfoque de background jobs usa wait -n para mantener un número máximo de jobs concurrentes sin herramientas externas. export -f hace visible la función de Bash a subprocesos cuando se usa xargs -I {} bash -c.
Variantes
| Enfoque | Herramienta | Pros | Contras |
|---|---|---|---|
| xargs | coreutils | Portátil, simple | Control limitado, salida desordenada |
| GNU parallel | parallel | Potente, reanudable, salida ordenada | Dependencia extra |
| Background jobs | builtin de bash | Sin dependencias externas | Bookkeeping manual, propenso a race conditions |
Lo que funciona
- Limita la concurrencia a un límite probado. Demasiados jobs agotan CPU, memoria o descriptores de archivo.
- Haz que los jobs sean idempotentes. Un job reintentado debe producir el mismo resultado sin efectos secundarios.
- Captura y agrega códigos de salida. Un job fallido no debe ocultarse silenciosamente entre los exitosos.
- Usa un directorio temporal por job. Esto previene colisiones de archivos y facilita la limpieza.
- Registra con el identificador del job. Prefija la salida con el nombre de la tarea para poder rastrear fallos.
Errores Comunes
- Paralelismo sin límites. Lanzar cada tarea en background de inmediato puede colapsar el shell.
- Perder códigos de salida.
xargsdevuelve el último código por defecto; usa-Pcon-to GNUparallelpara rastrear cada job. - Ignorar el quoting del shell. Los nombres de archivo con espacios rompen
xargsa menos que uses-0o-d. - Escribir en el mismo archivo de salida. Las escrituras concurrentes entrelazan la salida; usa un archivo por job o bloquea el archivo.
- Sin timeout. Un job atascado puede bloquear todo el lote; agrega
timeouta cada comando.
Mejores Prácticas Adicionales
- For a deeper guide, see Bash Parallel Execution.
- Usa
nprocpara auto-detectar núcleos de CPU. En lugar de hardcodearMAX_JOBS, derívalo del conteo de CPU del sistema. Para tareas I/O-bound, usa 2-4x el conteo de núcleos; para CPU-bound, usa 1x:
CPU_CORES=$(nproc)
IO_JOBS=$((CPU_CORES * 4)) # I/O-bound: 4x núcleos
CPU_JOBS=$CPU_CORES # CPU-bound: 1x núcleos
- Usa
--barcon GNU parallel para mostrar progreso. Para batches largos, una barra de progreso ayuda a estimar el tiempo de completitud:
parallel --bar --jobs "$MAX_JOBS" process_task {} < "$INPUT_FILE"
- Configura límites de descriptores de archivo para alta concurrencia. Cada job en background usa descriptores de archivo. Para más de 100 jobs concurrentes, aumenta el ulimit:
ulimit -n 4096 # Permitir 4096 descriptores de archivo abiertos Preguntas frecuentes
GNU parallel con salida ordenada y progreso
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS="${1:-4}"
INPUT_FILE="${2:-jobs.txt}"
LOG_DIR="${3:-/tmp/parallel-logs}"
mkdir -p "$LOG_DIR"
# GNU parallel con salida ordenada, barra de progreso y archivos de log por job
parallel --jobs "$MAX_JOBS" \
--progress \
--joblog "$LOG_DIR/joblog.txt" \
--results "$LOG_DIR" \
--halt soon,fail=20% \
--timeout 300 \
--shuf \
process_task {} \
< "$INPUT_FILE"
# El joblog contiene: Seq Host Starttime JobRuntime Send Receive Exitval Signal Command
echo "=== Resumen de Jobs ==="
awk 'NR>1 {exitcodes[$7]++} END {for (code in exitcodes) printf "Exit %s: %d jobs\n", code, exitcodes[code]}' "$LOG_DIR/joblog.txt"
# Mostrar jobs fallidos
FAILED=$(awk 'NR>1 && $7!=0 {print $NF}' "$LOG_DIR/joblog.txt")
if [ -n "$FAILED" ]; then
echo "=== Jobs Fallidos ==="
echo "$FAILED"
fi
Colección de códigos de salida con xargs
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS="${1:-4}"
INPUT_FILE="${2:-jobs.txt}"
RESULTS_DIR=$(mktemp -d)
process_with_exit() {
local task="$1"
local result_file="$2"
# Simular trabajo
sleep "$((RANDOM % 3 + 1))"
if (( RANDOM % 10 == 0 )); then
echo "FAIL" > "$result_file"
return 1
fi
echo "OK" > "$result_file"
return 0
}
export -f process_with_exit
# Ejecutar en paralelo y recolectar códigos de salida
cat "$INPUT_FILE" | xargs -P "$MAX_JOBS" -I {} bash -c '
process_with_exit "{}" "'"$RESULTS_DIR"'/{//}_result" || true
'
# Agregar resultados
TOTAL=$(wc -l < "$INPUT_FILE")
SUCCESS=$(grep -l "OK" "$RESULTS_DIR"/*_result 2>/dev/null | wc -l)
FAILED=$(grep -l "FAIL" "$RESULTS_DIR"/*_result 2>/dev/null | wc -l)
echo "Total: $TOTAL, Éxito: $SUCCESS, Fallidos: $FAILED"
# Limpieza
rm -rf "$RESULTS_DIR"
Control de jobs con timeouts y reintentos
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS="${1:-4}"
MAX_RETRIES="${2:-3}"
TIMEOUT="${3:-60}"
INPUT_FILE="${4:-jobs.txt}"
run_with_retry() {
local task="$1"
local attempt=1
while (( attempt <= MAX_RETRIES )); do
echo "[$(date -Iseconds)] Intento $attempt para tarea: $task"
if timeout "$TIMEOUT" bash -c "process_task '$task'" 2>&1; then
echo "[$(date -Iseconds)] ÉXITO: $task (intento $attempt)"
return 0
fi
echo "[$(date -Iseconds)] REINTENTO: $task falló intento $attempt"
attempt=$((attempt + 1))
# Backoff exponencial: 1s, 2s, 4s, 8s...
sleep "$((2 ** (attempt - 2)))"
done
echo "[$(date -Iseconds)] FALLÓ: $task después de $MAX_RETRIES intentos"
return 1
}
export -f run_with_retry
export MAX_RETRIES TIMEOUT
# Ejecutar con GNU parallel para mejor control
parallel --jobs "$MAX_JOBS" \
--retry-failed \
--jobs "$MAX_JOBS" \
run_with_retry {} \
< "$INPUT_FILE"
Comparación con multiprocessing de Python
import multiprocessing
import subprocess
import time
from pathlib import Path
def run_task(task: str) -> tuple[str, int, float]:
start = time.time()
result = subprocess.run(
["bash", "-c", f"process_task '{task}'"],
capture_output=True,
text=True,
timeout=300,
)
elapsed = time.time() - start
return task, result.returncode, elapsed
def main():
tasks = Path("jobs.txt").read_text().strip().split("\n")
max_workers = min(4, multiprocessing.cpu_count())
with multiprocessing.Pool(max_workers) as pool:
results = pool.map(run_task, tasks)
succeeded = sum(1 for _, code, _ in results if code == 0)
failed = len(results) - succeeded
total_time = sum(t for _, _, t in results)
print(f"Total: {len(results)}, Éxito: {succeeded}, Fallidos: {failed}")
print(f"Wall time: {total_time:.1f}s, Speedup paralelo: {total_time / max_workers:.1f}s ahorrados")
for task, code, elapsed in results:
status = "OK" if code == 0 else f"FAIL (exit {code})"
print(f" {task}: {status} ({elapsed:.1f}s)")
if __name__ == "__main__":
main()
Procesamiento paralelo de archivos con find y xargs
#!/usr/bin/env bash
set -euo pipefail
MAX_JOBS="${1:-$(nproc)}"
TARGET_DIR="${2:-.}"
# Comprimir todos los archivos log en paralelo usando find + xargs -0
find "$TARGET_DIR" -type f -name "*.log" -print0 | \
xargs -0 -P "$MAX_JOBS" -I {} gzip -9 {}
# Generar checksums para todos los archivos en paralelo
find "$TARGET_DIR" -type f -print0 | \
xargs -0 -P "$MAX_JOBS" -I {} sh -c 'sha256sum "{}" > "{}.sha256"'
# Redimensionar imágenes en paralelo (requiere ImageMagick)
find "$TARGET_DIR" -type f \( -name "*.jpg" -o -name "*.png" \) -print0 | \
xargs -0 -P "$MAX_JOBS" -I {} convert {} -resize 50% "{}.thumb"
# Subir archivos a S3 en paralelo
find "$TARGET_DIR" -type f -name "*.gz" -print0 | \
xargs -0 -P "$MAX_JOBS" -I {} aws s3 cp {} "s3://my-bucket/uploads/" --no-progress
Recursos Relacionados
Ejecución Paralela en Bash
Cómo ejecutar comandos de shell en paralelo con xargs, GNU parallel y trabajos en segundo plano de Bash controlando la concurrencia y recolectando resultados.
RecipeScripting en Bash para Automatizacion DevOps y Tareas de
Como escribir scripts Bash robustos para automatizar despliegues, monitoreo de sistemas, rotacion de logs y tareas de mantenimiento rutinarias
RecipeScript de Rotación de Backups
Automatiza backups con políticas de retención usando un script de Bash que rota snapshots diarios, semanales y mensuales.
RecipeLoop Sobre Archivos en Bash
Cómo iterar de forma segura sobre archivos y directorios en Bash, manejando espacios, globs y listas grandes de archivos con patrones correctos.
RecipeProcesamiento de Texto en Bash
Cómo construir pipelines de procesamiento de texto con grep, sed, awk, cut, sort, uniq y tr para análisis de logs y transformación de datos.
RecipeEjecutar Comandos Shell en Paralelo con Bash
Ejecuta múltiples comandos shell concurrentemente usando xargs, GNU parallel y background jobs.