Tüm yazılar
Altyapı 7 dk okuma

Edge Sync Monitor & Power Continuity: Raspberry Pi Veri Sürekliliği

Raspberry Pi üzerinde SQLite WAL modu, atomik senkronizasyon ve güvenli kapanma ile güç kesintisine dayanıklı, sıfır veri kayıplı saatlik senkron kurulumu.

#raspberry-pi #python #sqlite #backup #iot #edge-computing

Sahada çalışan küçük cihazlarla uğraşan herkes er ya da geç aynı gerçekle yüzleşir: elektrik gider, hem de en olmadık anda. Raspberry Pi veri senkronizasyonu kurarken yıllar içinde öğrendiğim en önemli ders şu oldu: asıl mesele veriyi merkeze taşımak değil, güç kesildiği anda diskte ne kaldığıdır. Senkronizasyon betiğiniz ne kadar şık olursa olsun, kesinti sırasında yarım kalan bir yazma işlemi veritabanını bozuyorsa o sistem üretim ortamında kullanılamaz.

Bu yazıda, sensör verisi toplayan ve saat başı merkeze gönderen bir Raspberry Pi kurulumunu güç kesintisine dayanıklı hâle getirmek için kullandığım yaklaşımı anlatacağım: SQLite'ın WAL modu, atomik senkronizasyon mantığı, kaldığı yerden devam eden aktarım ve güvenli kapanma. Hedef net: elektrik ne zaman kesilirse kesilsin, onaylanmış tek bir kaydın bile kaybolmaması.

Yaklaşımın tamamı standart araçlarla kuruluyor: Python, SQLite ve birkaç sistem servisi. Ek donanım şart değil, lisanslı yazılım hiç değil. Bu da çözümü hem ucuz hem de yıllarca bakımı kolay bir yapı hâline getiriyor.

Güç kesintisi neden veri kaybettirir?

Sorunu çözmeden önce nerede oluştuğunu bilmek gerekiyor. Bir Raspberry Pi'de veri kaybı genellikle üç katmandan birinde yaşanır:

  • Dosya sistemi tamponu: İşletim sistemi, yazma işlemlerini performans için bellekte biriktirir. Elektrik gittiğinde bellekteki bu tampon diske hiç ulaşmamış olabilir.
  • SD kart denetleyicisi: Ucuz SD kartların kendi iç tamponları vardır ve yazma sırasında güç kesilirse kart bazen tek bir dosyayı değil, koca bir bloğu bozar. Bu yüzden kritik kurulumlarda endüstriyel sınıf kart veya harici SSD tercih ediyorum.
  • Uygulama mantığı: En sinsi katman budur. Betik, kaydı merkeze gönderdi ama "gönderildi" işaretini koyamadan kapandıysa aynı kayıt iki kez gider; tersi durumda ise kayıt yerelde silinmiş ama merkeze hiç ulaşmamış olur.

İlk iki katman donanım ve işletim sistemi ayarlarıyla yönetilir; üçüncü katman ise tamamen bizim yazdığımız kodun disiplinine bakar. Aşağıdaki bölümlerin her biri bu katmanlardan birini kapatıyor.

SD kart tarafında ömrü uzatmanın yolu yazma yükünü azaltmaktan geçer. Flash bellek hücreleri sınırlı sayıda silme-yazma döngüsü kaldırır; küçük ve sık yazmalar, kart denetleyicisinin aynı blokları tekrar tekrar taşımasına yol açarak aşınmayı hızlandırır. Bu yüzden sistem günlüklerini bellek üstü bir dosya sistemine (tmpfs) yönlendiriyorum, takas alanını kapatıyorum ve uygulamanın her ölçüm için ayrı dosya açmasına izin vermiyorum: bütün yazmalar tek bir SQLite dosyasında toplanıyor. Böylece hem kartın aşınması yavaşlıyor hem de güç kesintisinde bozulabilecek dosya sayısı bire iniyor.

SQLite WAL modu ile dayanıklı kayıt

Sensör verisini önce yerel bir SQLite veritabanına yazıyorum; merkezle bağlantı bu dosyanın üzerinden, ayrı bir süreçle kuruluyor. SQLite'ı seçmemin nedeni basit: sunucu süreci gerektirmez, tek dosyadır ve doğru yapılandırıldığında ACID garantilerini küçük bir cihazda bile verir. Buradaki kritik ayar, WAL (write-ahead log) modudur. WAL modunda yeni kayıtlar önce ayrı bir günlük dosyasına eklenir; ana veritabanı dosyası ancak güvenli anlarda güncellenir. Yazma sırasında güç kesilirse SQLite bir sonraki açılışta günlüğü okur ve veritabanını tutarlı son duruma kendisi getirir.

import sqlite3
from datetime import datetime, timezone

def veritabani_ac(yol):
    baglanti = sqlite3.connect(yol, timeout=30)
    baglanti.execute("PRAGMA journal_mode=WAL;")
    baglanti.execute("PRAGMA synchronous=FULL;")
    baglanti.execute("""
        CREATE TABLE IF NOT EXISTS olcumler (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            zaman TEXT NOT NULL,
            sensor TEXT NOT NULL,
            deger REAL NOT NULL,
            senkron_edildi INTEGER NOT NULL DEFAULT 0
        )
    """)
    return baglanti

