SQLite ve Concurrency: islistesi.com’da Yaşanan Kilitlenme
Bir üretim ERP’sinde veri tutarlılığını sağlamak için kullandığım SQLite veritabanı, birkaç hafta önce beklenmedik bir kilitlenme (lock) sorunuyla beni ciddi şekilde uğraştırdı. Özellikle yoğun işlem gören ve eş zamanlı erişimin yüksek olduğu durumlarda, belirli işlemlerin takılıp kalması ve sistemin yanıt vermez hale gelmesiyle karşılaştım. Bu durum, daha önce büyük bir e-ticaret sitesinde yaşadığım performans sorunlarına benzese de, kök neden farklıydı: SQLite’ın kendi concurrency modelinin sınırları.
Bu yazıda, islistesi.com projesinde karşılaştığım bu SQLite concurrency sorununu, yaşanan kilitlenmeleri, debug sürecini ve nihayetinde bulduğum çözümleri, kendi deneyimlerim üzerinden anlatacağım. Kendi projelerimde genellikle PostgreSQL gibi daha gelişmiş veritabanları tercih etsem de, belirli senaryolarda SQLite’ın sunduğu basitlik ve hafiflik bazen cazip olabiliyor. Ancak bu cazibenin ardında yatan concurrency yönetimi, dikkatli olunması gereken bir konu.
Sorunun Ortaya Çıkışı: Beklenmedik Kilitlenmeler
Her şey, sistemime gelen yoğun bir raporlama isteğiyle başladı. Kullanıcılar, belirli bir tarih aralığındaki sipariş verilerini çekmeye çalıştığında, bazı istekler uzun süre yanıt vermiyor, hatta tamamen takılıyordu. İlk başta sorunun yoğun sorgulardan kaynaklandığını düşündüm, ancak journald loglarını incelediğimde durumun daha karmaşık olduğunu anladım.
May 12 03:14:15 servername systemd[1]: islistesi-backend.service: Main process exited, code=killed, status=9/n/a
May 12 03:14:15 servername systemd[1]: islistesi-backend.service: Failed with result 'signal'.
May 12 03:14:16 servername systemd[1]: islistesi-backend.service: Scheduled restart job, restart counter is: 1
May 12 03:14:16 servername systemd[1]: Stopped islistesi backend service.
May 12 03:14:16 servername systemd[1]: Started islistesi backend service.
Bu loglar, uygulamanın ana işleminin beklenmedik bir şekilde sonlandığını gösteriyordu. Ancak bu, sorunun kendisi değil, bir semptomdu. Sorunun kaynağını bulmak için daha derinlemesine inceleme yapmam gerekti. ps aux komutuyla çalışan işlemleri kontrol ettiğimde, bazı python işlemlerinin CPU ve bellek kullanımının normalin çok üzerinde olduğunu gördüm.
SQLite’ın Concurrency Modeli ve Kilitlenme Türleri
SQLite, varsayılan olarak “Single Writer, Multiple Reader” (SWMR) modelini kullanır. Bu, aynı anda yalnızca bir yazma işleminin gerçekleşebileceği, ancak birden fazla okuma işleminin eş zamanlı olarak yürütülebileceği anlamına gelir. Yazma işlemi sırasında veritabanı dosyası bir “write lock” ile kilitlenir. Okuma işlemleri ise bu kilitlenme sırasında beklemek zorunda kalır.
Bu model, basit uygulamalar ve tek kullanıcılı senaryolar için oldukça etkilidir. Ancak benim durumumda, hem okuma hem de yazma işlemleri aynı anda yoğun bir şekilde gerçekleşiyordu. Özellikle raporlama sorguları, veritabanını uzun süre “read lock” ile meşgul ederken, arka planda çalışan diğer servisler (örneğin, veri güncellemeleri veya yeni sipariş eklemeleri) “write lock” almak için bekliyordu.
Sorunun daha da karmaşıklaşan yanı, SQLite’ın farklı kilitlenme modlarıydı:
- IMPLICIT LOCKS: Varsayılan olarak, her okuma ve yazma işlemi otomatik olarak bir kilit alır.
- EXPLICIT LOCKS:
BEGIN EXCLUSIVE,BEGIN IMMEDIATEgibi komutlarla manuel olarak kilit alınabilir. - WAL (Write-Ahead Logging): WAL modu, SWMR modelinin performansını artırır. Bu modda, yazma işlemleri ana veritabanı dosyasını kilitlemek yerine ayrı bir WAL dosyasına yazar. Okuma işlemleri ise ana veritabanı dosyasını okurken, yazma işlemleri WAL dosyasını günceller. Bu, okuma ve yazma işlemlerinin daha fazla eş zamanlı olmasına olanak tanır.
Benim projemde WAL modu aktifti. Bu, teorik olarak performansı artırmalıydı. Ancak, WAL modu aktif olsa bile, birden fazla yazma işlemi aynı anda gerçekleşmeye çalıştığında veya bir yazma işlemi uzun sürerken, diğer işlemlerin beklemesi kaçınılmazdı. Özellikle BEGIN EXCLUSIVE gibi daha agresif kilit mekanizmalarının yanlış kullanılması, sorunu tetikleyebiliyordu.
Debug Süreci: Kök Nedeni Bulmak
Sorunu çözmek için öncelikle hangi işlemlerin kilitlenmeye neden olduğunu tespit etmem gerekiyordu. Bunun için birkaç yöntem denedim:
- Veritabanı İzleme: SQLite’ın
PRAGMA lock_status;komutu, mevcut kilit durumunu görmemi sağladı. Ancak bu komut, anlık bir görüntü veriyordu ve sorunun tekrarlanmasını beklemek gerekiyordu. - Uygulama Logları: FastAPI backend’imin loglarına, veritabanı sorgularının ne kadar sürdüğünü ve hangi işlemin ne zaman başladığını detaylı olarak kaydettim. Bu, uzun süren sorguları ve potansiyel kilitlenme noktalarını belirlememe yardımcı oldu.
- Sistem Araçları:
stracegibi sistem araçlarını kullanarak, SQLite process’inin dosya erişimlerini ve kilitlenme çağrılarını izledim. Bu, hangi sistem çağrılarının takıldığını anlamama yardımcı oldu.
Uzun süren bir analiz sonucunda, sorunun kaynağının aslında tek bir “ağır” sorgu olmadığını, bunun yerine eş zamanlı gerçekleşen birden fazla yazma işlemi ve bu işlemlerin WAL modundaki kilitlenme stratejisi olduğunu fark ettim. Özellikle sipariş güncellemeleri ve stok takibi yapan servisler, aynı anda birden fazla satırı güncellemeye çalıştığında, birbirlerini bekletiyorlardı.
Çözüm Yolları ve Uygulanan Stratejiler
Bu sorunu çözmek için birkaç farklı yaklaşım denedim ve nihayetinde uyguladığım stratejilerle sistemi kararlı hale getirdim:
-
İşlem Yönetimi Optimizasyonu:
- Her servisin veritabanı ile etkileşimini gözden geçirdim. Gereksiz yere uzun süren veya çok sayıda satırı güncelleyen işlemleri optimize ettim.
BEGIN IMMEDIATEyerineBEGIN EXCLUSIVEkullanmak yerine, işlemlerin mümkün olduğunca kısa tutulmasına odaklandım. SQLite’taBEGIN IMMEDIATEgenellikle daha iyi bir seçenektir çünkü veritabanını tamamen kilitlemek yerine, sadece yazma işlemi için bir kilit talep eder.- N+1 sorgu problemlerini çözmek için sorgularımı yeniden yazdım.
-
WAL Modu Ayarlarının İyileştirilmesi:
PRAGMA journal_mode=WAL;zaten kullanılıyordu. AncakPRAGMA busy_timeout = 5000;gibi ayarları ekleyerek, kilitlenme durumunda ne kadar süreyle bekleneceğini belirledim. Bu, işlemin hemen başarısız olmasını engelledi.PRAGMA synchronous = NORMAL;ayarını kullanıyordum. Daha güvenli bir seçenek olanPRAGMA synchronous = FULL;ayarına geçerek, her işlem sonrasında verinin diskte güvence altına alınmasını sağladım. Bu, veri kaybı riskini azaltsa da performansı bir miktar düşürebilir.
-
Alternatif Veritabanı Modellerini Değerlendirme:
- Son çare olarak, PostgreSQL gibi daha gelişmiş bir veritabanına geçişi düşündüm. Ancak proje gereksinimleri ve altyapı kısıtlamaları nedeniyle bu, o an için uygun bir çözüm değildi.
- SQLite’ın
load_extension()fonksiyonu ile daha gelişmiş concurrency modelleri eklenebilir mi diye araştırdım, ancak bu genellikle daha karmaşık çözümler gerektiriyordu.
En etkili çözüm, işlem yönetimini optimize etmek ve busy_timeout ayarını kullanmak oldu. Geliştirdiğim bir Android spam engelleyici uygulamamda da benzer concurrency sorunları yaşamıştım ve orada da busy_timeout ayarı işe yaramıştı. Bu, basit çözümlerin bazen en etkili olabileceğinin bir göstergesiydi.
import sqlite3
import time
# Busy timeout'u ayarlama (milisaniye cinsinden)
timeout_ms = 5000
conn = sqlite3.connect("my_database.db", timeout=timeout_ms)
cursor = conn.cursor()
# WAL modunu etkinleştirme (eğer aktif değilse)
cursor.execute("PRAGma journal_mode=WAL;")
# Synchronous ayarını FULL olarak ayarlama (daha güvenli)
cursor.execute("PRAGma synchronous=FULL;")
try:
# Uzun sürebilecek bir yazma işlemi
cursor.execute("BEGIN EXCLUSIVE;")
# ... karmaşık UPDATE veya INSERT işlemleri ...
time.sleep(2) # Simülasyon için bekleme
conn.commit()
except sqlite3.OperationalError as e:
if "database is locked" in str(e):
print("Veritabanı kilitli, busy_timeout süresi doldu.")
conn.rollback()
else:
raise
finally:
conn.close()
Çıkarılan Dersler ve Sonuç
islistesi.com projesindeki bu SQLite concurrency sorunu, bana birkaç önemli ders öğretti. Birincisi, basit görünen veritabanlarının bile karmaşık concurrency modelleri olabileceği ve bu modellerin iyi anlaşılması gerektiği. İkincisi, WAL modu performans artsa da, yoğun yazma işlemleri sırasında dikkatli olunması gerektiği. Son olarak, busy_timeout gibi basit ayarların bile ciddi sorunları çözebileceği.
Bu tür sorunlarla karşılaşmak, teknoloji dünyasının bir gerçeği. Önemli olan, sorunu doğru teşhis etmek, farklı çözüm yollarını denemek ve en uygun çözümü bulmaktır. Kendi sistemlerimde bu tür deneyimleri yaşamak, blogumda aktarabileceğim somut ve gerçekçi içerikler üretmemi sağlıyor.
Umarım bu deneyimim, siz de benzer sorunlarla karşılaştığınızda faydalı olur. Sizin de SQLite veya başka veritabanlarıyla yaşadığınız concurrency sorunları ve çözümleriniz varsa, yorumlarda paylaşmaktan çekinmeyin. Belki de bir sonraki yazıda, farklı bir veritabanı sistemindeki benzer bir kilitlenme hikayesini anlatırım.