flock Komutu ile Kabuk Betiklerinde Dosya Kilitleme ve Eşzamanlı Çalışmayı Önleme

Cron job’larınız birbirinin üzerine mi basıyor? Bir betik çalışırken aynısı tekrar tetikleniyor ve veritabanı tutarsızlıkları, yarım kalan işlemler mi yaşıyorsunuz? Bu sorun, özellikle yoğun üretim ortamlarında can sıkıcı bir hal alıyor. flock komutu bu tür problemleri temelden çözen, hafif ama son derece güçlü bir araç. Bugün bu aracı her yönüyle ele alacağız.

flock Nedir ve Neden Var?

flock, Linux’ta dosya kilitleme mekanizmasını kabuk betiklerinden kullanmanızı sağlayan bir komut satırı aracıdır. Temelinde flock(2) sistem çağrısını kullanır. Çekirdek seviyesinde bir kilit oluşturduğu için, aynı anda birden fazla betik örneğinin çalışmasını engellemek için ideal bir çözümdür.

Klasik örnek: Her gece çalışan bir yedekleme betiğiniz var. Normalde 20 dakikada bitiyor, ama bir gece dosya sistemi yavaşlıyor ve betik 35 dakika sürüyor. Tam bu sırada cron yeni bir örneği tetikliyor. İki betik aynı anda çalışıyor, aynı dosyalara yazıyor, yedek bozuluyor. flock bu senaryoyu tamamen ortadan kaldırır.

flock genellikle util-linux paketinin bir parçası olarak gelir ve neredeyse tüm modern Linux dağıtımlarında hazır bulunur. Kontrol etmek için:

which flock
flock --version

Temel Kullanım Mantığı

flock iki farklı modda kullanılabilir: doğrudan bir komut çalıştırma modu ve kabuk betiği içinde kullanım modu.

Doğrudan komut çalıştırma:

flock /tmp/benim-kilit.lock -c "echo 'Bu komut sadece bir kez çalışır'"

Burada /tmp/benim-kilit.lock dosyası kilit dosyası olarak kullanılır. Eğer bu dosya üzerinde başka bir flock kilidi varsa, komut bekler ya da hata döner.

Betik içinde kullanım (fd ile):

#!/bin/bash

exec 200>/var/lock/yedekleme.lock

flock -n 200 || { echo "Betik zaten çalışıyor, çıkıyorum."; exit 1; }

echo "İşlem başladı: $(date)"
# Gerçek işlemler buraya gelir
sleep 30
echo "İşlem bitti: $(date)"

Bu ikinci yöntem daha idiomatik ve esnektir. exec 200> ile dosya tanımlayıcı 200’ü kilit dosyasına bağlıyoruz, ardından flock -n 200 ile kilit almaya çalışıyoruz.

Önemli Parametreler

-s / --shared: Paylaşımlı (okuma) kilidi alır. Birden fazla süreç aynı anda paylaşımlı kilit tutabilir.

-x / --exclusive: Özel (yazma) kilidi alır. Varsayılan davranış budur. Yalnızca bir süreç bu kilidi tutabilir.

-n / --nonblock: Kilit alınamazsa beklemek yerine hemen hata döner (çıkış kodu 1).

-w / --timeout : Belirtilen süre kadar kilit almayı bekler, süre aşılırsa çıkış kodu 1 döner.

-u / --unlock: Kilidi açar.

-o / --close: Çocuk süreç başlatılmadan önce kilit dosyasını kapatır.

-c / --command: Çalıştırılacak komutu belirtir, sh -c üzerinden çalıştırır.

-E / --conflict-exit-code : Kilit alınamadığında döndürülecek çıkış kodunu belirler.

Gerçek Dünya Senaryoları

Senaryo 1: Cron Job Çakışmalarını Önleme

En yaygın kullanım alanı budur. Aşağıdaki betik, birden fazla kez tetiklense bile yalnızca bir örneği çalışacak şekilde tasarlanmıştır:

