- 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
- No-Lock: Varsayılan mod; ek kilit yok. Çoğu durumda performans düşüşünü önlemek için kullanılır
- Optimistic Locking: Başarısızlık durumunda Polly kütüphanesi ile yeniden deneme yapılır
- 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
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
SQLite işlemleri varsayılan olarak “deferred” modda başlar
Yani gerçek bir yazma işlemi denenene kadar write lock alınmaz
SQLITE_BUSYhatası, 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_timeoutayarlamak ve yazma içeren işlemleri “immediate” modda başlatmaktırİlgili açıklama bu blog yazısında iyi özetlenmiş
SQLITE_BUSYsorunu olduğunu düşündüm. İlgili örnekleri burada topluyorumSQLITE_BUSYbence 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 tasarlayabiliyorsunuzbusy_timeoutbu 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” moddurSQLITE_BUSYgeçmiyordu; muhtemelen bu ayar eksiktiYazı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
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
SQLite’in yeni hctree özelliği kararlı hale geldiğinde, o noktadan sonra yalnızca SQLite kullanmayı düşünüyorum
İsimdeki
hcmuhtemelen High Concurrency anlamına geliyorresmi 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
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
auto_vacuumaçık olup olmadığını kontrol etmenizi öneririmVACUUM belgeleri
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
"Hah?" dedirten bir kısım vardı; bu yüzden hemen yorumlara baktım, beklediğim gibi...