def olcum_kaydet(baglanti, sensor, deger):
    zaman = datetime.now(timezone.utc).isoformat()
    with baglanti:  # otomatik BEGIN/COMMIT, hata olursa ROLLBACK
        baglanti.execute(
            "INSERT INTO olcumler (zaman, sensor, deger) VALUES (?, ?, ?)",
            (zaman, sensor, deger),
        )

İki ayrıntıya dikkat: synchronous=FULL ayarı, SQLite'ın her commit'te işletim sistemine "bunu fiziksel diske işle" emri vermesini sağlar. Performanstan biraz feragat edersiniz ama saatte birkaç yüz kayıt yazan bir cihazda bu fark hissedilmez. İkincisi, with baglanti bloğu her kaydı kendi işlemi (transaction) içine alır; blok yarıda kesilirse SQLite o işlemi hiç olmamış sayar. Yarım kayıt diye bir şey kalmaz.

WAL modunun disk davranışını da bilmekte fayda var. Günlük dosyası commit'lerle büyür ve SQLite, varsayılan olarak belirli bir sayfa sayısına ulaşıldığında otomatik checkpoint çalıştırıp içeriği ana dosyaya taşır. Sürekli açık tutulan bir okuyucu bağlantısı bu taşımayı engelleyebilir ve günlük dosyası kontrolsüz büyümeye başlar; bu yüzden okuyucu süreçlerin bağlantıyı kısa tutmasına özen gösteriyorum. Yazma yükü yüksek cihazlarda synchronous ayarını NORMAL seviyesine çekmek de meşru bir tercihtir: WAL modunda bu ayar güç kesintisinde veritabanını bozmaz, yalnızca son birkaç commit'in kaybolabilmesi riskini kabul eder. Ben onaylanmış kayıt kaybetmemek için FULL ayarında kalıyorum.

Atomik senkronizasyon ve kaldığı yerden devam

Saatlik senkronizasyonun altın kuralı şu: bir kaydı yerelde "gönderildi" olarak işaretlemeden önce, karşı tarafın o kaydı gerçekten aldığından emin olmalısınız. Bu yüzden akış hep aynı sırayla ilerler: bekleyenleri oku, gönder, sunucudan onay al, ancak ondan sonra işaretle. Ağ tam onay anında koparsa aynı parti bir sonraki turda yeniden gider; merkez tarafında kayıt kimliğine göre tekilleştirme yaptığım için mükerrer veri de oluşmaz. Kaybetmek ile iki kez göndermek arasında seçim yapmak gerektiğinde, her zaman iki kez göndermeyi seçin.

import sqlite3
import requests

def bekleyenleri_senkronla(baglanti, uc_nokta, parti_boyutu=200):
    satirlar = baglanti.execute(
        "SELECT id, zaman, sensor, deger FROM olcumler "
        "WHERE senkron_edildi = 0 ORDER BY id LIMIT ?",
        (parti_boyutu,),
    ).fetchall()
    if not satirlar:
        return 0

    yuk = [
        {"id": s[0], "zaman": s[1], "sensor": s[2], "deger": s[3]}
        for s in satirlar
    ]
    yanit = requests.post(uc_nokta, json=yuk, timeout=15)
    yanit.raise_for_status()  # onay gelmediyse işaretleme yapılmaz

    son_id = satirlar[-1][0]
    with baglanti:
        baglanti.execute(
            "UPDATE olcumler SET senkron_edildi = 1 "
            "WHERE senkron_edildi = 0 AND id <= ?",
            (son_id,),
        )
    return len(satirlar)

Bu fonksiyonun güzelliği, kendi checkpoint'ini veritabanının içinde taşımasıdır. Ayrı bir "son gönderilen kayıt" dosyası tutmaya gerek yok; senkron_edildi sütunu zaten hangi kayıtların gittiğini söylüyor. Cihaz senkronun tam ortasında kapansa bile bir sonraki çalıştırma kaldığı yerden devam eder. Bu "önce doğrula, sonra işaretle" disiplinini yalnızca IoT projelerinde değil, Excel tabanlı kalite kontrol süreçlerini otomatikleştirirken de birebir uyguluyorum; veri nereden akarsa aksın kural aynı.

Merkez tarafında bu akışın karşılığı idempotent bir uç noktadır. Cihaz kimliği ile kayıt kimliğini birleşik anahtar yapıp ekleme işlemini UPSERT mantığıyla kuruyorum: aynı parti iki kez geldiğinde ikinci kopya sessizce eleniyor, veri tabanında tek satır kalıyor. Ağ kesintisi uzarsa yeniden deneme stratejisi devreye girer. Sabit aralıkla ısrar etmek yerine üstel geri çekilme kullanmak, yani her başarısız denemeden sonra bekleme süresini kademeli artırmak gerekir; aksi hâlde bağlantı gelince sahadaki bütün cihazlar aynı saniyede yüklenmeye çalışır ve merkezi kendi elinizle sıkıştırırsınız. Deneme aralığına küçük bir rastgele pay eklemek bu yığılmayı iyice dağıtır; yerel tampon sayesinde bekleme maliyetsizdir.