#!/bin/bash
# /usr/local/bin/gunluk-rapor.sh

KILIT_DOSYASI="/var/lock/gunluk-rapor.lock"
LOG="/var/log/gunluk-rapor.log"

exec 9>"$KILIT_DOSYASI"

if ! flock -n 9; then
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Betik zaten çalışıyor, bu örnek sonlandırılıyor." >> "$LOG"
    exit 0
fi

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rapor oluşturma başladı." >> "$LOG"

# Rapor oluşturma işlemleri
/usr/local/bin/veri-cek.sh | /usr/local/bin/rapor-olustur.sh > /tmp/bugunun-raporu.html

# E-posta gönderimi
mail -s "Günlük Rapor $(date '+%d/%m/%Y')" [email protected] < /tmp/bugunun-raporu.html

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rapor gönderildi." >> "$LOG"

/etc/cron.d/gunluk-rapor:

*/15 * * * * root /usr/local/bin/gunluk-rapor.sh

Bu yapıda betik her 15 dakikada bir tetiklenebilir, ama eğer önceki çalışma hâlâ devam ediyorsa yeni örnek sessizce çıkar.

Senaryo 2: Zaman Aşımlı Bekleme ile Veritabanı Yedekleme

Bazen betiğin çakışma durumunda beklemesini isteyebilirsiniz. Örneğin birden fazla yedekleme işi varsa ve sıralı çalışmalarını istiyorsanız:

#!/bin/bash
# /usr/local/bin/pg-yedekle.sh

DB_ADI="${1:-production}"
YEDEK_DIZIN="/mnt/yedekler/postgresql"
KILIT="/var/lock/pg-yedek.lock"
BEKLEME_SURESI=300  # 5 dakika bekle

exec 8>"$KILIT"

if ! flock -w "$BEKLEME_SURESI" 8; then
    echo "HATA: $BEKLEME_SURESI saniye içinde kilit alınamadı. Başka bir yedekleme sürüyor olabilir."
    exit 2
fi

echo "Yedekleme başlıyor: $DB_ADI - $(date)"

pg_dump -Fc -d "$DB_ADI" -f "${YEDEK_DIZIN}/${DB_ADI}_$(date '+%Y%m%d_%H%M%S').dump"

if [ $? -eq 0 ]; then
    echo "Yedekleme başarıyla tamamlandı: $(date)"
    # Eski yedekleri temizle (30 günden eskiler)
    find "$YEDEK_DIZIN" -name "${DB_ADI}_*.dump" -mtime +30 -delete
else
    echo "HATA: Yedekleme başarısız oldu!"
    exit 1
fi

Senaryo 3: Paylaşımlı ve Özel Kilit Kombinasyonu

Bu senaryo biraz daha ileri düzey. Birden fazla okuma işleminin eş zamanlı yürüyebileceği, ama yazma işleminin tek başına çalışması gereken durumlar için:

#!/bin/bash
# okuyucu.sh - Birden fazla örnek aynı anda çalışabilir

KILIT="/var/lock/veri-islem.lock"

exec 7>"$KILIT"
flock -s 7  # Paylaşımlı kilit

echo "Okuma yapılıyor: PID $$ - $(date)"
# Okuma işlemleri
cat /var/data/paylasilan-veri.json | jq '.items[]'

echo "Okuma tamamlandı: PID $$"
#!/bin/bash
# yazici.sh - Sadece bir örnek çalışabilir, okuyucular bekler

KILIT="/var/lock/veri-islem.lock"

exec 7>"$KILIT"

if ! flock -x -w 60 7; then
    echo "Yazma kilidi alınamadı, okuyucular devam ediyor olabilir."
    exit 1
fi

echo "Yazma yapılıyor: PID $$ - $(date)"
# Yazma işlemleri - tüm okuyucular bu süre boyunca bekleyecek
echo '{"items": ["yeni", "veri"]}' > /var/data/paylasilan-veri.json

