İçeriğe Atla
Mustafa Erbay
Teknoloji · 9 dk okuma · görüntülenme Read in English
100%

SQLite ve Concurrency: islistesi.com'da Yaşanan Kilitlenme

islistesi.com projesinde SQLite'ın concurrency sorunlarını ve yaşadığım kilitlenme problemlerini, çözüm adımlarını ve çıkarılan dersleri ilk elden anlatıyorum.

SQLite veritabanı simgesi ve kilitlenme sembolü ile gösterilen concurrency sorunu

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 IMMEDIATE gibi 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:

  1. 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.
  2. 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.
  3. Sistem Araçları: strace gibi 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:

  1. İş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 IMMEDIATE yerine BEGIN EXCLUSIVE kullanmak yerine, işlemlerin mümkün olduğunca kısa tutulmasına odaklandım. SQLite’ta BEGIN IMMEDIATE genellikle 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.
  2. WAL Modu Ayarlarının İyileştirilmesi:

    • PRAGMA journal_mode=WAL; zaten kullanılıyordu. Ancak PRAGMA 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 olan PRAGMA 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.
  3. 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.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

Sıkça Sorulanlar

Bu makale ile ilgili okurların sorduğu yaygın sorular.

Yoğun raporlama isteklerinde SQLite kilitlenmesini nasıl önleyebilirim?
Ben öncelikle veritabanı erişim katmanını asenkron hale getirdim ve her isteği ayrı bir işlem yerine bir iş kuyruğuna yönlendirdim. Böylece aynı anda birden fazla yazma işlemi aynı dosyaya ulaşmaz, okuma‑yazma çakışması azalır. Ayrıca sık kullanılan raporları önbelleğe alarak veritabanına yapılan okuma sayısını düşürdüm. Sorgularımı mümkün olduğunca indeksli hale getirerek I/O süresini kısalttım ve `PRAGMA busy_timeout` değerini artırarak kısa süreli kilitlenmelerde otomatik yeniden deneme sağladım. Bu kombinasyon, kilitlenme olasılığını %70‑80 oranında azalttı.
SQLite yerine PostgreSQL kullanmak, aynı senaryoda daha iyi mi?
Ben PostgreSQL'e geçmeyi düşündüğümde, yüksek eşzamanlılık ve karmaşık transaction yönetimi gerektiren bir ortamda kesinlikle avantaj sağladığını gördüm. PostgreSQL, çoklu bağlantı ve satır‑seviyesi kilitleme sunar, bu da raporlama ve aynı anda veri girişi yapan kullanıcılar arasında çakışmayı büyük ölçüde azaltır. Ancak, PostgreSQL kurulum ve bakım maliyetleri SQLite’a göre daha fazladır; ek bir servis, yedekleme stratejisi ve konfigürasyon gerektirir. Eğer projeniz hafif, tek‑dosya tabanlı ve düşük bakım maliyeti istiyorsa SQLite hâlâ tercih edilebilir, fakat yoğun eşzamanlılık kritikse PostgreSQL daha güvenli bir seçim olur.
Kilitlenme hatası alırsam, ilk debug adımım ne olmalı?
Ben her zaman log dosyalarını ilk inceleme noktası olarak kullanırım. `journalctl -u islistesi-backend.service` gibi komutlarla sistem servisi loglarını ve SQLite’ın `sqlite3_trace` çıktısını yakalarım. Ardından, kilitlenmeye yol açan sorguyu `EXPLAIN QUERY PLAN` ile analiz eder, eksik indeksleri tespit ederim. Eğer aynı anda birden çok yazma işlemi çalışıyorsa, `PRAGMA locking_mode` ve `PRAGMA journal_mode` ayarlarını kontrol ederim; WAL moduna geçmek genellikle sorunu hafifletir. Son olarak, `busy_timeout` değerini yükselterek kısa süreli kilitlenmelerde otomatik yeniden denemeyi etkinleştiririm. Bu adımlar, sorunun kök nedenini hızlıca izole etmeme yardımcı olur.
Write-Ahead Logging (WAL) modunu etkinleştirmek, kilitlenmeleri tamamen çözer mi?
Ben WAL modunu açtığımda okuma‑yazma çakışması önemli ölçüde azalsa da, kilitlenmeleri tamamen ortadan kaldırmadığını gördüm. WAL, okuyucuların veritabanı dosyasına doğrudan erişimini engellemez, ancak yazma işlemlerini ayrı bir log dosyasına yönlendirir; bu sayede bir yazar ve birden çok okuyucu aynı anda çalışabilir. Ancak yüksek yoğunluklu yazma işlemleri hâlâ bir kilit oluşturabilir ve log dosyası büyüdükçe checkpoint süreci gecikmelere yol açabilir. Bu yüzden WAL’ı kullanırken `PRAGMA checkpoint` ayarlarını ve `busy_timeout` değerini de optimize etmek gerekir; aksi takdirde yeni bir darboğaz ortaya çıkabilir.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Yorumlar

Sunucu Taraflı AI Moderasyon

Yorumlar sunucuda yapay zeka ile denetlenir ve kalıcı olarak saklanır.

?
0/2000

Sunucu taraflı AI denetim

✉️ Ücretsiz · Spam yok · İstediğin an çık

Haftalık özet — AI değil, bizzat ben seçiyorum

Haftada bir mail: o haftanın en önemli yazısı, perde arkası notları, ve "bu hafta gerçekten kullandığım araç" bölümü. Az gürültü, çok sinyal.

  • 📌
    Haftanın en iyisi Sadece okumaya değer tek yazı
  • 🔧
    Alet çantası Bu hafta kullandığım araçlar
  • 🧠
    Perde arkası Blog'a girmeyen notlar

Spam yapmıyoruz. İstediğiniz zaman ayrılabilirsiniz. · Sadece Umami (self-hosted, Google yok) ile takip.

Okuma İstatistikleriniz

0

Yazı Okundu

0dk

Okuma Süresi

0

Gün Serisi

-

Favori Kategori

İlgili Yazılar