Çakışan Cron Görevlerini Önleme: flock Kullanımı

Bir cron job’u saate bir çalışacak şekilde ayarladınız, sistem gayet güzel çalışıyor gibi görünüyor. Ama bir gün fark ediyorsunuz ki aynı process birden fazla çalışıyor, veritabanı tutarsız verilerle dolu ya da log dosyaları birbirinin üzerine yazılmış. İşte bu klasik “çakışan cron görevi” problemi, ve flock bu problemin en temiz çözümlerinden biri.

Problem Nedir ve Neden Önemli?

Cron görevleri zamana bağlı çalışır, ama her zaman zamanında bitmez. Diyelim ki her 5 dakikada bir çalışan bir veri senkronizasyon scripti var. Normalde 2-3 dakikada bitiyor, sorun yok. Ama bir gün veritabanı biraz yavaş, network gecikmeleri var ya da işlenecek veri miktarı normalin üç katı. Script 5 dakikayı geçiyor ve cron ikinci bir instance başlatıyor. Artık iki ayrı process aynı verileri işlemeye çalışıyor.

Bu durumun yol açabileceği sorunlar gerçekten ciddi olabilir:

  • Aynı kayıt iki kez işlenebilir
  • Dosya yazma çakışmaları nedeniyle veri bozulması yaşanabilir
  • Sistem kaynakları gereksiz yere tüketilir
  • Log dosyaları anlamsız hale gelir
  • Kritik operasyonlarda geri dönüşü zor hatalar oluşabilir

Peki çözüm ne? Tabii ki process lock mekanizması. Ve Linux’ta bunun için en pratik araç flock.

flock Nedir?

flock, Linux’ta dosya kilitleme (file locking) işlemi yapan bir komut satırı aracıdır. util-linux paketinin parçasıdır ve neredeyse tüm modern Linux dağıtımlarında varsayılan olarak gelir. Temel mantığı şudur: bir script çalışmaya başladığında belirli bir dosyayı kilitler, başka bir instance aynı dosyayı kilitlemek istediğinde ya bekler ya da vazgeçer.

flock‘un güzel yanı kernel seviyesinde çalışmasıdır. Yani PID dosyası tabanlı çözümler gibi “PID dosyası var ama process ölmüş” gibi stale lock sorunlarıyla uğraşmazsınız. Kernel, process sonlandığında kilidi otomatik olarak serbest bırakır.

Temel Kullanım

En basit kullanım şekli doğrudan komut satırından ya da crontab’dan çağırmaktır:

flock -n /var/lock/benim_scriptim.lock /path/to/script.sh

Burada:

  • -n: Non-blocking mod. Kilit alınamazsa hemen çıkar, beklemez.
  • /var/lock/benim_scriptim.lock: Kilit dosyasının yolu. Bu dosya yoksa otomatik oluşturulur.
  • /path/to/script.sh: Çalıştırılacak komut veya script.

Eğer -n parametresi kullanmazsanız flock kilit serbest kalana kadar bekler. Çakışan cron görevleri için genellikle -n kullanmak daha mantıklıdır çünkü yeni instance’ın beklemesi yerine vazgeçmesini isteriz.

Bir de -w (wait) parametresi var:

flock -w 10 /var/lock/benim_scriptim.lock /path/to/script.sh

-w 10: 10 saniye bekle, hala kilit alınamazsa çık. Bazen kısa süreli çakışmalarda bu daha mantıklı olabilir.

Crontab’da Doğrudan Kullanım

En pratik yöntem crontab içinde direkt kullanmaktır:

# Her 5 dakikada bir çalış, ama önceki instance hala çalışıyorsa atla
*/5 * * * * /usr/bin/flock -n /var/lock/veri_sync.lock /opt/scripts/veri_sync.sh

Bu kadar. Artık /opt/scripts/veri_sync.sh scripti aynı anda sadece bir kez çalışacak. Bir önceki instance henüz bitmemişse yeni cron tetiklemesi sessizce geçip gidecek.

Dikkat edilmesi gereken bir nokta: /usr/bin/flock şeklinde tam path kullanmak daha güvenlidir. Cron’un PATH’i kullanıcı shell’ininkinden farklı olabilir.

Kilit alınamadığında log yazmak istiyorsanız:

*/5 * * * * /usr/bin/flock -n /var/lock/veri_sync.lock /opt/scripts/veri_sync.sh || echo "$(date): Önceki instance hala çalışıyor, atlandı" >> /var/log/veri_sync_skip.log