echo "Yazma tamamlandı: PID $$"

Gelişmiş Kullanım Kalıpları

PID Dosyası ile Birleşik Kullanım

Sadece kilit almak yetmez, bazen hangi sürecin kilidi tuttuğunu da bilmek istersiniz. PID dosyası ile birleştirme:

#!/bin/bash

KILIT="/var/lock/uzun-islem.lock"
PID_DOSYASI="/var/run/uzun-islem.pid"

exec 6>"$KILIT"

if ! flock -n 6; then
    CALISNA_PID=$(cat "$PID_DOSYASI" 2>/dev/null)
    if [ -n "$CALISAN_PID" ]; then
        echo "Betik zaten çalışıyor (PID: $CALISAN_PID)"
    else
        echo "Betik zaten çalışıyor (PID bilinmiyor)"
    fi
    exit 1
fi

# Kilidi aldık, PID'imizi kaydet
echo $$ > "$PID_DOSYASI"

# Çıkışta PID dosyasını temizle
trap 'rm -f "$PID_DOSYASI"' EXIT

echo "İşlem başlıyor (PID: $$)"
# Uzun süren işlemler
sleep 60
echo "İşlem tamamlandı"

flock ile Paralel İş Kuyruğu

Birden fazla iş eş zamanlı çalışabileceği ama toplam sayının sınırlı tutulması gereken durumlar için:

#!/bin/bash
# is-kuyrugu.sh

MAX_PARALEL=3
IS_LISTESI=("is1" "is2" "is3" "is4" "is5" "is6")

bir_is_yap() {
    local is_adi="$1"
    local slot="$2"
    local kilit="/var/lock/is-slot-${slot}.lock"
    
    exec {fd}>"$kilit"
    flock -x "$fd"
    
    echo "[Slot $slot] Başlıyor: $is_adi"
    sleep $((RANDOM % 10 + 5))
    echo "[Slot $slot] Bitti: $is_adi"
    
    eval "exec ${fd}>&-"
}

for is in "${IS_LISTESI[@]}"; do
    # Boş slot bul ve işi oraya yerleştir
    for slot in $(seq 1 $MAX_PARALEL); do
        kilit="/var/lock/is-slot-${slot}.lock"
        exec {fd}>"$kilit"
        if flock -n "$fd"; then
            eval "exec ${fd}>&-"
            bir_is_yap "$is" "$slot" &
            break
        fi
        eval "exec ${fd}>&-"
    done
done

wait
echo "Tüm işler tamamlandı"

systemd Timer ile Entegrasyon

Cron yerine systemd timer kullanıyorsanız, flock yine işe yarar çünkü systemd’nin ConditionPathExists direktifi bazı durumlar için yeterli olmayabilir:

#!/bin/bash
# /usr/local/bin/systemd-gibi-betik.sh
# systemd service olarak çalışır ama örtüşmeyi engeller

set -euo pipefail

SERVIS_ADI="veri-senkron"
KILIT="/run/lock/${SERVIS_ADI}.lock"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | systemd-cat -t "$SERVIS_ADI" -p info
}

exec 5>"$KILIT"

if ! flock -n 5; then
    log "Önceki çalışma henüz tamamlanmadı, bu örnek atlanıyor."
    exit 0
fi

log "Senkronizasyon başlıyor"

# rsync ile senkronizasyon
rsync -avz --delete 
    --exclude='.git' 
    /kaynak/dizin/ 
    yedek-sunucu:/hedef/dizin/

log "Senkronizasyon tamamlandı"

Kilit Dosyası Nereye Konulmalı?