Güvenli kapanma ve sinyal yönetimi

Her kesinti anlık olmaz; bazen sistem yöneticisi cihazı yeniden başlatır, bazen bir UPS devreye girip kapanma için birkaç dakika kazandırır. Bu senaryolarda işletim sistemi sürece SIGTERM sinyali gönderir ve size temiz kapanma fırsatı verir. Bu fırsatı kullanmayan betik, elinde olan zamanı da çöpe atmış olur.

import signal

kapanma_istendi = False

def kapanma_sinyali(sinyal_no, cerceve):
    global kapanma_istendi
    kapanma_istendi = True

signal.signal(signal.SIGTERM, kapanma_sinyali)
signal.signal(signal.SIGINT, kapanma_sinyali)

def ana_dongu(baglanti, olcum_al):
    while not kapanma_istendi:
        sensor, deger = olcum_al()
        olcum_kaydet(baglanti, sensor, deger)
    # WAL günlüğünü ana dosyaya işle ve bağlantıyı temiz kapat
    baglanti.execute("PRAGMA wal_checkpoint(TRUNCATE);")
    baglanti.close()

Kapanmadan önce çalıştırdığım wal_checkpoint komutu, günlük dosyasında bekleyen her şeyi ana veritabanına işler. Böylece cihaz yeniden açıldığında SQLite'ın toparlanma sürecine iş kalmaz. Betiği systemd servisi olarak koşturuyorsanız, servise makul bir durdurma süresi tanımlamayı unutmayın; varsayılan süre dolduğunda systemd süreci zorla öldürür ve bütün bu nezaket boşa gider.

Donanım tarafı ve izleme

Yazılım disiplini tek başına yetmez; birkaç donanım ve işletme önlemi tabloyu tamamlar:

  • Beslemeyi ciddiye alın: Raspberry Pi'nin voltaj düşümüne toleransı azdır. Kalitesiz adaptör, uzun ve ince kablo kombinasyonu, kesinti olmasa bile dosya sistemi hatası üretebilir.
  • Küçük bir UPS veya süper kapasitör modülü cihaza 30-60 saniyelik kapanma penceresi kazandırır; yukarıdaki sinyal yönetimiyle birleşince neredeyse bütün senaryoları kapatır.
  • Saat problemini unutmayın: Pi'de pil destekli gerçek zaman saati yoktur. Uzun kesinti sonrası cihaz yanlış tarihle açılabilir; kayıtlarda UTC ve tekil kimlik kullanmak bu yüzden önemlidir.
  • Dışarıdan izleyin: Cihazın "ben buradayım" mesajını merkeze düzenli göndermesini sağlayın. Sessizlik de bir sinyaldir ve çoğu zaman en değerlisidir.

İzlemede hangi metriklerin gerçekten anlamlı olduğu deneyimle netleşiyor. Benim vazgeçmediğim dörtlü şu: bekleyen (senkron edilmemiş) kayıt sayısı, son başarılı senkronun üzerinden geçen süre, WAL dosyasının boyutu ve disk doluluk oranı. Bekleyen kayıt sayısının sürekli artması ağ ya da merkez tarafında bir soruna, WAL dosyasının büyümesi ise takılı kalmış bir okuyucu bağlantısına işaret eder. Zaman damgası güvenilirliği için de basit bir önlem uyguluyorum: cihaz her açılışta, ağ üzerinden saat eşitlemesi tamamlanana kadar yazdığı kayıtları ayrı bir bayrakla işaretliyor; merkez bu kayıtları kabul ediyor ama analize sokmadan önce ayrıca değerlendiriyor. Yanlış tarihli veri, eksik veriden daha tehlikelidir çünkü fark edilmeden hesaba karışır.

Bu iş görünüşte küçük bir altyapı detayı; ama sahadan kesintisiz akan temiz veri, talep tahmini için kurduğunuz makine öğrenmesi modellerinin de ham maddesidir. Eksik saatler ve mükerrer kayıtlarla beslenen bir model, ne kadar sofistike olursa olsun yanlış öğrenir. Veri sürekliliği, analitik zincirin en alttaki ve en az konuşulan halkasıdır.

Özetlemek gerekirse: WAL modu diski, atomik işlemler kayıt bütünlüğünü, onay-sonrası-işaretleme senkronu, sinyal yönetimi ise kapanma anını güvence altına alıyor. Bu dört parçayı doğru kurduğunuzda, elektriğin ne zaman gideceğini dert etmeyi bırakıp verinin kendisine odaklanabiliyorsunuz. Benzer bir edge veri toplama kurulumu planlıyorsanız veya elinizdeki sistemde veri kaybı yaşıyorsanız, danışmanlık verdiğim konuların özetini bulabileceğiniz ana sayfamdaki iletişim bölümünden (/#contact) bana yazabilirsiniz; kurulumunuzu birlikte gözden geçirelim.