Bash ile Paralel İşlem Yönetimi: Background Jobs, wait ve xargs ile Hız Optimizasyonu
Sunucuda 500 dizini teker teker işleyen bir script yazdığınızda ve kahvenizi içip geri döndüğünüzde hâlâ yüzde otuzda olduğunu gördüğünüzde, paralel işlemenin ne kadar kritik olduğunu anlıyorsunuz. Bash, görünürde sıradan bir kabuk dili gibi dursa da doğru kullanıldığında ciddi bir paralelleştirme kapasitesine sahip. Bu yazıda background jobs, wait komutu ve xargs ile paralel iş akışlarını nasıl yöneteceğimizi, performansı nasıl artıracağımızı ve işleri kontrol altında nasıl tutacağımızı ele alacağız.
Temel Kavramlar: Foreground ve Background
Normalde bir komut çalıştırdığınızda terminal o komut bitene kadar bloke olur. Buna foreground çalışma denir. Komutu & ile bitirdiğinizde ise kabuk komutu arka plana atar ve siz başka işler yapmaya devam edebilirsiniz.
# Foreground - terminal bloke olur
sleep 10
# Background - terminal serbest kalır
sleep 10 &
echo "Bu hemen çalışır"
Arka planda başlayan her iş bir Job ID ve bir PID alır. jobs komutuyla aktif arka plan işlerini görebilirsiniz:
#!/bin/bash
sleep 30 &
sleep 20 &
sleep 10 &
jobs
# Çıktı:
# [1] Running sleep 30 &
# [2]- Running sleep 20 &
# [3]+ Running sleep 10 &
Job ID’ye göre işlemi öne almak için fg %2, durdurmak için kill %2 kullanabilirsiniz. Ama asıl güç, wait komutuyla gelir.
wait Komutu: Kaosun İçinde Düzen
wait olmadan paralel işleme yapmak, fırında bir şeyler pişirken kapıyı açmadan içeri bakmaya çalışmak gibidir. Bir şeylerin ne zaman hazır olduğunu bilemezsiniz.
wait komutu üç farklı şekilde kullanılır:
- wait (argümansız): Tüm arka plan işleri bitene kadar bekler
- wait PID: Belirli bir PID bitene kadar bekler
- wait %JobID: Belirli bir job bitene kadar bekler
Basit Paralel İşlem Örneği
#!/bin/bash
echo "Başlangıç: $(date)"
# Üç işlemi paralel başlat
process_one() {
sleep 3
echo "İşlem 1 tamamlandı"
}
process_two() {
sleep 5
echo "İşlem 2 tamamlandı"
}
process_three() {
sleep 4
echo "İşlem 3 tamamlandı"
}
process_one &
process_two &
process_three &
# Hepsinin bitmesini bekle
wait
echo "Bitiş: $(date)"
# Sıralı çalışsaydı 12 saniye sürerdi, paralelde ~5 saniye sürer
PID Takibi ile Gelişmiş wait Kullanımı
Gerçek dünyada her işin başarılı olup olmadığını da takip etmeniz gerekir. wait PID sözdizimi, $? ile çıkış kodunu döndürür:
#!/bin/bash
declare -A pids
declare -A results
# Paralel işlemleri başlat ve PID'leri kaydet
for server in web01 web02 web03 db01 db02; do
(
# Simüle edilmiş sunucu kontrolü
ping -c 2 -W 1 "$server" > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
echo "$server: ONLINE"
else
echo "$server: OFFLINE"
exit 1
fi
) &
pids[$server]=$!
done
# Her işlemin sonucunu ayrı ayrı kontrol et
failed_servers=()
for server in "${!pids[@]}"; do
wait "${pids[$server]}"
if [[ $? -ne 0 ]]; then
failed_servers+=("$server")
fi
done
if [[ ${#failed_servers[@]} -gt 0 ]]; then
echo "UYARI: Şu sunuculara ulaşılamıyor: ${failed_servers[*]}"
exit 1
fi
echo "Tüm sunucular erişilebilir durumda."
Paralel İşlem Sınırlama: Throttling
Sınırsız paralel iş başlatmak sistemin kaynaklarını tüketir. 1000 dosyayı aynı anda işlemeye çalışırsanız ya OOM killer devreye girer ya da disk I/O darboğazı oluşur. İş havuzu (job pool) yöntemi bu sorunu çözer.
Manuel Job Pool Yönetimi
#!/bin/bash
MAX_JOBS=4
current_jobs=0
process_file() {
local file="$1"
# Gerçek işlem burada yapılır
gzip -k "$file"
echo "Sıkıştırıldı: $file"
}
# Aktif iş sayısını kontrol eden fonksiyon
wait_for_slot() {
while true; do
current_jobs=$(jobs -r | wc -l)
if [[ $current_jobs -lt $MAX_JOBS ]]; then
break
fi
sleep 0.1
done
}
# /var/log altındaki .log dosyalarını paralel sıkıştır
find /var/log -name "*.log" -size +10M | while read -r logfile; do
wait_for_slot
process_file "$logfile" &
done
# Kalan tüm işleri bekle
wait
echo "Tüm dosyalar işlendi."
Bu pattern küçük işler için gayet işe yarar, ama xargs çok daha temiz ve güçlü bir alternatif sunar.
xargs ile Paralel İşlem: Asıl Güç Buraya Geliyor
xargs, standart girdiden gelen argümanları alıp bir komuta besleyen araç. -P parametresiyle paralel işleme desteği eklenince inanılmaz güçlü hale geliyor.
Temel parametreler:
- -P N: N adet paralel iş başlat (0 = mümkün olduğu kadar çok)
- -I {}: Argümanın yerleştirileceği yer tutucusu
- -n N: Her çağrıda kaç argüman kullanılacağı
- -L N: Her çağrıda kaç satır kullanılacağı
- –max-args N: -n ile aynı, uzun format
- -0: Null-terminated input (find -print0 ile kullanılır)
xargs ile Basit Paralel İşlem
#!/bin/bash
# 8 paralel iş ile dosyaları işle
find /data/images -name "*.jpg" -print0 |
xargs -0 -P 8 -I {} convert {} -resize 800x600 {}_resized.jpg
# Sunucu listesine paralel SSH komutu
cat sunucular.txt | xargs -P 10 -I {}
ssh -o ConnectTimeout=5 {} "df -h / | tail -1"
# Paralel md5sum hesaplama
find /backup -name "*.tar.gz" -print0 |
xargs -0 -P 4 md5sum >> /backup/checksums.md5
Gerçek Dünya: Paralel Yedek Alma
Bu senaryo sysadminlerin günlük hayatta sık karşılaştığı bir durum. Birden fazla veritabanını sırayla yedeklemek yerine paralel alın:
#!/bin/bash
BACKUP_DIR="/backup/mysql/$(date +%Y%m%d)"
MYSQL_USER="backup_user"
MYSQL_PASS="gizli_sifre"
MAX_PARALLEL=3
LOG_FILE="/var/log/backup_$(date +%Y%m%d).log"
mkdir -p "$BACKUP_DIR"
# Yedeklenecek veritabanlarını al
DATABASES=$(mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "SHOW DATABASES;" 2>/dev/null |
grep -Ev "^(Database|information_schema|performance_schema|sys)$")
backup_database() {
local db="$1"
local start_time=$(date +%s)
echo "[$(date '+%H:%M:%S')] Başlıyor: $db" | tee -a "$LOG_FILE"
mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS"
--single-transaction
--routines
--triggers
"$db" 2>>"$LOG_FILE" |
gzip > "$BACKUP_DIR/${db}.sql.gz"
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
if [[ $exit_code -eq 0 ]]; then
local size=$(du -sh "$BACKUP_DIR/${db}.sql.gz" | cut -f1)
echo "[$(date '+%H:%M:%S')] Tamamlandı: $db (${duration}s, $size)" | tee -a "$LOG_FILE"
else
echo "[$(date '+%H:%M:%S')] HATA: $db yedeklenemedi!" | tee -a "$LOG_FILE"
return 1
fi
}
export -f backup_database
export BACKUP_DIR MYSQL_USER MYSQL_PASS LOG_FILE
echo "$DATABASES" | xargs -P "$MAX_PARALLEL" -I {} bash -c 'backup_database "$@"' _ {}
echo "Yedek işlemi tamamlandı. Log: $LOG_FILE"
Burada dikkat edilmesi gereken önemli bir nokta var: xargs ile bash -c kombinasyonunda export -f ile fonksiyonları alt süreçlere aktarmanız gerekiyor.
GNU Parallel: xargs’ın Büyük Kardeşi
xargs yeterli çoğu senaryo için, ama parallel (GNU Parallel) daha gelişmiş kontrol imkânı sunuyor. Sistemde kurulu değilse apt install parallel veya yum install parallel ile kurabilirsiniz.
#!/bin/bash
# Sunucu listesindeki tüm makinelere paralel güncelleme
# --jobs: paralel iş sayısı
# --tag: hangi sunucudan geldiğini gösterir
# --timeout: zaman aşımı
# --retry-failed: başarısız olanları tekrar dene
parallel --jobs 5 --tag --timeout 120
"ssh {} 'sudo apt-get update -qq && sudo apt-get upgrade -y -qq'"
:::: sunucular.txt
# Farklı parametrelerle test koşusu
parallel --jobs 4
"curl -s -o /dev/null -w '%{http_code} {}n' https://{}/health"
::: api01.example.com api02.example.com api03.example.com web01.example.com
Paralel İşlemlerde Hata Yönetimi
Paralel çalışmanın en can sıkıcı yanı hata yönetimi. Bir işlem sessizce başarısız olduğunda bunu yakalamak için trap ve sinyal yönetimi kullanmanız gerekir.
#!/bin/bash
FAILED_JOBS=()
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR; kill 0" EXIT INT TERM
process_item() {
local item="$1"
local result_file="$TEMP_DIR/${item////_}.result"
# İşlemi yap
if some_command "$item" > "$result_file" 2>&1; then
echo "OK" >> "$result_file"
return 0
else
echo "FAIL" >> "$result_file"
return 1
fi
}
declare -A job_pids
# İşlemleri başlat
for item in "${ITEMS[@]}"; do
process_item "$item" &
job_pids["$item"]=$!
done
# Sonuçları topla
for item in "${!job_pids[@]}"; do
pid="${job_pids[$item]}"
if ! wait "$pid"; then
FAILED_JOBS+=("$item")
echo "BAŞARISIZ: $item"
fi
done
# Özet rapor
total=${#job_pids[@]}
failed=${#FAILED_JOBS[@]}
success=$((total - failed))
echo ""
echo "=== İşlem Özeti ==="
echo "Toplam: $total"
echo "Başarılı: $success"
echo "Başarısız: $failed"
if [[ $failed -gt 0 ]]; then
echo "Başarısız işlemler: ${FAILED_JOBS[*]}"
exit 1
fi
Performans Ölçümü ve Optimizasyon
Kaç paralel iş açacağınıza karar vermek için önce darboğazın nerede olduğunu anlamanız gerekir.
CPU-bound işlemler için: nproc kadar paralel iş genellikle optimum noktadır. Daha fazlası context switching overhead’i artırır.
I/O-bound işlemler için: CPU sayısının 2-4 katı paralel iş açabilirsiniz çünkü işlemler çoğunlukla disk veya ağı bekler.
Ağ-bound işlemler için: Bant genişliğinize ve hedef sistemin kapasitesine göre karar verin.
#!/bin/bash
# Sisteme göre otomatik paralel iş sayısı belirle
get_optimal_jobs() {
local job_type="$1" # cpu, io, network
local cpu_count
cpu_count=$(nproc)
case "$job_type" in
cpu)
echo "$cpu_count"
;;
io)
echo $((cpu_count * 2))
;;
network)
echo $((cpu_count * 4))
;;
*)
echo "$cpu_count"
;;
esac
}
# Basit benchmark: hangi paralel sayısı en hızlı?
benchmark_parallel() {
local test_items=100
for parallel_count in 1 2 4 8 16; do
local start_time=$(date +%s%N)
seq 1 $test_items | xargs -P "$parallel_count" -I {}
bash -c 'sleep 0.05' 2>/dev/null
local end_time=$(date +%s%N)
local duration=$(( (end_time - start_time) / 1000000 ))
echo "Paralel=$parallel_count: ${duration}ms"
done
}
CPU_JOBS=$(get_optimal_jobs cpu)
IO_JOBS=$(get_optimal_jobs io)
echo "CPU-bound işlemler için önerilen: $CPU_JOBS iş"
echo "I/O-bound işlemler için önerilen: $IO_JOBS iş"
Gerçek Dünya: Log Analizi Pipeline’ı
Son olarak, tüm öğrendiklerimizi bir araya getirelim. Birden fazla sunucudan log toplayıp paralel analiz eden bir script:
#!/bin/bash
# Paralel Log Analizi
# Kullanım: ./log_analyze.sh sunucu_listesi.txt "YYYY-MM-DD"
SERVER_LIST="$1"
TARGET_DATE="${2:-$(date +%Y-%m-%d)}"
OUTPUT_DIR="/tmp/log_analysis_$(date +%Y%m%d_%H%M%S)"
MAX_PARALLEL=6
ERRORS_FOUND=0
mkdir -p "$OUTPUT_DIR"
analyze_server_logs() {
local server="$1"
local date="$2"
local output_dir="$3"
local result_file="$output_dir/${server}.analysis"
echo "Analiz ediliyor: $server"
# Uzak sunucudan log çek ve analiz et
ssh -o ConnectTimeout=10 -o BatchMode=yes "$server"
"grep '$date' /var/log/nginx/access.log 2>/dev/null |
awk '{print $9}' | sort | uniq -c | sort -rn | head -20"
> "$result_file" 2>/dev/null
if [[ $? -ne 0 ]]; then
echo "BAĞLANTI HATASI: $server" > "$result_file"
return 1
fi
# 500 hatalarını say
local error_count
error_count=$(ssh -o ConnectTimeout=10 -o BatchMode=yes "$server"
"grep -c ' 5[0-9][0-9] ' /var/log/nginx/access.log 2>/dev/null" 2>/dev/null || echo "0")
echo "500 Hata Sayısı: $error_count" >> "$result_file"
echo "$server analizi tamamlandı (500 hata: $error_count)"
return 0
}
export -f analyze_server_logs
# Paralel analiz başlat
cat "$SERVER_LIST" |
xargs -P "$MAX_PARALLEL" -I {}
bash -c 'analyze_server_logs "$@"' _ {} "$TARGET_DATE" "$OUTPUT_DIR"
# Sonuçları birleştir
echo ""
echo "=========================================="
echo "LOG ANALİZİ RAPORU - $TARGET_DATE"
echo "=========================================="
for result_file in "$OUTPUT_DIR"/*.analysis; do
server_name=$(basename "$result_file" .analysis)
echo ""
echo "--- $server_name ---"
cat "$result_file"
done
# Özet istatistik
total_servers=$(wc -l < "$SERVER_LIST")
successful=$(find "$OUTPUT_DIR" -name "*.analysis" |
xargs grep -L "BAĞLANTI HATASI" 2>/dev/null | wc -l)
failed=$((total_servers - successful))
echo ""
echo "=========================================="
echo "Toplam Sunucu: $total_servers"
echo "Başarılı: $successful"
echo "Başarısız: $failed"
echo "Sonuçlar: $OUTPUT_DIR"
echo "=========================================="
Dikkat Edilmesi Gereken Yaygın Hatalar
- Race condition: Birden fazla paralel iş aynı dosyaya yazıyorsa veri bozulur. Her iş için ayrı geçici dosya kullanın, sonunda birleştirin.
- Zombie process:
waitkullanmadan script biterse alt süreçler zombie olabilir. Her zamanwaitile temizlik yapın.
- Sinyallerin alt süreçlere iletilmesi:
trap "kill 0" INT TERMekleyerek CTRL+C geldiğinde tüm alt süreçleri öldürün.
- stdout/stderr karışması: Paralel çıktılar birbirine karışır. Her işin çıktısını ayrı dosyaya yönlendirip sonunda birleştirin.
- Dosya tanımlayıcı limiti: Çok fazla paralel SSH bağlantısı açarsanız
ulimit -nlimitine çarpabilirsiniz.ulimit -n 65536ile artırabilirsiniz.
- Fork bomb riski:
xargs -P 0kullanırken dikkatli olun. “0” değeri mümkün olduğu kadar çok iş demek. Büyük dosya listelerinde sistemi çökertebilir.
Sonuç
Bash ile paralel işlem yönetimi, doğru araçları bildiğinizde dramatik performans artışları sağlıyor. 100 sunucuyu sırayla kontrol etmek 10-15 dakika alırken paralel yapıldığında bu süre 1-2 dakikaya düşebiliyor. Temel yaklaşımı şöyle özetleyebiliriz: basit paralel işlemler için & ve wait, toplu veri işleme için xargs -P, karmaşık iş akışları için GNU parallel.
Hangi yöntemi seçerseniz seçin, şu üç kuralı asla unutmayın: her zaman wait ile süreçleri temizleyin, hata kodlarını takip edin ve sisteminizin kaynak limitlerini aşmayacak makul bir paralellik derecesi belirleyin. İlk başta biraz karmaşık görünen bu yapılar, pratikte yaptığınızda bakımı kolay, güvenilir ve çok daha hızlı scriptler ortaya çıkarıyor.
