11 puan yazan GN⁺ 2025-11-02 | 2 yorum | WhatsApp'ta paylaş
  • SQLite’ın dosya tabanlı yapısı basittir, ancak aynı anda birden fazla yazma işlemi yapıldığında kilitleme (locking) çakışmaları oluşabilir
  • Jellyfin uzun süredir SQLite kullanıyor, ancak bazı sistemlerde işlem sırasında veritabanı kilitlendi hataları nedeniyle uygulamanın durduğu sorunlar ortaya çıktı
  • EF Core’un interceptor özelliği kullanılarak üç kilitleme stratejisi (No-Lock, Optimistic, Pessimistic) uygulanıp sorun hafifletildi
  • Optimistic yaklaşım yeniden deneme temelli olduğu için performans kaybını en aza indiriyor, Pessimistic yaklaşım ise kararlılığı artırırken hız düşüşünü göze alıyor
  • Bu yaklaşım, diğer EF Core uygulamalarına da kolayca uygulanabilecek bir yapı sunarak SQLite eşzamanlılık sorunlarına pratik bir alternatif sağlıyor

SQLite’ın temel yapısı ve kısıtları

  • SQLite, uygulama içinde çalışan dosya tabanlı ilişkisel bir veritabanı motorudur
    • Tüm verileri tek bir dosyada saklar ve ayrı bir sunucu uygulaması gerektirmez
  • Tek dosya tamamen uygulama tarafından yönetildiği için, aynı anda birden fazla süreç erişirse çakışma riski vardır
  • Bu nedenle SQLite kullanan uygulamalar aynı anda yalnızca tek bir yazma işlemi gerçekleştirmelidir

Write-Ahead-Log (WAL) modu

  • SQLite, WAL (Write-Ahead-Log) özelliğiyle eşzamanlılık kısıtlarını hafifletir
    • WAL dosyası, veritabanı değişikliklerini kaydeden bir journal dosyası görevi görür
    • Birden fazla yazma işlemini paralel biçimde kuyruğa alır ve okuma sırasında WAL’deki değişiklikleri uygular
  • Ancak WAL da kusursuz değildir; belirli durumlarda hâlâ kilitleme çakışmaları yaşanabilir

SQLite işlem sorunları

  • İşlemler, değişikliklerin atomikliğini garanti etmekten ve okuma engellemesini kontrol etmekten sorumludur
  • Jellyfin’in bazı sistemlerinde işlem sırasında SQLite’ın “database is locked” hatası döndürüp hemen durduğu durumlar görüldü
    • Bu sorun işletim sistemi, disk hızı veya sanallaştırma olup olmamasından bağımsız olarak raporlandı
    • Yeniden üretmesi zor ve düzensiz gerçekleştiği için nedenini saptamak güçtü

Jellyfin’in SQLite kullanım biçimi ve sorunlar

  • Önerilen ortamda (ağ olmayan depolama, SSD) sorun nadir görülse de, 10.11 öncesi sürümlerde paralel iş sınırlandırma hatası nedeniyle
    • Kütüphane tarama işleri aşırı paralel çalışarak binlerce eşzamanlı yazma isteği oluşturdu
    • SQLite motorunun yeniden deneme ve zaman aşımı sınırları aşıldı, bunun sonucunda veritabanı aşırı yüklendi ve hatalar oluştu
  • Uzun işlemler ve verimsiz sorgular da sorunu daha da kötüleştirdi

EF Core tabanlı çözüm

  • Jellyfin, kod tabanını EF Core’a taşıyarak yapısal denetim imkânı kazandı
  • EF Core’un Interceptors özelliği kullanılarak tüm komut ve işlem çalıştırmaları yakalanıp şeffaf kilitleme denetimi uygulandı
  • Üç kilitleme stratejisi getirildi
    1. No-Lock: Varsayılan mod; ek kilit yok. Çoğu durumda performans düşüşünü önlemek için kullanılır
    2. Optimistic Locking: Başarısızlık durumunda Polly kütüphanesi ile yeniden deneme yapılır
    3. Pessimistic Locking: Her yazma işleminden önce ReaderWriterLockSlim ile tüm veritabanı kilitlenir

Optimistic Locking nasıl çalışır

  • İşlemin başarılı olacağı varsayılır; başarısız olursa yeniden denenir
    • İki yazma işlemi çakışırsa biri başarısız olur, belirli bir süre bekledikten sonra yeniden dener
  • Polly kütüphanesi kullanılarak yalnızca kilitten kaynaklanan başarısızlıklar yeniden deneme kapsamına alınır
  • Pessimistic yaklaşıma göre overhead’i daha düşüktür ve performans kaybı daha azdır