Script İçinde flock Kullanımı

Bazen daha fazla kontrol istiyorsunuz. Mesela scriptin kendisi içinde lock mekanizmasını yönetmek, özel log mesajları yazmak ya da lock alınamadığında farklı davranışlar sergilemek istiyorsunuz. Bunun için script içinde file descriptor yöntemi kullanılır:

#!/bin/bash

LOCK_FILE="/var/lock/veri_sync.lock"
LOG_FILE="/var/log/veri_sync.log"

# Lock dosyasını aç ve file descriptor 200'e ata
exec 200>"$LOCK_FILE"

# Non-blocking kilit almayı dene
if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - UYARI: Script zaten çalışıyor, bu instance sonlandırılıyor." >> "$LOG_FILE"
    exit 1
fi

# Buradan itibaren sadece bir instance çalışır
echo "$(date '+%Y-%m-%d %H:%M:%S') - Script başladı." >> "$LOG_FILE"

# Asıl işlemler burada yapılır
sleep 30  # Örnek uzun süren bir işlem
echo "Veri senkronizasyonu tamamlandı" >> "$LOG_FILE"

echo "$(date '+%Y-%m-%d %H:%M:%S') - Script başarıyla tamamlandı." >> "$LOG_FILE"

# Script bitince flock otomatik olarak kilidi serbest bırakır

Bu yöntemin avantajı daha fazla esneklik sağlamasıdır. Lock alınamadığında ne yapacağınızı kendiniz belirleyebilirsiniz.

Gerçek Dünya Senaryosu 1: Veritabanı Yedekleme

Gece çalışan bir MySQL yedekleme scriptini düşünelim. Normal şartlarda 20 dakikada bitiyor, ama büyük bir deployment sonrası veritabanı boyutu artmış ve yedek 45 dakika sürüyor. Cron ise her 30 dakikada bir tetikleniyor:

#!/bin/bash

LOCK_FILE="/var/lock/mysql_backup.lock"
BACKUP_DIR="/backup/mysql"
DATE=$(date '+%Y%m%d_%H%M%S')
LOG_FILE="/var/log/mysql_backup.log"
MYSQL_USER="backup_user"
MYSQL_PASS="guclu_sifre"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - [SKIP] Önceki yedekleme işlemi devam ediyor." >> "$LOG_FILE"
    # İsteğe bağlı: alerting sisteminize bildirim gönderin
    # send_alert "MySQL backup çakışması tespit edildi"
    exit 0
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') - [START] MySQL yedekleme başladı." >> "$LOG_FILE"

# Tüm veritabanlarını yedekle
if mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS" --all-databases --single-transaction 
    | gzip > "$BACKUP_DIR/full_backup_${DATE}.sql.gz"; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - [SUCCESS] Yedekleme tamamlandı: full_backup_${DATE}.sql.gz" >> "$LOG_FILE"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') - [ERROR] Yedekleme başarısız!" >> "$LOG_FILE"
    exit 1
fi

# 7 günden eski yedekleri temizle
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete
echo "$(date '+%Y-%m-%d %H:%M:%S') - [CLEANUP] Eski yedekler temizlendi." >> "$LOG_FILE"

Crontab’daki karşılığı:

# Her 30 dakikada bir çalış
*/30 * * * * /opt/scripts/mysql_backup.sh

Gerçek Dünya Senaryosu 2: Log İşleme Pipeline’ı

Bir web sunucusunun access loglarını işleyip veritabanına yazan bir script düşünelim. Bu tür scriptler özellikle çakışmaya açıktır çünkü log dosyasını okuma, işleme ve veritabanına yazma adımlarının hepsi atomik değildir:

#!/bin/bash

LOCK_FILE="/var/lock/log_processor.lock"
ACCESS_LOG="/var/log/nginx/access.log"
PROCESSED_LOG="/var/log/nginx/access.log.processing"
DB_HOST="localhost"
DB_NAME="analytics"
LOG_FILE="/var/log/log_processor.log"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Log işleyici zaten aktif, bu çalıştırma atlandı." >> "$LOG_FILE"
    exit 0
fi

