omeryanbas.com

Ömer Yanbaş

Genel Müdür, Ticofab Yazılım

OperasyonVeri

Gece yarısı hiç çalışmayan zamanlanmış iş

Bir zamda yazılan schedule, başka zamanda koşan makine ve yılda iki kez değişen saat: gecelik iş neden geç, iki kez ya da hiç çalışmıyor.

Gecelik iş gece yarısına kurulur, rapor ise her gün sabah üçte düşer ve crontab satırında yanlış bir şey bulunamaz. Yılda iki kez iş daha da tuhaflaşır: bir pazar iş iki kez çalışır ve aynı satırlar tekrar sayılır, ilkbaharda bir pazar ise hiç çalışmaz. İfade yanlış değildir. Ölçüldüğü saat makinenin saatidir ve o saat, ifadeyi yazan kişinin kafasındaki saat değildir.

Gerçekte ne oluyor

Cron ifadesi, üzerinde saat dilimi taşımayan bir duvar saati değeridir. 0 0 * * * ifadesi, çalışan sürecin yerel saat saydığı şeye göre gece yarısı demektir ve bu kanaat bambaşka bir yerden gelir: host ayarı, container image'ı ya da servis yöneticisinin devrettiği ortam. Base image'lar UTC ile gelir, çünkü bir makine için savunulabilir tek varsayılan budur. Schedule'ı yazan kişi ise ondan birkaç saat ötedeki bir zamanda oturmaktadır. Crontab bu anlaşmazlığı hiçbir yere kaydetmez, dolayısıyla hiçbir yerden raporlayamaz.

Sabit fark, işin kolay yarısıdır. Her gün yanlıştır, o yüzden bir hafta içinde biri fark eder ve rapor makul bir saate düşene kadar sayıyı kaydırır. Asıl zarar da o kaydırmadır: schedule artık niyeti değil bir farkı kodlar ve makine taşındığı ya da kural değiştiği anda yine yanlış olur.

Diğer yarısı yılda iki kez gelir ve yalnızca yaz saati uygulayan zamanları vurur. Saat ileri alındığında bir saatlik duvar saati dilimi hiç yaşanmaz: 02:00'de değişen zamanlarda 01:59'dan sonraki dakika 03:00'tür. 02:30'a kurulmuş bir işin çalışacağı an yoktur. Cron uygulamalarının çoğu o çalışmayı atlar. Bazıları sıçramadan hemen sonra çalıştırır; aynı konfigürasyonla farklı bir davranıştır ve hangisinin geçerli olduğunu yazan manual sayfasını ekipte kimse okumamıştır.

Saat geri alındığında o bir saat iki kez yaşanır. 02:30 gelir, bir saat sonra 02:30 yine gelir ve cron ikisini birbirinden ayıramaz. İş bir ya da iki kez tetiklenebilir. İş bir günlük satırı toplayıp bir toplama yazıyorsa, toplam artık yanlıştır ve hiçbir yerde hata yoktur. Pahalı olan durum budur, çünkü haftalarca bir veri sorunu gibi görünür ve kimse bunu ekimdeki bir tarihe bağlamaz.

Aynı aileden üçüncü bir saat daha var: veritabanındaki saat. Saat dilimi olmadan saklanan, UTC'de koşan bir süreç tarafından yazılıp yerel saatle okunan bir timestamp, tam olarak farka eşit sapan ve bire bir schedule hatasına benzeyen bir sonuç verir. Veritabanında saat dilimi aynı hatanın öbür ucudur.

Nasıl görülür

Makineye saatin kaç olduğunu sorun, sonra aynı soruyu schedule'ın yazıldığı zamanda tekrar sorun:

date +"%F %T %Z %z"
# 2026-05-24 21:12:04 UTC +0000

TZ=Europe/Istanbul date +"%F %T %Z %z"
# 2026-05-25 00:12:04 +03 +0300

Bu iki satır birbirini tutmuyorsa, makinedeki her schedule o kadar kaymış demektir. Cron'un shell profilinizi okumadığını da unutmayın: login dosyasında export ettiğiniz TZ, işe giden TZ değildir.

Yaz saati tarafı için seçtiğiniz dakikanın gerçekten var olup olmadığını test edin. Aşağıdaki kod bir günü dakika dakika yürür ve hedef duvar saati değerinin o zamanda kaç kez göründüğünü sayar:

// bu tarihte, bu zamanda 02:30 kaç kez yaşanıyor?
const zone = 'Europe/Berlin';
const fmt = new Intl.DateTimeFormat('en-GB', {
  timeZone: zone, hour: '2-digit', minute: '2-digit', hour12: false,
});