Pessimistic Locking nasıl çalışır

  • Her yazma anında tüm veritabanı kilitlenir
    • Yazma sırasında tüm okuma ve yazma işlemleri engellenir
  • Bu yöntem en kararlı ama en yavaş yaklaşımdır
    • Örneğin “Alice” tablosu okunurken “Bob” tablosuna yazmak mümkün olsa bile buna izin vermez
  • ReaderWriterLockSlim kullanılarak çoklu okumaya izin verilir, ancak yalnızca tek bir yazmaya izin verilir

Gelecek planı: Smart Locking

  • Optimistic ve Pessimistic yaklaşımlarını birleştiren Smart Locking de değerlendiriliyor
    • Bu iki yaklaşımın avantajlarını bir araya getirerek performans ve kararlılık arasında denge kurulması hedefleniyor

Sonuçlar ve uygulanabilirlik

  • İlk test sonuçlarına göre, her iki kilitleme modu da sorunu çözmede etkili oldu
  • Sorunun kök nedeni hâlâ net değil, ancak kullanıcıların artık Jellyfin’i daha kararlı kullanabilmesi için seçenekler mevcut
  • İnternette de benzer hata bildirimleri çoktu, ancak tam bir çözüm bulunmuyordu
  • Jellyfin’in uygulaması, EF Core interceptor tabanlı olduğu için kolayca kopyalanıp uygulanabilecek bir yapı sunuyor
    • Çağıran tarafın iç kilitleme davranışını bilmesi gerekmiyor
  • Aynı SQLite eşzamanlılık sorununu yaşayan diğer EF Core uygulamalarında da hemen kullanılabilir