# Log dosyasını işleme için taşı (atomic operation)
if [ ! -f "$ACCESS_LOG" ] || [ ! -s "$ACCESS_LOG" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - İşlenecek log bulunamadı veya boş." >> "$LOG_FILE"
    exit 0
fi

mv "$ACCESS_LOG" "$PROCESSED_LOG"
# nginx'e yeni log dosyası açması için sinyal gönder
nginx -s reopen 2>/dev/null

echo "$(date '+%Y-%m-%d %H:%M:%S') - Log işleme başladı: $(wc -l < "$PROCESSED_LOG") satır" >> "$LOG_FILE"

# Logları işle ve veritabanına yaz
awk '{print $1, $7, $9}' "$PROCESSED_LOG" | 
while read ip url status; do
    mysql -h"$DB_HOST" "$DB_NAME" -e 
        "INSERT INTO access_logs (ip, url, status_code, created_at) VALUES ('$ip', '$url', '$status', NOW());" 2>/dev/null
done

echo "$(date '+%Y-%m-%d %H:%M:%S') - Log işleme tamamlandı." >> "$LOG_FILE"
rm -f "$PROCESSED_LOG"

Timeout ile Güvenli Kullanım

Bazen script’in kendisi bir sorun nedeniyle sonlanmıyor ve kilit sonsuza kadar tutuluyor olabilir. Bu durumda timeout komutuyla flock‘u birlikte kullanmak mantıklıdır:

#!/bin/bash

LOCK_FILE="/var/lock/kritik_islem.lock"
MAX_RUNTIME=3600  # Maksimum 1 saat

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Çakışma tespit edildi, çıkılıyor."
    exit 1
fi

# Script 1 saatten uzun sürerse zorla sonlandır
timeout $MAX_RUNTIME bash -c '
    echo "İşlem başladı..."
    
    # Uzun süren işlemler
    for i in $(seq 1 100); do
        echo "Adım $i işleniyor..."
        sleep 10
    done
    
    echo "İşlem tamamlandı."
'

EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - UYARI: Script maksimum süreyi aştı ve sonlandırıldı!"
    # Alerting burada yapılabilir
fi

Kilit Durumunu Kontrol Etme

Bazen mevcut bir kilit var mı diye kontrol etmek istersiniz, mesela monitoring scriptlerinde:

#!/bin/bash

LOCK_FILE="/var/lock/veri_sync.lock"

check_lock_status() {
    # flock -n ile test et, hemen bırak
    if flock -n "$LOCK_FILE" true 2>/dev/null; then
        echo "Kilit boş - script çalışmıyor"
        return 0
    else
        echo "Kilit aktif - script şu anda çalışıyor"
        # Hangi process tutuyor?
        LOCKER_PID=$(fuser "$LOCK_FILE" 2>/dev/null)
        if [ -n "$LOCKER_PID" ]; then
            echo "Kilidi tutan PID: $LOCKER_PID"
            echo "Process detayı: $(ps -p $LOCKER_PID -o pid,ppid,cmd --no-headers 2>/dev/null)"
        fi
        return 1
    fi
}

check_lock_status

Birden Fazla Lock ile Paralel Kontrol

Bazen belirli kaynaklar için ayrı ayrı lock kullanmak gerekir. Örneğin farklı veritabanları için bağımsız scriptler çalıştırıyorsunuz ama her birinin kendi kilidi olsun istiyorsunuz:

#!/bin/bash

# Hangi veritabanı işlenecek?
DB_NAME="${1:-default}"
LOCK_FILE="/var/lock/db_process_${DB_NAME}.lock"
LOG_FILE="/var/log/db_process_${DB_NAME}.log"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $DB_NAME için işlem zaten çalışıyor." >> "$LOG_FILE"
    exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') - $DB_NAME işlemi başladı." >> "$LOG_FILE"

# Veritabanı işlemleri burada
process_database "$DB_NAME"

echo "$(date '+%Y-%m-%d %H:%M:%S') - $DB_NAME işlemi tamamlandı." >> "$LOG_FILE"

Crontab’da farklı veritabanları için paralel çalıştırma:

# Her 3 veritabanı bağımsız olarak kilitlenir, paralel çalışabilirler
*/10 * * * * /opt/scripts/db_process.sh production
*/10 * * * * /opt/scripts/db_process.sh staging
*/10 * * * * /opt/scripts/db_process.sh analytics

Sık Yapılan Hatalar

Lock dosyasının yanlış yerde olması: /tmp dizini bazen temizlenebilir, bu nedenle /var/lock veya özel bir dizin tercih edin.

Yanlış exit kodu kontrolü: flock çalıştırılan komutun exit kodunu döndürür. Kilit alınamadığında exit kodu 1 olur, buna göre hareket edin.

Sudo ile karışıklık: Script root olarak çalışıyorsa ve normal kullanıcı da çalıştırıyorsa, lock dosyasının izinlerine dikkat edin.

Log dizini oluşturmayı unutmak: Script çalışmadan önce log ve lock dizinlerinin var olduğundan emin olun:

#!/bin/bash

# Script başında gerekli dizinleri oluştur
mkdir -p /var/lock
mkdir -p /var/log/myapp

LOCK_FILE="/var/lock/myapp.lock"
LOG_FILE="/var/log/myapp/sync.log"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Zaten çalışıyor, çıkılıyor." >> "$LOG_FILE"
    exit 0
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') - Başladı." >> "$LOG_FILE"
# işlemler...
echo "$(date '+%Y-%m-%d %H:%M:%S') - Tamamlandı." >> "$LOG_FILE"

systemd Timer ile Alternatif Yaklaşım

Modern sistemlerde cron yerine systemd timer kullanıyorsanız, flock‘a gerek kalmadan Type=oneshot ve RemainAfterExit=no ayarlarıyla benzer koruma sağlanabilir. Ama mevcut cron altyapınız varsa flock çok daha pratiktir.

İkisini birlikte de kullanabilirsiniz tabii ki, systemd timer cron’u tetikler, script içinde de flock çalışır. Bu şekilde ekstra bir güvenlik katmanı oluşturmuş olursunuz.

Monitoring ve Alerting Entegrasyonu

Production ortamında sadece sessizce atlamak yetmez, çakışma olduğunda haberdar olmak istersiniz:

#!/bin/bash

LOCK_FILE="/var/lock/kritik_sync.lock"
LOG_FILE="/var/log/kritik_sync.log"
ALERT_EMAIL="[email protected]"
APP_NAME="Kritik Senkronizasyon"

exec 200>"$LOCK_FILE"

if ! flock -n 200; then
    MESAJ="$(date '+%Y-%m-%d %H:%M:%S') - UYARI: $APP_NAME çakışması! Önceki instance hala çalışıyor."
    echo "$MESAJ" >> "$LOG_FILE"
    
    # Email gönder (mail komutu kurulu ise)
    echo "$MESAJ" | mail -s "[$APP_NAME] Cron Çakışması Tespit Edildi" "$ALERT_EMAIL" 2>/dev/null
    
    # Slack webhook varsa
    # curl -s -X POST -H 'Content-type: application/json' 
    #   --data "{"text":"$MESAJ"}" 
    #   "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" 2>/dev/null
    
    exit 1
fi

START_TIME=$(date +%s)
echo "$(date '+%Y-%m-%d %H:%M:%S') - [$APP_NAME] Başladı. PID: $$" >> "$LOG_FILE"

# Asıl işlemler
/opt/scripts/gercek_sync_islemi.sh

EXIT_CODE=$?
END_TIME=$(date +%s)
SURE=$((END_TIME - START_TIME))

if [ $EXIT_CODE -eq 0 ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - [$APP_NAME] Tamamlandı. Süre: ${SURE}s" >> "$LOG_FILE"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') - [$APP_NAME] HATA ile sonlandı! Exit code: $EXIT_CODE, Süre: ${SURE}s" >> "$LOG_FILE"
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $APP_NAME hata verdi (exit: $EXIT_CODE)" | 
        mail -s "[$APP_NAME] Script Hatası" "$ALERT_EMAIL" 2>/dev/null
fi

exit $EXIT_CODE

Sonuç

flock basit ama son derece güçlü bir araçtır. Kernel seviyesinde çalışması, stale lock sorunlarının önüne geçmesi ve neredeyse her Linux sistemde hazır bulunması onu cron çakışmalarına karşı ideal çözüm yapar. Yeni bir proje başlatırken ya da mevcut cron yapınızı gözden geçirirken şu prensipleri aklınızda tutun:

  • Kritik olan her cron job’a mutlaka lock mekanizması ekleyin
  • Lock alınamadığında uygun loglama yapın, sessizce geçip gitmeyin
  • Production ortamında alerting ile birleştirin
  • Lock dosyalarını kalıcı dizinlerde tutun
  • Uzun süren script’lerde timeout ile birleştirmeyi düşünün

Bir sysadmin olarak söyleyebilirim ki, flock‘u öğrenmeden önce yaşadığım en sinir bozucu debug oturumlarının çoğu çakışan cron job’lardan kaynaklanıyordu. Birkaç satır eklemenin bu kadar büyük fark yaratacağını görmek her zaman tatmin edicidir.

Yorum yapın