for (const day of ['2026-03-29', '2026-10-25']) {
  const start = new Date(`${day}T00:00:00Z`).getTime();
  const hits = [];
  for (let m = 0; m < 24 * 60; m++) {
    const t = start + m * 60000;
    if (fmt.format(t) === '02:30') hits.push(new Date(t).toISOString());
  }
  console.log(day, hits.length, hits);
}
// 2026-03-29 0 []
// 2026-10-25 2 [ '2026-10-25T00:30:00.000Z', '2026-10-25T01:30:00.000Z' ]

Bir tarihte sıfır, diğerinde iki. Hatanın tamamı iki satır çıktıda duruyor. Bunu yalnızca kendi zamanınız için değil, schedule'larınızın iddia ettiği her zaman için koşturun.

Çözüm

Her schedule için işin gerçekte hangisini kastettiğine karar verin. Ya sabit bir aralıktır, o zaman gerçek UTC'dir ve yerel saat sadece gösterimdir; ya da yerel duvar saatine verilmiş bir sözdür, mesela raporun birinin masasında dokuzda olması, o zaman zamanı saklamanız ve anı her seferinde yeniden hesaplamanız gerekir. Schedule'ların çoğu birinci türdendir ve kazara ikinci tür gibi yazılır.

Önce sürecin saat dilimini sabitleyin ki makinenin bu konuda bir fikri kalmasın:

CRON_TZ=UTC
30 23 * * * /usr/bin/flock -n /var/lock/nightly.lock /opt/app/bin/nightly

Systemd karşılığı saat dilimini doğrudan takvim ifadesinin içine koyar. Bu daha nettir, çünkü unit dosyası kopyalandığında da ayakta kalır:

[Timer]
OnCalendar=*-*-* 23:30:00 UTC
Persistent=true
AccuracySec=1s

Sonra schedule'ı kendini anlatır hale getirin. En çok hatayı yakalayan tek değişiklik budur, çünkü soyut bir ifadeyi bir insanın okuyabileceği beş tarihe çevirir:

// schedule kaydedilir kaydedilmez formda gösterilir
// istenen: 02:30 yerel, zaman Europe/Berlin
// yerel             utc                 not
// 2026-03-27 02:30  2026-03-27T01:30Z
// 2026-03-28 02:30  2026-03-28T01:30Z
// 2026-03-29 03:30  2026-03-29T01:30Z   02:30 yaşanmıyor, bir saat geç çalışacak
// 2026-03-30 02:30  2026-03-30T00:30Z
// 2026-03-31 02:30  2026-03-31T00:30Z

Kimse bir cron ifadesine bakıp mart satırını görmez. Herkes mart satırını okur.

Son olarak iki kez çalışmayı zararsız hale getirin. Her çalışmaya planlanan anın UTC karşılığı olan bir slot verin ve iş bir şey yapmadan önce o slot'u sahiplensin:

create table job_run (
  job      text        not null,
  slot     timestamptz not null,
  started  timestamptz not null default now(),
  finished timestamptz,
  ok       boolean,
  primary key (job, slot)
);

/* işin çalıştırdığı ilk sorgu; sıfır satır dönerse bu slot'u başkası almış demektir */
insert into job_run (job, slot) values ('nightly', $1)
on conflict do nothing
returning slot;

Dosya lock'u aynı sunucudaki ikinci kopyayı durdurur. Unique key ise iki ayrı sunucudaki kopyayı da, bir saat sonraki retry'ın işi tekrar yapmasını da durdurur. Bu tam olarak işi tam olarak bir kez yapmak yazısındaki özelliktir ve schedule, o kimliği bedava aldığınız ender yerlerden biridir.

Çalıştığını nasıl doğrularsınız

Timer listesi iki saati birlikte gösterir, yan yana görmek istediğiniz şey de budur:

systemctl list-timers nightly.timer
# NEXT                        LEFT     LAST                        PASSED  UNIT
# Sun 2026-05-24 23:30:00 UTC 2h 17min Sat 2026-05-23 23:30:00 UTC 21h ago nightly.timer

Sonra crontab'ı izlemeyi bırakıp çalışmaları izleyin. Tek sorgu, önemli olan tek soruyu yanıtlar: her iş yeterince yakın bir zamanda başarılı olmuş mu?

select job,
       max(finished) filter (where ok)            as last_ok,
       now() - max(finished) filter (where ok)    as age
from job_run
group by job
order by age desc nulls first;

age değeri aralığı artı payı geçtiğinde alarm verin. Schedule'dan tamamen düşen bir iş bu alarmı tetikler, çalışıp hata veren bir iş de tetikler. İkisinden de beklediğiniz davranış budur.

Nelere dikkat etmeli

  • Telafi çalışması iki tarafı da keser. Persistent bir timer, makine kapalıyken kaçırdığı işi açılışta çalıştırır; bu bir rapor için doğru, mesaj gönderen herhangi bir iş için yanlıştır. Kararı iş başına verin ve schedule'ın yanına yazın.
  • Zaman kuralları, bir hükûmet kuralı değiştirdiğinde değişir, bazen birkaç hafta önce duyurulur. Zaman veritabanını işletim sistemiyle birlikte güncel tutun ve ardından uzun ömürlü süreçleri yeniden başlatın, çünkü birçok runtime saat dilimini yalnızca açılışta okur.
  • İki makinedeki tek schedule, iki schedule'dır. Failover çifti, ölçeklenen bir servis ya da birinin test için başlattığı ikinci instance hepsi tetiklenir. Lock'u ikisinin de gördüğü yere koyun, yerel diske değil.
  • Cron ifadesi "ayın son iş günü" diyemez. Geniş bir gün aralığı artı erken çıkışla taklit etmek sorun değil, yeter ki erken çıkış bir satır log yazsın. Sessiz erken çıkış, hiç çalışmamış bir işten ayırt edilemez.
  • Yarım saatlik ve kırk beş dakikalık farklar gerçekten var. Tam saat farkı varsayan kod gerçek kullanıcılarda yanlış sonuç üretir ve hata alakasız bir yuvarlama sorunu gibi görünür.

Dersin özü şu: schedule bir string değil, saat dilimi olan bir veridir; saklanması, doğrulanması ve gösterilmesi de öyle olmalıdır. Bu disiplinin en ucuz hali bir öğleden sonra sürer: her makineyi UTC'ye sabitleyin, kastedilen zamanı kendi alanında tutun, bir insan schedule kaydettiğinde sonraki birkaç çalışmayı yazdırın ve iş başlamadan önce slot'u sahiplenin. Ondan sonrası, gerçekten önemsediğiniz şeyi izlemektir: bir dosyada satır olduğunu değil, işin yapıldığını. Aynı düşünce biçimi log'ları okunmaya değer kılan şeydir, çünkü ikisi de bir çalışmanın ne anlama geldiğini kaydetmeye dayanır, sadece başladığını değil.

Sorular ve cevaplar

Cron işim neden her gece üç saat geç çalışıyor?
Çünkü schedule yerel saatle yazıldı, süreç ise UTC ile koşuyor. Cron ifadesi saat dilimi taşımaz, 0 0 ifadesi makinenin gece yarısı dediği andır. Makinenin zamanını date ile, kastettiğiniz zamanı da aynı komutun başına TZ koyarak görün; aradaki fark gecikmenin tam olarak kendisidir.
Saat değişiminde 02:30'a kurulu işe ne olur?
İlkbahardaki değişimde o dakika hiç yaşanmaz, cron uygulamalarının çoğu çalışmayı tamamen atlar. Sonbahardaki değişimde o dakika iki kez yaşanır ve uygulamaya göre iş bir ya da iki kez tetiklenir. İki durumda da hata üretilmez, bu yüzden sorun genelde log'larda değil rakamlarda fark edilir.
Sunucunun saat dilimini yerel saate çeksem olmaz mı?
Olmaz. Bütün makineleri UTC'de tutun, dönüşümü yalnızca bir insanın saat okuduğu ya da yazdığı uçlarda yapın. Makine başına yerel saat, iki sunucunun log'larını yan yana koymayı imkânsız hale getirir ve her sonbaharda kendi timestamp'lerinizin bir saatini belirsiz bırakır.
İşin iki kez çalışmadığından nasıl emin olurum?
Her çalışmaya bir slot verin; slot, planlanan anın UTC karşılığıdır. İşin yaptığı ilk şey bu slot'u unique key'li bir tabloya insert etmek olsun. Insert zaten var olan bir satıra denk gelirse iş sessizce çıksın. Tek sunucuda dosya lock'u yeter, ama devreye ikinci bir sunucu girebiliyorsa lock'un ortak bir yerde durması gerekir.
Zamanlanmış işlerde doğru olarak ne izlenir?
İş başına son başarılı çalışmanın yaşı izlenir, eşik de aralığın biraz üstüne kurulur. Crontab satırı hiçbir şey kanıtlamaz, başlayıp hata veren bir süreç de başlamış sayılır. Gecelik bir işin son başarısı otuz saat önceyse birinin bunu duyması gerekir.