2 yorum

 
GN⁺ 2025-11-02
Hacker News görüşleri
  • Geçmişte SQLite’in bloklanma sorununu yaşadım; nedenin disk parçalanması (fragmentation) olduğu ortaya çıktı
    Eski Android tabletlerde uygulamayı yıllarca her gün 8 saat kullanan kullanıcılar yavaşlama ve kilit hatalarından şikayet ediyordu
    Verileri kopyalayıp aldığımızda sorun yeniden üretilemiyordu, ama sonunda cihazı doğrudan inceleyince DB dosyasını yeni bir konuma kopyalayıp sonra eski adına geri döndürerek bir nevi “defrag” yapınca sorunun tamamen ortadan kalktığını gördük
    Aynı yöntemle Jellyfin DB’de de performans artışı yaşadım

    • Bunun parçalanmadan çok flash bellek yıpranması olma ihtimali daha yüksek. Acaba bunlar eMMC depolamalı düşük kaliteli tabletler miydi?
    • SQLite’in VACUUM fonksiyonu ile de aynı etkinin elde edilip edilemeyeceğini merak ediyorum
    • İlginç bir örnek. Ama kullanıcıya doğrudan defrag yaptıramayacağımıza göre, daha pratik bir çözüme ihtiyaç var
  • SQLite işlemleri varsayılan olarak “deferred” modda başlar
    Yani gerçek bir yazma işlemi denenene kadar write lock alınmaz
    SQLITE_BUSY hatası, bir okuma işlemi yazmaya dönüşmeye çalışırken başka bir işlemin write lock’ı zaten almış olması durumunda ortaya çıkar
    Çözüm, busy_timeout ayarlamak ve yazma içeren işlemleri “immediate” modda başlatmaktır
    İlgili açıklama bu blog yazısında iyi özetlenmiş

    • Ben de ilk başta bunun bir SQLITE_BUSY sorunu olduğunu düşündüm. İlgili örnekleri burada topluyorum
    • SQLITE_BUSY bence bir tür mimari koku. WAL modunda tasarımı salt okunur connection pool ile tek yazarlı connection pool’u ayıracak şekilde kuruyorum. Böylece kilit sahipliğini net biçimde görebiliyor ve çekişme durumlarını baştan tasarlayabiliyorsunuz
    • busy_timeout bu durumda uygulanmaz. WAL modunda sayfalar tek bir log dosyasına eklendiği için, okuma sırasında yazmaya geçmeye çalışırsanız SQLite serileştirme garantisi için hemen hata verir. Bunu önleyen şey “immediate” moddur
    • Sonuçta SQLite kullanan herkes bir gün bu sorunla bir kez olsun yanacak ve nedenini bulmak için zaman harcayacaktır
    • Blog yazısında SQLITE_BUSY geçmiyordu; muhtemelen bu ayar eksikti
  • Yazının bazı kısımları hatalı görünüyor
    SQLite kendi kilit yönetimini yaptığı için, uygulamanın dosya erişimini doğrudan kontrol etmesi gerekmez
    Ayrıca WAL birden fazla eşzamanlı yazmaya izin vermez. Sadece okuma ile tek bir yazmanın aynı anda yapılabilmesini sağlar

    • Ben de SQLite’i gerçekten çok seviyorum ama bu yazı en temel eşzamanlılık kavramlarında bile yanlış olduğu için tavsiye edemem
  • SQLite harika bir veritabanı ama varsayılan ayarlar (defaults) fazla tutucu
    Gerçek servis ortamında kullanmak için çeşitli PRAGMA ayarlarını değiştirmek gerekiyor

    • Varsayılan olarak hangi PRAGMA’ların açık olmasının iyi olacağını bilmek isterim
    • Böyle bir durumdaysa belki de doğrudan fork edip yeni varsayılanlar oluşturmak daha mantıklıdır
  • SQLite’in yeni hctree özelliği kararlı hale geldiğinde, o noktadan sonra yalnızca SQLite kullanmayı düşünüyorum
    İsimdeki hc muhtemelen High Concurrency anlamına geliyor
    resmi belge bağlantısı

  • Böyle yazıları görünce, sorunun kök neden analizinden çok geçici çözümlere odaklanıldığı hissine kapılıyorum
    Daha derin hata ayıklama ve araştırmayla gerçek nedenin ortaya çıkarılması asıl değerli paylaşım olurdu

    • Muhtemelen yazar konuyu kısmen inceleyip eksik bir çözüm paylaştı. Belki de HN’de daha iyi yanıtları tetiklemek istemiştir. Hani “yanlış cevabı yazarsan doğru cevap daha hızlı gelir” denir ya
  • WAL modunun da sonuçta tek yazıcı, çok okuyucu yapısında olduğunu anlamamış gibi görünüyor
    Paralel yazma mümkün değil; sadece okuma işlemlerinin yazma tarafından bloke edilmemesini sağlıyor
    Tam bir MVCC olsa güzel olurdu ama mevcut yapı da mantığı anlaşılınca gayet iyi çalışıyor

  • Ben de Jellyfin’de benzer bir sorun yaşadım
    Normalde sorunsuz çalışıyor ama bazı durumlarda DB’nin kilitli kalıp donduğu oluyor
    Loglarda yalnızca “database is locked” kalıyor ve çözmek için sonunda Docker container’ını yeniden başlatmak gerekiyor
    En çok TV arayüzünde art arda hızlı şekilde düğmelere basıldığında oluyor

  • Konudan biraz farklı ama SQLite in-memory DB’yi yoğun insert/delete işlemleri için kullanınca bellek kullanımı giderek artıyor
    Örneğin her 5 dakikada bir 100 bin satır ekleyip silme işini günlerce tekrarlarsanız, macOS’ta bellek 1GB’a kadar çıkıyor
    Böyle bir durumda ayarlanabilecek bir seçenek olup olmadığını merak ediyorum

    • Düzenli olarak VACUUM çalıştırıp çalıştırmadığınızı ve auto_vacuum açık olup olmadığını kontrol etmenizi öneririm
      VACUUM belgeleri
    • Buffer’ın kullanım desenine göre dinamik olarak ayarlanması şeklinde normal bir davranış da olabilir
    • Tüm satırlar siliniyorsa, doğrudan tabloyu drop edip yeniden oluşturmak daha verimli olur
  • SQLite harika ama böyle sorunları görünce insan bazen doğrudan Postgres kullanmak daha iyi olmaz mı diye düşünüyor
    Tek dosya taşınabilirliği ya da gömülü kullanım gerekmiyorsa, Postgres eşzamanlılık sorunlarını daha basit çözüyor

    • Ama Jellyfin bir self-hosted medya sunucusu, dolayısıyla Postgres zorunlu olursa kurulum ve bakım daha karmaşık hale gelir. SQLite daha uygun
    • Jellyfin çoğunlukla ev tipi tek kullanıcılı ortamda çalıştığı için SQLite yeterli. Yalnız mevcut ayarlar en iyi durumda görünmüyor
    • SQLite’in avantajlarını görmezden gelip Postgres’e geçelim demek, “kamp yapacaksın diye kulübe inşa et” demek gibi
    • Postgres kullanınca yalnızca kurulum değil, sürüm yükseltmelerinde migration da düşünmek gerekir. SQLite’ta böyle bir yük yok
    • Jellyfin yakın zamanda DB kodunu Entity Framework ile yeniden yazdı; böylece ileride DB seçimini daha esnek hale getirmeye hazırlanıyor
 
ndrgrd 2025-11-03

"Hah?" dedirten bir kısım vardı; bu yüzden hemen yorumlara baktım, beklediğim gibi...