ETL işleri tek sunucuda, birbirinden bağımsız ve gece bir kez çalışıyorsa cron yeter, ama iki şartla: çakışan çalışmayı kilitle engellemeniz ve işi aynı tarih için tekrar çalıştırdığınızda verinin ikilenmemesi gerekir. Seçimi "kaç iş var" sorusu belirlemez. Üç soru belirler: işler birbirini bekliyor mu, kaçan ya da hatalı günleri geriye dönük yeniden doldurmanız gerekiyor mu, hata olduğunda kim ve nasıl haberdar oluyor.
Üç seçenek yan yana
| Sorulacak soru | cron | systemd timer | Airflow |
|---|---|---|---|
| Makine kapalıyken kaçan zamanlama ne olur? | crontab(5) telafiden söz etmez; davranışı varsaymayın, deneyin | Persistent=true ile son tetiklenme zamanı diskte tutulur; zamanlayıcı yeniden etkinleşince, kapalıyken en az bir kez tetiklenecek olan servis hemen tetiklenir (yalnız OnCalendar= ile) | catchup ayarı belirler; yazmazsanız sürümün varsayılanına kalırsınız |
| Aynı iş bitmeden yenisi başlarsa? | Kilidi siz eklersiniz (flock) | Etkinleştirilecek servis zaten çalışıyorsa yeniden başlatılmaz, çalışmaya devam eder | DAG'daki max_active_runs eşzamanlı aktif çalışma sayısını sınırlar |
| Geçmiş günleri yeniden doldurma | Betiğe tarih argümanı verip elle ya da döngüyle çalıştırırsınız | Aynı | Backfill var; tamamlanmış çalışma temizlenerek (clear) arayüzden ya da komut satırından yeniden denenebilir |
| Hatada yeniden deneme | Komutun içine sizin yazdığınız mantık | Servis tanımında ya da betikte sizin kurduğunuz mantık | Görevde retries, retry_delay, execution_timeout |
| Bağımlılık (A bitmeden B başlamasın) | Saat farkıyla tahmin | Zamanlayıcı tek bir servisi tetikler; zincir kurmak için birimleri siz bağlarsınız (bu yazıda denenmedi) | DAG'ın kendisi bağımlılık grafiğidir |
| İşletme yükü | Sıfıra yakın | Sıfıra yakın | Ayrı bir platformu kurmak, yükseltmek ve izlemek |
Cron'da kalacaksanız: çakışmayı kilitleyin
Saatlik çalışan bir yükleme bir saati aşarsa ya da gece işi sürerken biri aynı betiği elle başlatırsa, iki kopya aynı tabloya yazar. flock bunu dosya kilidiyle önler. Üç seçenek işe yarar (flock(1)): -n kilit hemen alınamazsa beklemeden başarısız olur, -w verilen saniye içinde alınamazsa başarısız olur, -E bu iki durumda dönecek çıkış kodunu belirler (varsayılan 1). Kilit, dosya kapanınca otomatik düşer; iş çökse de kilit takılı kalmaz. Betik arka planda çocuk süreç bırakırsa kilit onda kalabilir; bu durumda -o kilit tutan dosya tanımlayıcısını komut çalışmadan önce kapatır.
[email protected]
0 2 * * * flock -n -E 75 /var/lock/etl-satis.lock /opt/etl/yukle.sh || /opt/etl/alarm.sh $?yukle.sh aşağıdaki bölümde. Kilit dosyasının bulunduğu dizine cron'u çalıştıran kullanıcının yazabilmesi gerekir. 75 keyfi bir kod; amaç, kilit çakışmasını işin kendi hata kodlarından ayırmaktır. alarm.sh kodu görüp "çakışma" ya da "iş hatası" diye farklı mesaj atabilir. -n ve -w ile birlikte -E'nin sizin dağıtımınızdaki flock sürümünde bulunduğunu man flock ile kontrol edin; yukarıdaki sayfa güncel util-linux içindir.
Çakışmada işin sessizce atlanması da bir hatadır: o gece veri yüklenmemiştir. Kilit çıkış kodu alarma bağlı değilse, kilidi eklemekle sorunu görünmez hâle getirmiş olursunuz.
Cron'un sessiz kırılma noktaları
| Belirti | Neden | Çözüm |
|---|---|---|
| Dosya adındaki tarih bozuk, komut yarım çalışıyor | Komuttaki kaçışsız % satır sonuna çevrilir; ilk %'ten sonrası komuta girdi olarak gider (crontab(5)) | date +\%F yazın ya da tarihi hesaplayan mantığı betiğe taşıyın |
| Hata olunca kimse haberdar olmuyor | MAILTO boş bırakılmışsa posta gönderilmez; tanımsızsa çıktı crontab sahibine gider | MAILTO'yu yazın, ayrıca log dosyasına yönlendirin ve çıkış koduyla alarm bağlayın |
Betik [[ ... ]] gibi bash söz diziminde hata veriyor | Varsayılan kabuk /bin/sh | Crontab'a SHELL=/bin/bash ekleyin ya da komutu bash ile çalışan betiğe taşıyın |
| cronie'de (RHEL, Fedora, Alma) yaz saati geçişinde iş ya hiç çalışmadı ya da iki kez çalıştı | Var olmayan saat eşleşmez; iki kez yaşanan saat eşleşen işi iki kez çalıştırır (crontab(5)) | İşi geçişin yaşandığı saatlerin dışına alın; cronie'de tablo saatini CRON_TZ ile bilinçli seçin. Yükleme tekrar çalıştırılabilir olsun (aşağıdaki SQL) ve kilit kalsın |
Debian/Ubuntu cron'da geçiş gecesi iş geç çalıştı ya da tekrarlanan saatte çalışmadı | İleri alınan saatte atlanan saatteki işler geçişten hemen sonra çalışır; 3 saatten kısa geri alınan saatte tekrarlanan saate düşen işler yeniden çalışmaz. Yalnızca belirli saate bağlı işler etkilenir, joker (*) kullananlar yeni saate göre çalışır (cron(8)) | Aynı çözüm: işi geçiş saatlerinin dışına alın, yükleme tekrar çalıştırılabilir olsun, kilit kalsın. Debian crontab(5) CRON_TZ'den söz etmez; orada belgelenmiş bir çözüm olarak önermiyoruz |
Elle çalışıp cron'da çalışmayan bir komutla karşılaşırsanız, betiğin içinde komutların tam yolunu yazın ve hatayı log dosyasına yönlendirin.
Orkestratörsüz bir adım ötesi: systemd zamanlayıcı
Aynı sunucuda kalacak ama kaçan çalışmanın telafisini istiyorsanız iki dosya yeter. /etc/systemd/system/etl-satis.service:
[Unit]
Description=Gunluk satis yuklemesi
[Service]
Type=oneshot
User=etl
ExecStart=/opt/etl/yukle.sh/etc/systemd/system/etl-satis.timer:
[Unit]
Description=Satis yuklemesi zamanlayicisi
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
AccuracySec=1min
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetEtkinleştirmek için systemctl daemon-reload sonrası systemctl enable --now etl-satis.timer çalıştırılır. systemd.timer(5) belgesine göre Persistent= yalnız OnCalendar= ile etkilidir, AccuracySec= varsayılanı 1 dakikadır, RandomizedDelaySec= zamanlamayı 0 ile verilen süre arasında rastgele geciktirir (varsayılan 0). Birçok zamanlayıcı aynı saate denk geliyorsa bu gecikme başlangıçları yayar. Zamanlayıcı servisi tetiklediğinde servis zaten çalışıyorsa yeniden başlatılmaz; ek flock bu yolda gerekmeyebilir, ama aynı betiği elle de çalıştırıyorsanız yine işe yarar.
Airflow'a geçerken: tek DAG
Aşağıdaki örnek Airflow 3.3.2 belgesindeki içe aktarma yollarına göredir. Her gün 00:00 UTC'de çalışır, bir önceki günün veri aralığını işler ve aralığın tarihlerini bir sonraki bölümdeki SQL'e verir. schedule="@daily" yerine veri aralıklı zamanlayıcı açıkça seçildi; nedeni aşağıdaki tabloda.
import subprocess
from datetime import timedelta
import pendulum
from airflow.sdk import CronDataIntervalTimetable, dag, task
@dag(
schedule=CronDataIntervalTimetable("0 0 * * *", timezone="UTC"),
start_date=pendulum.datetime(2026, 10, 1, tz="UTC"),
catchup=False, # geçmişi kendiliğinden doldurma; açıkça yazılı
max_active_runs=1, # aynı anda tek çalışma
default_args={"retries": 2, "retry_delay": timedelta(minutes=5)},
)
def gunluk_satis_yukle():
@task
def yukle(*, data_interval_start, data_interval_end):
bas = data_interval_start.date().isoformat()
bit = data_interval_end.date().isoformat()
subprocess.run(
["psql", "-v", "ON_ERROR_STOP=1",
"-v", f"bas={bas}", "-v", f"bit={bit}",
"-f", "/opt/etl/yukle.sql"],
check=True,
)
yukle()
gunluk_satis_yukle()Parametrelerin anlamı belgelerde şöyle geçer: max_active_runs eşzamanlı aktif çalışmayı sınırlar, retries ve retry_delay görev belgesinde tanımlıdır, data_interval_start ve data_interval_end şablon başvurusunda yer alır ve @task fonksiyonuna anahtar sözcüklü argüman olarak verilebilir (TaskFlow öğreticisi). catchup'ın varsayılanı sürüme ve yapılandırmaya göre değiştiği için örnekte değeri açıkça yazılıdır. Bağlantı bilgisi koda girmez; psql bunu ortam değişkenlerinden okur.
Veri aralıklı zamanlayıcıda data_interval_end çalışmanın tetiklendiği andır, data_interval_start aralığın başıdır; tetikleme zamanlayıcısında ise ikisi aynı değerdir (zamanlayıcı belgesi). Airflow 3'te schedule içindeki düz cron dizesi varsayılan olarak tetikleme zamanlayıcısına çevrilir; @daily gibi kısayolların nasıl çözüldüğü belgede açık olmadığı için örnek kısayola dayanmaz. İkisi aynı olursa bas ile bit eşitlenir ve sonraki bölümdeki SQL boş bir aralık siler. Veri aralıklı zamanlayıcıda çalışma kendi aralığından sonra başlar ve logical_date aralığın başlangıcını gösterir, çalışmanın yapıldığı anı değil (DAG çalışmaları belgesi). Bu yüzden "bugünün" çalışması dünün verisini yükler; panelde şaşırtıcı görünür ama doğru işleyiş budur.
Airflow 2 ile 3 arasındaki farklar
| Konu | Airflow 2.x | Airflow 3 |
|---|---|---|
| Çalışma anahtar sözcüğü | execution_date | logical_date; execution_date bağlam değişkenlerinden kaldırıldı |
| Geçmişi doldurma varsayılanı | catchup_by_default varsayılanı True | Yükseltme belgesine göre catchup_by_default artık varsayılan olarak False |
schedule içindeki düz cron dizesi | Veri aralıklı zamanlayıcı (CronDataIntervalTimetable) | Varsayılan olarak tetikleme zamanlayıcısı (CronTriggerTimetable); yükseltme belgesi create_cron_data_intervals varsayılanının False olduğunu söyler |
| İçe aktarma | from airflow.decorators import dag, task; DAG için airflow.models.dag.DAG | from airflow.sdk import dag, task; DAG için airflow.sdk.DAG |
| Zamanlama parametresi | Sürümünüzün belgesine bakın | DAG sınıfı belgesinde schedule bulunur, schedule_interval parametresi yer almaz |
| Standart operatörler | Çekirdek pakette | apache-airflow-providers-standard paketine ayrıldı |
Kaynaklar: Airflow 3'e yükseltme belgesi ve Task SDK DAG başvurusu. Yukarıdaki kod Airflow 3 içindir; 2.x'te yalnız içe aktarma satırı değil, bağlam anahtarları da değişir, o sürümün belgesinden denetleyin.
Airflow'un ne olduğunu bilmiyorsanız önce Airflow rehberine, zamanlama kavramı için zamanlama ve orkestrasyon rehberine bakın.
Hangisini seçerseniz seçin: yüklemeyi yeniden çalıştırılabilir yapın
Üç yolda da bir iş yarıda kesilebilir, elle yeniden çalıştırılabilir, otomatik denenebilir ve zamanlama iki kez tetiklenebilir. Yükleme "sona ekle" biçimindeyse her tekrar satırları ikiler. Çözüm, bir tarih aralığını aynı işlemde silip yeniden yazmaktır; aynı aralık kaç kez çalışırsa çalışsın sonuç aynı kalır.
/opt/etl/yukle.sql (PostgreSQL, psql değişkenleriyle; tablo ve sütun adları örnektir):
BEGIN;
DELETE FROM dwh.satis_gunluk
WHERE satis_tarihi >= :'bas'::date
AND satis_tarihi < :'bit'::date;
INSERT INTO dwh.satis_gunluk (satis_tarihi, magaza_id, adet, tutar)
SELECT s.satis_tarihi, s.magaza_id, SUM(s.adet), SUM(s.tutar)
FROM stg.satis s
WHERE s.satis_tarihi >= :'bas'::date
AND s.satis_tarihi < :'bit'::date
GROUP BY s.satis_tarihi, s.magaza_id;
COMMIT;ON_ERROR_STOP=1 ile çalışan psql, ilk hatada durur; bağlantı kapanınca bitmemiş işlem geri alınır, yani silme tek başına kalıcı olmaz.
Cron ve systemd için çağıran betik /opt/etl/yukle.sh: argüman verilmezse dünü yükler, verilirse o günü yeniden yükler. date -d GNU date söz dizimidir.
#!/bin/sh
bas="${1:-$(date -d yesterday +%F)}"
bit=$(date -d "$bas + 1 day" +%F)
psql -v ON_ERROR_STOP=1 -v bas="$bas" -v bit="$bit" -f /opt/etl/yukle.sqlGeçmişi doldurmak için /opt/etl/yukle.sh 2026-10-05 yeter. Airflow'da aynı aralığı temizleyip yeniden çalıştırmak aynı SQL'i aynı tarihlerle çalıştırır. Yalnızca değişen kayıtları çekmek ve kaçan kayıtları yakalamak ayrı bir konudur; onun için artımlı yüklemede kayıp kayıt yazısına bakın. Kavramın kendisi için ETL rehberi yeterlidir.
Orkestratöre geçmeden önce altı soru
- İşler birbirini bekliyor mu, yani B'nin A'nın başarısına bağlı olduğu en az bir zincir var mı?
- Geçmiş günleri yeniden doldurmak istisna mı, yoksa ayda birkaç kez yaptığınız bir iş mi?
- Hata olunca bakılacak yer tek ve herkesin görebildiği bir ekran mı olmalı?
- Zamanlama tanımı kod gözden geçirmesinden ve sürüm kontrolünden geçmeli mi? Crontab sunucuda duran bir dosyadır.
- Airflow'u kuracak, yükseltecek ve izleyecek biri var mı? Bu emeği karşılayacak bir işiniz var mı?
- Mevcut işler tekrar çalıştırılabilir mi? Değilse orkestratör sorunu çözmez, yalnızca ikilenmeyi otomatikleştirir.
Karar verirken şöyle bir yol izleyin:
- İşler bağımsız, tek sunucuda ve yeniden doldurma nadirse: cron ile
flockve çıkış koduna bağlı alarm. - Aynı sunucuda kalıyor ama kaçan çalışmanın telafisi lazımsa:
Persistent=trueile systemd zamanlayıcı. - Zincir bağımlılığı, rutin yeniden doldurma ve ortak izleme gerekiyorsa: Airflow. Kurulum ve işletim desteği düşünüyorsanız Apache Airflow danışmanlık sayfası var.



