Bluesky, tek kiracılı SQLite'a geçti
(github.com/bluesky-social)- Bluesky atproto’nun PDS refaktörleme PR’ı #1705, PDS’nin tek kiracılı SQLite veri deposu kullanacak şekilde değiştirilmesini ve kullanıcı bazlı repo ile özel hesap durumunun her biri kendi SQLite dosyasında saklanmasını sağlıyor
- Kullanıcı DB’si
/${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}yol yapısında saklanıyor ve her repo’nun imzalama anahtarı ilgili SQLite dosyasının yanında tutuluyor - Mevcut kullanıcı verisi erişim soyutlaması ActorStore ile değiştiriliyor ve SQLite eşzamanlı transaction desteklemediği için yazma işlemlerinde açıkça store ve transaction kurulması gerekiyor
- Açık DB dosya tanıtıcıları ve imzalama anahtarları LRUCache ile yönetiliyor; bellekte en fazla 30k açık dosya tanıtıcısı ve 30k anahtar tutuluyor, DB önbellekten çıkarıldığında dosya tanıtıcısı kapatılıyor
- Servis durumu yönetimi için ayrı 3 SQLite DB ekleniyor ve bunlar WAL modunda çalıştırılarak eşzamanlı okuma ile streaming replikasyon mümkün hale getiriliyor; PDS dağıtımına Litestream veya benzer bir aracın dahil edilmesi planlanıyor
PR’ın temel değişiklikleri
- PR #1705, PDS’yi tek kiracılı SQLite veri deposu temelli olacak şekilde refaktör ediyor
- Her kullanıcıya özel bir SQLite dosyası bulunuyor ve bu dosyada ilgili kullanıcının repo’su ile özel hesap durumu saklanıyor
- Kullanıcı DB’si, DID hash’i kullanan hiyerarşik bir yolda saklanıyor
- Yol biçimi:
/${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
- Yol biçimi:
- Her repo’nun repo signing key bilgisi SQLite dosyasıyla aynı konumda saklanıyor
ActorStore ve transaction modeli
- Kullanıcı verisine erişim soyutlaması, önceki “services” yapısından ActorStore’a geçiyor
- ActorStore’un temel farkı, okuma ve yazma için sınıfların ayrılmış olması
- SQLite eşzamanlı transaction desteklemediği için, yazma işlemi yapılacaksa store ile transaction açık biçimde kurulmalı
- Commit log’da reader ve transactor yeniden çalışması, actor store transaction race durumlarının ele alınması ve store arayüzünün sadeleştirilmesi gibi değişiklikler yer alıyor
Önbellek ve dosya tanıtıcısı yönetimi
- İmzalama anahtarları ve veritabanları için LRUCache korunuyor
- Yapılandırılmış sınırlar şöyle
- Açık dosya tanıtıcısı üst sınırı: 30k
- Bellekte tutulan anahtar üst sınırı: 30k
- Bir veritabanı önbellekten atıldığında dosya tanıtıcısı kapatılıyor
- İlgili commit’ler arasında
actor store in lru cachevefix open handlesbulunuyor
Servis durumu için 3 SQLite DB
- Kullanıcı bazlı DB’lerin yanı sıra servis durumu yönetimi için ayrı 3 SQLite veritabanı ekleniyor
- service DB: hesap bilgileri, davet kodları, refresh token vb. verileri yönetiyor
- did cache DB: DID resolution önbelleklemesi için yalnızca tek tablo içeriyor
- sequencer DB: bir servisteki tüm repo güncellemelerinin sırasını yöneten yalnızca tek tablo içeriyor
- Her SQLite dosyası WAL mode ile çalışıyor
- WAL mode’un amacı, eşzamanlı okumayı ve streaming replikasyonu mümkün kılmak
- PDS dağıtımına Litestream veya benzer bir aracın dahil edilmesi planlanıyor
İnceleme ve birleştirme durumu
- Bu PR toplam 143 commit içeriyor ve
pds-sqlite-refactorbranch’indenpds-v2branch’ine birleştirildi - Birleştirme tarihi 1 Kasım 2023, merge commit ise
8449ceb - İncelemeyi yapan devinivy, çeşitli notlar ve yorumlar bıraktıktan sonra değişiklikleri onayladı
- devinivy, bu refaktörlemenin “çok sayıda harika sadeleştirme” içerdiğini ve genel olarak daha derli toplu göründüğünü söyledi
- Birleştirme sonrası
pds-sqlite-refactorbranch’i silindi
Sonraki soru
- 28 Şubat 2025’te npetrangelo, bu PR’daki değişikliklerin kapsamına bakarak önceki Postgres mimarisi ile bu PR’ın getirdiği SQLite mimarisi arasındaki trade-off’ların özetlenmesini istedi
- Sağlanan metinde bu soruya Bluesky tarafından verilmiş bir yanıt yer almıyor
1 yorum
Hacker News yorumları
SQLite’ı seviyorum, ancak her tenant için ayrı şema ya da veritabanı kullanma yaklaşımı genelde çok zorluk çıkarıyor
Paylaşımlı bir instance’ta satır düzeyi güvenlik (RLS) kullanırsanız, migration başarısız olsa bile tamamını rollback etmek mümkün olur; ama tenant başına şema modelinde, beklenmeyen veriler yüzünden veri migration’ı başarısız olursa nedeni bulunana kadar kullanıcılar farklı şema sürümlerinde kalır
Sharding ölçeğine ulaşıldığında zaten benzer şeyler yaşanabilir, ancak o noktaya kadar tek veritabanı en kolayıdır; ileride verileri birleştirmeniz veya kaynak sahipliğini atomik olarak taşımanız da gerekebilir
Bu konfigürasyona karşı değilim, kullanım alanları var; ama şirkette tenant başına şema modelinden tüm hızımızla uzaklaşıyoruz. Doğru yatırım yapılmazsa çok fazla sorun çıkarıyor ve ilk fikir ortaya atıldığında buna hazır olunması nadir bir durum bence
İlginç olan şu: yaklaşık 10 yıl önce uygulama tenant başına SQLite ile başlayıp PostgreSQL’de tenant başına şemaya geçmişti; şimdi ise RLS’li tek şemaya gidiyor, yani tamamen ters yönde ilerlemiş olduk
Production’da devasa veritabanları yönetmiş biri olarak bunu bir daha yapmak istemem
Yük yeterince büyüdüğünde her değişiklik riskli hale gelir, çünkü performansın tüm uç durumlarını eksiksiz test etmek mümkün değildir
Ücretsiz katmandaki bir kullanıcının indeksi olmayan bir kod yolunu bulup production’ı bozması da sık görülen bir kalıptır
Veri migration’ı başarısız olduğu için bazı kullanıcıların farklı şema sürümlerinde kalması büyük bir sorun olmayabilir
Bu kadar büyük ve karmaşık bir serviste şema yükseltmeleri genelde aşamalı yapılır: 1. kodu gelecekteki şemayla uyumlu hale getirmek, 2. veriyi migrate etmek, 3. eski şema desteğini kaldırmak
Bu yüzden genelde 1. ve 2. aşama arasındaki durumda uzun süre çalışmanın güvenli olması gerekir. Elbette yeni bug’lar istisna, ama operasyonel açıdan bu prosedür kullanıldığı sürece migration ara durumuna dönen sistemleri de kabul edilebilir görüyorum
Ürünün müşteri sayısı 100’den azsa, kullanıcıların farklı şema sürümlerinde olması hatta daha iyi olabilir
Her müşterinin yükseltme takvimi ve gereksinimleri farklı olabilir; bazı müşteriler için özel işler yapıp fiilen aynı kodu bile çalıştırmayan işler de biliyorum
Sonuçta iş modeline bağlı
Adil olmak gerekirse 10 yıl önce RLS henüz yoktu. PostgreSQL 9.5 ile 2016’da geldi
https://blog.turso.tech/introducing-embedded-replicas-deploy...
https://electric-sql.com/
“SQLite eşzamanlı transaction’ları desteklemiyor” ifadesiyle ne kastedildiğini anlamıyorum
.dbdosyasına UNC veya NFS gibi bir dosya paylaşımı üzerinden erişmediğiniz sürece desteklediğini biliyorum: https://www.sqlite.org/wal.htmlAynı makinedeki birden fazla thread/process’ten veritabanını okumak ve güncellemek için kullandım; tutarlı bir görünüm gerekiyorsa veya transaction’ı uzun süre elde tutmak istemiyorsanız sqlite backup API ile snapshot almak da mümkün
Bir şeyi kaçırıyor olabilirim ve SQLite’a birkaç yıldır dokunmadığım için emin değilim
Değilmiş. Yanılmışım. Aslında daha çok çoklu okuma, tekli yazma modeline yakın
Bunu varsaymışım ve yeterince dikkatli kontrol etmemişim sanırım. Yine de SQLite ile yaptığım veritabanlarının çoğu yazmadan çok okuma ağırlıklıydı
Düzeltiyorum
Biraz beklerseniz hctree [1] olgunlaşacak ve geleneksel backend mekanizması ile yeni uygulanmış eşzamanlılık destekli backend arasında seçim yapabileceksiniz
[1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html
Dokümantasyona göre yazar yalnızca WAL dosyasının sonuna yeni içerik ekler; bu yüzden okuma ve yazma aynı anda mümkündür, ancak tek bir WAL dosyası olduğu için aynı anda yazabilen tek bir yazar vardır
Orijinal yazıda kastedilenin, güncelleme işlemlerinin sırayla çalışması gerektiği olduğunu düşünüyorum
Trafik düşükse çalışır, ancak transaction’lar büyüdükçe veya eşzamanlı yazma sayısı arttıkça WAL açık olsa bile bir noktada database locked sorunuyla karşılaşırsınız
Uygulama seviyesinde bir ölçüde etrafından dolaşabilirsiniz, ancak genelde o noktaya geldiyseniz başka bir veritabanı backend’ini ciddi biçimde değerlendirmeniz gerekir
En azından son kontrol ettiğimde satır düzeyi kilitleme olmadığı, tablo düzeyi kilitlemenin de çok sınırlı olduğu anlamına geliyor olabilir
Dokümantasyona göre yazar hâlâ tüm veritabanı üzerinde kilit alıyor
İlginç; 1 kullanıcıya 1 veritabanı şeklindeki 1:1 stratejiyi sevdim
Ancak kullanıcılar arası aggregation gereken verileri nasıl ele aldıklarını merak ediyorum. Başka bir kullanıcıyı takip ediyorsam ve o kullanıcı bir gönderi paylaştığında benim veritabanım yeni gönderiyle nasıl güncelleniyor; yoksa yalnızca profil verisi veya takip ilişkileri gibi kalıcı verileri mi kapsıyorlar, feed gibi etkileşimli veriler ayrı mı işleniyor merak ediyorum
“Connection pooling”in açık handle sayısını LRU cache ile sınırlamaktan ibaret olması da hoşuma gitti. Her DB bağlantısının tek thread’li olması nedeniyle eşzamanlılığı bağlantı düzeyinde değil tenancy düzeyinde ele almaları da ilginç
Bunun üzerine veritabanı bazında hız sınırlamayı kolayca ekleyip belirli bir kullanıcının kötüye kullanımını da engellemek mümkün gibi görünüyor
Litestream’i rastgele sayıda veritabanı için yapılandırmanın basit bir yolu olup olmadığını da merak ediyorum
Sunucularda SQLite/Litestream kullanımının artmasını görmek her zaman sevindirici. Biz de yeni uygulama geliştirirken kullanıyoruz
SQLite + Litestream, tenant veritabanları için daha iyi bir seçenek; S3/R2’ye replikasyon ve yedekleme maliyeti pahalı bulut yönetimli veritabanlarına göre çok daha ucuz [1]
Azure’daki SQLServer’a kıyasla %3900’e kadar daha ucuz
[1] https://docs.servicestack.net/ormlite/litestream
%3900 daha ucuz ne demek, anlamadım
Önceki fintech işimde şirket, müşteri hesaplarını blob depolamada şifrelenmiş sqlite3 dosyaları olarak saklıyordu; erişim kalıplarına oldukça iyi uyuyordu
Dışarıdan bakınca en kötüyle berbatın birleşimi gibi görünüyor
Birinin gerçek rakamlarla avantajlarını açıklayan ve beklenen kusurları analiz eden iyi bir yazı yazmasını isterdim. Doğru öğrenilirse gerçekten ilginç bir konu olabilir
Dışarıdan bakınca, özellikle profesyonel sistem yöneticisi olmayan çok sayıda kullanıcının çalıştırıp dağıtacağı dağıtık bir sistem yaptığını varsayarsak, oldukça makul bir seçim gibi görünüyor
Buradaki hedefin de bu olması gerektiğini düşünüyorum; ek bir veritabanı ya da başka bir sunucu kurma, yapılandırma ve yönetme ihtiyacından kaçınmanın tasarım hedefi olmasını beklerim
Bluesky’ı daha iyi bilen biri SQLite’ta hangi verilerin saklandığını, hangilerinin saklanmadığını açıklasa iyi olur
Kullanıcılar arası mesajlar gibi şeyler olmadığını varsayıyorum
E-postayı düşünün. Bir e-posta gönderip beş kişiyi cc’ye koyarsanız, yedi kişi aynı e-postanın birer kopyasını kendi e-posta sunucusunda saklar
Yani tek bir e-postayı tutan ve diğer insanların referans verdiği merkezi bir veritabanı yapısı yoktur
İlişkisel veritabanı sharding’i de temelde böyle çalışır
Bu tür veri denormalizasyonu, uygulama ölçeklendikçe; özellikle yazmaya göre okuma oranı yüksek olan çoktan çoğa uygulamalarda neredeyse zorunlu hale gelir
Okuma/yazma oranı düşükse, tek master ve birden çok slave’den oluşan ilişkisel veritabanı yapısı bile şaşırtıcı derecede çok istek ve veriyi kaldırabilir
Şu anda Bluesky fiilen tek PDS’yi kendisi barındırıyor, ancak nihai hedef her son kullanıcının kendi PDS’sine sahip olması
Inrupt/SOLID bu kavrama “pod” diyor
Aslında dün ikinci production PDS’yi onboard ettik; yani ilerleme var
Yalnızca tüm dünyaya yayınlanan herkese açık mesajlar var
Doğrudan mesaj planı olup olmadığını ayrıca araştırmadım
Kullanıcıları sha256 ile hash’leyip iki karakterlik hedef dizinlere ayırmanın sebebi ne olabilir?
md5 çok daha hızlı ve aynı sorunu çözmüyor mu?
“Neden güvenli olmayan bir hash kullandınız?” sorusuna cevap vermek zorunda kalmamak ve olası bir güvenlik sorunu sınıfını ortadan kaldırmak ya da en aza indirmek daha değerli
Ya da benim gibi şirketin güvenlik araçlarına gömülmüş durumdadırlar ve her md5 kullanımı için ayrı istisna oluşturmak istemiyorlardır
Güvenli hash’e ihtiyacınız yoksa hızlı pek çok kriptografik olmayan hash var
Bluesky hâlâ davetiyeyle mi kullanılıyor?
Backend ve kötüye kullanımı önleme tarafında sistemi ölçeklendirirken büyümeyi sınırlamanın bir yolu
Geliştiriciler için ayrı bir bekleme listesi var ve erişimi oldukça hızlı alabiliyorsunuz: https://atproto.com/blog/call-for-developers