Bu soru pratikte önemlidir. Deneyimden gelen birkaç kural:

  • /var/lock/: Sistem genelindeki betikler için, ancak önyüklemede temizlenmez.
  • /run/lock/ veya /var/run/: Önyüklemede temizlenir, geçici işlemler için idealdir. Modern sistemlerde tercih edilmesi gereken yer burasıdır.
  • /tmp/: Kullanılabilir ama güvenlik açısından dikkatli olunmalı (symlink saldırıları). Root olmayan betikler için bile /tmp yerine mktemp kullanımı tercih edilebilir.
  • Betiğin kendi dizini: Bağıl yollarla kilit dosyası oluşturmaktan kaçının, mutlak yol kullanın.

Ayrıca kilit dosyasının bulunduğu dizin, betiği çalıştıran kullanıcı tarafından yazılabilir olmalıdır.

Dikkat Edilmesi Gereken Durumlar

NFS üzerinde flock çalışmaz: flock(2) NFS üzerinde güvenilir değildir. Paylaşımlı NFS dizinlerinde kilit mekanizması olarak flock kullanmayın, lockd protokolü tutarsız davranabilir. NFS’te lockfile komutu ya da özel çözümler tercih edilmelidir.

Kilit dosyasını silmek kilidi açmaz: Bir süreç kilit dosyasını silse bile, diğer süreçlerde açık olan dosya tanımlayıcı kilit bilgisini korur. Kilit, dosya tanımlayıcısı kapatıldığında ya da süreç sonlandığında serbest bırakılır. Bu aslında bir özellik; betik çöktüğünde kilit otomatik serbest kalır.

# Test: Kilit tutulurken dosya silinirse ne olur?
flock /tmp/test.lock sleep 30 &
rm /tmp/test.lock  # Silindikten sonra bile kilit geçerli
flock -n /tmp/test.lock echo "Bu çalışmaz"  # Yeni dosya oluşur, kilit farklı inode'da

Çıkış kodlarını kontrol edin: flock -n ile nonblock modda çalışırken, özellikle set -e kullanıyorsanız kilit alınamama durumu betiği beklenmedik şekilde sonlandırabilir. Bu yüzden if ! flock -n fd gibi açık kontrol kullanmak daha güvenlidir.

Hızlı Test ve Debug

Kilit mekanizmanızın doğru çalışıp çalışmadığını test etmek için:

# Terminal 1: Kilit tut
flock -x /tmp/test.lock bash -c 'echo "Kilit alındı, 30 sn bekliyorum"; sleep 30'

# Terminal 2: Kilidi almaya çalış (nonblock)
flock -n /tmp/test.lock echo "Ben de çalıştım"
# Çıktı: Hiçbir şey (çıkış kodu 1)

# Hangi süreç kilidi tutuyor?
fuser /tmp/test.lock

# lslocks ile sistem genelindeki kilitleri gör
lslocks | grep test

lslocks komutu, sistemdeki tüm aktif kilitleri listeler ve debug sürecinde çok işe yarar.

Sonuç

flock komutu, Linux betik yazımında kritik bir güvenlik ağıdır. Özellikle üretim ortamlarında cron job’ların birbirinin üzerine basmasından, paralel çalışmaların yarattığı veri tutarsızlıklarından ve uzun süren işlemlerin çift tetiklenmesinden kaynaklanan sorunların büyük çoğunluğunu çözer.

Hafif, güvenilir ve çekirdek destekli bir mekanizma kullandığı için üçüncü taraf araçlara ya da karmaşık çözümlere gerek kalmaz. Betiklerinize flock entegre etmek için birkaç satır eklemek yeterli, karşılığında elde ettiğiniz güvenlik ve öngörülebilirlik ise paha biçilemez.

Küçük bir öneri ile bitirelim: Mevcut kritik betiklerinizi gözden geçirin. Eğer bir betiğin cron ile her X dakikada çalıştığını ve süresinin bazen X’i aştığını biliyorsanız, o betik flock için birinci adaydır. Bu tür betiklere flock eklemek için “bir şey bozulmayı beklemek” zorunda değilsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir