Sessiz başarısızlık nöbetçisi: heartbeat ve dead man's switch ile otomasyon denetimi
Heartbeat ve dead man's switch ile otomasyon denetimi: JSON nabız yazan Python sarmalayıcı, YAPILDI/ATLANDI/KAYIP raporu üreten nöbetçi ve yanlış alarm kuralları.
Otomasyonun en tehlikeli hâli hata vermesi değil, hiç çalışmadığını kimsenin fark etmemesidir. Bir işin patlaması aslında iyidir: günlük dolar, çıkış kodu sıfırdan farklı döner, birileri bakar. Asıl sorun, zamanlanmış görevin tetiklenip yedi saniyede düşmesi, hiç tetiklenmemesi ya da çalışıp "tamam" deyip gerçekte hiçbir şey yapmamasıdır. Bu yazıda sessiz başarısızlık nöbetçisi dediğim yapıyı anlatıyorum: her iş bitişte bir nabız (heartbeat) kaydı bırakır, bağımsız bir nöbetçi de beklenen saat geçtiği hâlde nabız yoksa alarm üretir. Raylı sistemlerdeki dead man's switch ilkesinin, yani "makinist düğmeyi bırakırsa tren kendiliğinden durur" fikrinin otomasyona uyarlanmış hâli.
Bu konuya merak ettiğim için değil, canımı yaktığı için girdim. Günlük yayın yapan bir otomasyonum vardı; görev planlayıcı her sabah tetikliyor, süreç ayağa kalkıyor, sonra sessizce ölüyordu. Günlüğe tek satır yazılmamıştı. Sitede yeni içerik olmadığını günler sonra, tesadüfen fark ettim. O günden beri kurduğum her işte iki soruyu ayrı ayrı soruyorum: "iş çalıştı mı" ve "çalışmadıysa bunu kim söyleyecek".
Aşağıda iki küçük Python betiği var: biri işi sarmalayıp JSON nabız dosyası yazan sarmalayıcı, diğeri nabızları okuyup YAPILDI / ATLANDI / KAYIP raporu üreten nöbetçi. İkisi de yalnızca standart kütüphaneyle çalışıyor. Asıl değer kodda değil, arkasındaki ayrımlarda; bu yüzden zaman-kör nöbetçinin ürettiği yanlış alarmlara ve tek denemede alarm vermeme kuralına ayrıca yer ayırdım.
Sessiz başarısızlık nasıl olur?
Bir otomasyonun sessizce ölmesinin tek yolu yok; benim karşılaştığım hâller şunlar:
- Zamanlayıcı hiç tetiklemiyor: makine kapalı, kullanıcı oturumu açık değil, görev fark edilmeden "devre dışı" durumuna düşmüş.
- Görev tetikleniyor ama süreç ilk saniyelerde düşüyor; günlük dosyası açılamadan bittiği için iz de kalmıyor.
- Betik çalışıyor, çıkış kodu sıfır, ama eski bir dosyayı okuyup aynı çıktıyı yeniden üretiyor. Cron "başarılı" diyor, sitede yeni içerik yok.
- İş bilerek atlanıyor (bugün yayın günü değil, tampon boş) ama bunu hiçbir yere yazmıyor; atlanan iş ile ölen iş aynı görünüyor.
- Nöbetçi var ama onu çalıştıran zamanlayıcı da aynı makinede; makine kapanınca iş de uyarı da birlikte susuyor.
Ortak nokta şu: bu hâllerin hiçbiri hata üretmiyor. Klasik izleme "hata var mı" diye bakar; burada aranması gereken "beklenen şey oldu mu" sorusudur. Bu iki soru farklı araçlar ister. Sistem kaynaklarını izleyen bir betik makinenin sağlığını söyler; işin yapılıp yapılmadığını söylemez.
Her iş bitişte nabız bırakır: Python sarmalayıcı
İlk kural: iş kendi durumunu kendisi yazmaz, onu çalıştıran sarmalayıcı yazar. Çünkü iş çökerse kendi kaydını atamaz; sarmalayıcı ise çöken işin çıkış kodunu görür ve kaydı yine de bırakır. Sarmalayıcı şu bilgileri tek bir JSON dosyasına koyar: iş adı, durum, çıkış kodu, başlangıç ve bitiş zamanı, süre, çıktının son birkaç satırı, makine adı.
Üç durum ayrımı bilinçli. YAPILDI, çıkış kodu sıfır demek. ATLANDI, işin "bugün bana görev yok" diyerek önceden kararlaştırılmış bir kodla çıkması; bu bir sorun değil, kayıt altına alınmış bir karar. HATA ise geri kalan her şey. Dosya önce geçici bir ada yazılıp sonra yerine taşınıyor; böylece nöbetçi yarım kalmış bir JSON okuyup yanlış karar vermiyor.
#!/usr/bin/env python3
"""nabiz.py — bir komutu calistirir, bitiste JSON nabiz dosyasi birakir.
Kullanim:
python nabiz.py --is blog-yayin -- python yayinla.py --sunucu
"""
import json
import os
import platform
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
NABIZ_DIZINI = Path(os.environ.get("NABIZ_DIZINI", "/var/lib/nabiz"))
ATLANDI_KODU = 3 # is "bugun bana gorev yok" derse bu kodla cikar
ZAMAN_ASIMI_SN = 1800
def nabiz_yaz(is_adi: str, kayit: dict) -> None:
"""Once gecici dosyaya yazar, sonra yerine tasir: yarim kayit kalmaz."""
NABIZ_DIZINI.mkdir(parents=True, exist_ok=True)
hedef = NABIZ_DIZINI / f"{is_adi}.json"
fd, gecici = tempfile.mkstemp(dir=NABIZ_DIZINI, suffix=".tmp")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(kayit, f, ensure_ascii=False, indent=1)
os.replace(gecici, hedef)
def main() -> int:
if "--is" not in sys.argv or "--" not in sys.argv:
print(__doc__, file=sys.stderr)
return 2
is_adi = sys.argv[sys.argv.index("--is") + 1]
komut = sys.argv[sys.argv.index("--") + 1:]
baslangic = datetime.now(timezone.utc)
try:
sonuc = subprocess.run(komut, capture_output=True, text=True,
timeout=ZAMAN_ASIMI_SN)
sys.stdout.write(sonuc.stdout) # isin kendi gunlugu kaybolmasin
sys.stderr.write(sonuc.stderr)
kod = sonuc.returncode
son = (sonuc.stderr or sonuc.stdout).strip().splitlines()[-5:]
except subprocess.TimeoutExpired:
kod, son = 124, [f"zaman asimi: {ZAMAN_ASIMI_SN} sn"]
except OSError as hata: # komut hic baslayamadi
kod, son = 127, [str(hata)]
bitis = datetime.now(timezone.utc)
if kod == 0:
durum = "YAPILDI"
elif kod == ATLANDI_KODU:
durum = "ATLANDI"
else:
durum = "HATA"
nabiz_yaz(is_adi, {
"is": is_adi,
"durum": durum,
"cikis_kodu": kod,
"baslangic": baslangic.isoformat(timespec="seconds"),
"bitis": bitis.isoformat(timespec="seconds"),
"sure_sn": round((bitis - baslangic).total_seconds(), 1),
"son_satirlar": son,
"makine": platform.node(),
})
# Bilerek atlanan is, zamanlayicinin gozunde basaridir; ayrimi nobetci okur.
return 0 if durum == "ATLANDI" else kod
if __name__ == "__main__":
sys.exit(main())
Kullanımı basit: cron ya da görev planlayıcı işi artık doğrudan değil, sarmalayıcı üzerinden çağırıyor: python nabiz.py --is blog-yayin -- python yayinla.py --sunucu. İşin kendi çıktısı yine ekrana ve günlüğe akıyor; ek olarak /var/lib/nabiz/blog-yayin.json her koşumda yenileniyor. Zaman aşımı ve "komut hiç başlayamadı" hâlleri de birer HATA nabzı bırakıyor; sessizce kaybolan hiçbir yol kalmıyor.
Bağımsız nöbetçi: YAPILDI, ATLANDI, KAYIP
Nöbetçi işten bağımsız ikinci bir süreç. Tek görevi var: planı bilir, nabız dosyalarına bakar, her iş için bir sonuç söyler. KAYIP, "yapılmadı ve sebebi de yazılmadı" demek; sessiz başarısızlığın tam tanımı bu. Buna bir de DOĞRULANAMADI eklemek gerekiyor: nöbetçi dosyayı okuyamadıysa (izin sorunu, bozuk JSON, uzak sunucuya bağlanamama) tahmin etmez, okuyamadığını söyler. "Bulgu yok" ile "kontrol etmedim" aynı satıra sığmaz.
#!/usr/bin/env python3
"""nobetci.py — nabiz dosyalarina bakar; YAPILDI / ATLANDI / KAYIP raporu basar.
Kullanim: python nobetci.py [--gun YYYY-MM-DD]
Cikis: 0 = her sey yerinde · 1 = en az bir KAYIP, HATA ya da DOGRULANAMADI
"""
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
NABIZ_DIZINI = Path(os.environ.get("NABIZ_DIZINI", "/var/lib/nabiz"))
TOLERANS = timedelta(minutes=60) # planli saat + bu sure gecmeden KAYIP deme
# is adi -> planli yerel saat. Nobetci yalniz vakti GECMIS isleri sorgular.
PLAN = {
"blog-yayin": (9, 2),
"mezunlar-yayin": (9, 35),
"gece-yedek": (2, 30),
}
def vakti_gecti_mi(saat_dk, simdi):
esik = simdi.replace(hour=saat_dk[0], minute=saat_dk[1],
second=0, microsecond=0)
return simdi >= esik + TOLERANS
def nabiz_oku(is_adi):
yol = NABIZ_DIZINI / f"{is_adi}.json"
try:
return json.loads(yol.read_text(encoding="utf-8"))
except FileNotFoundError:
return None
except (OSError, ValueError) as hata: # izin yok, bozuk JSON: tahmin etme
return {"durum": "DOGRULANAMADI", "hata": str(hata)}
def degerlendir(kayit, simdi):
if kayit is None:
return "KAYIP", "nabiz dosyasi hic yok"
if kayit.get("durum") == "DOGRULANAMADI":
return "DOGRULANAMADI", kayit.get("hata", "?")
bitis = datetime.fromisoformat(kayit["bitis"]).astimezone()
if bitis.date() != simdi.date():
return "KAYIP", f"son nabiz {bitis:%Y-%m-%d %H:%M}, bugun yok"
detay = f"{bitis:%H:%M} · {kayit.get('sure_sn', '?')} sn"
if kayit["durum"] == "HATA":
detay += " · " + (kayit.get("son_satirlar") or ["(ayrinti yok)"])[-1]
return kayit["durum"], detay
def main():
simdi = datetime.now().astimezone()
if "--gun" in sys.argv: # gecmis gun: her isin vakti gecmistir
gun = sys.argv[sys.argv.index("--gun") + 1]
simdi = datetime.fromisoformat(gun).astimezone().replace(hour=23, minute=59)
kirmizi = False
for is_adi, saat_dk in PLAN.items():
if not vakti_gecti_mi(saat_dk, simdi):
print(f"{is_adi:16} BEKLIYOR planli {saat_dk[0]:02d}:{saat_dk[1]:02d}")
continue
durum, detay = degerlendir(nabiz_oku(is_adi), simdi)
print(f"{is_adi:16} {durum:14} {detay}")
kirmizi = kirmizi or durum in ("KAYIP", "HATA", "DOGRULANAMADI")
return 1 if kirmizi else 0
if __name__ == "__main__":
sys.exit(main())
Çıkış kodu tasarımın parçası: en az bir KAYIP, HATA ya da DOĞRULANAMADI varsa 1 döner. Böylece nöbetçiyi Telegram'a, e-postaya ya da başka bir betiğe bağlamak için çıktıyı ayrıştırmak gerekmiyor; çıkış kodu yetiyor. Nabız kayıtlarını küçük bir SQLite tablosuna taşımak da mümkün, ama iş başına bir JSON dosyası bence en dayanıklı hâli: bir dosya bozulsa diğerleri okunmaya devam ediyor.
Zaman-kör nöbetçi ve yanlış alarmlar
İlk nöbetçim zaman-kördü: "bugün nabız var mı" diye bakıyordu, o kadar. Gece yarısından hemen sonra elle çalıştırdığımda günün hiçbir işi henüz başlamadığı için üç KAYIP birden verdi. Yanlış alarm, alarmsızlıktan daha tehlikeli olabilir; ikinci yanlış alarmdan sonra insan kırmızıyı görmezden gelmeye başlar, üçüncüsünde gerçek KAYIP da aynı gürültünün içinde kaybolur.
Çözüm yukarıdaki vakti_gecti_mi fonksiyonu: nöbetçi bir işi yalnızca planlı saati artı tolerans süresi geçtiyse sorgular, öncesinde BEKLİYOR der. Toleransı işin normal süresine göre seçiyorum; bir dakikalık iş için altmış dakika cömert görünebilir ama gereksiz alarmın maliyeti bu cömertlikten büyük. Bu konuda edindiğim birkaç kural daha:
- Nöbetçi
--gunile geçmiş bir günü sorguluyorsa her işin vakti geçmiş sayılır; bugünü sorguluyorsa saat ölçüttür. - Sürekli kırmızı, kırmızı değildir. Nöbetçi yalnızca bugüne bakar; dünkü kayıp dün raporlandı, bugün yeniden alarm üretmemeli.
- Nabız dosyasındaki bitiş zamanı UTC, planlı saat yerel. Karşılaştırmadan önce ikisini aynı dilime çekmezseniz saat dilimi değişiminde bir gün boyunca yanlış rapor alırsınız.
- İş sunucuda koşuyor ama nöbetçi bilgisayardaki eski kopyayı okuyorsa, doğru çalışan sisteme her gün KAYIP der. Durumu işin gerçekten koştuğu yerden okuyun.
Tek denemede alarm yok ve nöbetçinin kendi nabzı
İkinci büyük kural: tek denemede alarm verilmez. Bir sağlık sorgusu 09:12'de hata verip 09:16'da geçiyorsa bu bir arıza değil, ağın olağan dalgalanmasıdır; aynı hata dört saat boyunca her denemede tekrar ediyorsa alarmdır. Bu yüzden nöbetçinin dış dünyaya dokunan her adımı üç denemeli ve aralıklı çalışıyor; alarm metnine de ham hata, deneme numarası ve saat yazılıyor. "Geçici hata" teşhisini koymak için ikinci denemenin kanıtı gerekiyor; ilk denemeye bakıp "geçicidir" dediğim durumların önemli bir kısmı sonradan deterministik bir hata çıktı.
Üçüncü ve en çok atlanan kural: nöbetçinin kendisi de sessizce ölebilir. Nöbetçi aynı makinede aynı zamanlayıcıdan koşuyorsa, makine kapandığında hem iş hem uyarı susar; arıza kendini gizler. Dead man's switch tam burada devreye giriyor: nöbetçi her koşumda dışarıdaki bağımsız bir uç noktaya "buradayım" isteği atar; o servis belirlenen süre içinde istek görmezse e-posta gönderir. Healthchecks.io gibi nabız izleme servisleri tam bu iş için var. Bu hattın sır taşımaması, sizin altyapınızdan bağımsız durması ve sizin izlemenizi gerektirmemesi şart; aksi hâlde aynı kesinti onu da götürür.
- İş, nabız dosyası bırakır: yerel ve ayrıntılı.
- Nöbetçi, nabızları okur, raporu basar, dışarıya "buradayım" der.
- Dış gözcü, nöbetçi susarsa insanı arar.
Üç katmanın her biri bir alttakinin sessizliğini yakalıyor. Bu zinciri veri ve otomasyon danışmanlığı kapsamında kurduğum her sistemin standart parçası hâline getirdim; kurulumu bir öğleden sonra sürüyor ve ilk gerçek KAYIP'ı yakaladığı gün maliyetini çıkarmış oluyor.
Özetle: otomasyonda "hata yok" ile "iş yapıldı" aynı şey değil. Her işi nabız bırakan bir sarmalayıcıyla çalıştırın, nabızları planı bilen ve zamana duyarlı bağımsız bir nöbetçiye okutun, nöbetçinin kendi nabzını da dışarıdaki bir gözcüye bağlayın. Tek denemede alarm vermeyin, ama doğrulayamadığınızı da "temiz" diye geçiştirmeyin.
Kendi otomasyonlarınızda hangi işlerin sessizce ölebileceğini birlikte çıkarmak ve nabız-nöbetçi katmanını mevcut zamanlayıcınıza eklemek isterseniz iletişim bölümünden yazabilirsiniz; ilk görüşmede genellikle "bu iş en son ne zaman gerçekten çalıştı" sorusundan başlıyoruz.