1 puan yazan GN⁺ 2025-04-29 | 1 yorum | WhatsApp'ta paylaş
  • Rails'te her tenant için ayrı bir veritabanı kullanan bir yapının nasıl kurulacağı ve bu süreçteki zorluklar açıklanıyor
  • ActiveRecord temelde tek bir DB bağlantısını varsayacak şekilde tasarlandığı için, tenant bazlı bağlantı geçişi karmaşık ve zahmetli
  • Rails 6 ve sonrasındaki connected_to özelliği kullanılarak çalışma zamanında bağlantıyı dinamik olarak değiştirme yöntemi öneriliyor
  • SQLite3, küçük ölçekli ve çok sayıda bağımsız DB ile çalışmaya uygun olduğundan yedekleme, hata ayıklama, silme gibi işlemleri kolaylaştırıyor
  • Büyük sistem optimizasyonları etrafında gelişen Rails altyapısına karşılık, küçük ve bağımsız veritabanları merkezli bir mimarinin de mümkün olduğu vurgulanıyor

Her tenant için ayrı veritabanı kullanmanın nedeni

  • Veri modeli içinde bağımsız çalışan tenant (Site) bazında ayrım yapmak, veri izolasyonunu ve yönetimi kolaylaştırır
  • Her tenant'ın verisini ayrı bir DB'de tutmak, büyük ölçekli site büyümesi veya güvenlik sorunları karşısında avantaj sağlar
  • SQLite kullanıldığında, sunucu yapılandırması gerekmeksizin tek bir dosyayla veritabanı çalıştırılabildiği için kullanım basit ve esnektir

Rails'te zor olan noktalar

  • SQLite'ın temel open/close işlemleri çok basittir, ancak ActiveRecord içeride karmaşık bir bağlantı yönetimi yapısına sahiptir
  • ActiveRecord, bağlantıları modele sabitleyen bir yapıyla tasarlandığından, çalışma zamanında tenant değiştirmek zordur
  • Bağlantı havuzu, sorgu önbelleği ve şema önbelleği gibi bileşenlerin tümü bağlantıya bağlı olduğundan, her seferinde bağlantıyı değiştirmek maliyetlidir

Rails'te çoklu veritabanı yönetiminin geçmişi

  • Rails 1: DB, ActiveRecord::Base düzeyinde belirtilebiliyordu
  • Rails 3: bağlantı havuzu eklendi
  • Rails 4: connection_handling eklendi
  • Rails 6: connected_to eklendi
  • Rails 7: connected_to genişletildi ve sharding desteği eklendi
  • Ancak hâlâ "çalışma zamanında dinamik olarak tenant ekleme/silme" gibi senaryolar varsayılan olarak desteklenmiyor

Tenant başına veritabanının avantajları

  • Tenant bazında yalnızca ilgili dosyalar yedeklenip geri yüklenebildiği için, operasyon ve hata ayıklama basitleşir
  • Tenant kaldırma işlemi yalnızca dosyayı silerek (unlink) yapılabilir
  • Büyük veritabanı sunucuları onlarca terabaytlık DB'leri optimize ederken, SQLite binlerce küçük DB için optimize edilmiştir
  • Nitekim iCloud da milyonlarca küçük SQLite DB'yi Cassandra üzerinde saklayan bir yapı kullanıyor

Sorunun çözüm süreci

  • Mevcut yaklaşım (elle establish_connection) çoklu erişim ortamında ConnectionNotEstablished hatasına yol açıyordu
  • Rails 6 sonrası yaklaşıma uygun olarak, bağlantı havuzunu elle yönetmek yerine bunun Rails tarafından yönetilmesi tercih edildi
  • Her tenant için dinamik olarak bir connection pool oluşturulup, işlemler connected_to bloğu içinde yürütüldü
  • Middleware kullanılarak istek anında gereken DB bağlantısının dinamik olarak hazırlanıp sonrasında serbest bırakıldığı bir yapıya geçildi

Temel kod deseni

  • Önce connection pool kontrol edilir, yoksa oluşturulur
MUX.synchronize do  
  if ActiveRecord::Base.connection_handler.connection_pool_list(role_name).none?  
    ActiveRecord::Base.connection_handler.establish_connection(database_config_hash, role: role_name)  
  end  
end  
  • Bağlantı kurulduktan sonra sorgular connected_to bloğu içinde güvenli şekilde çalıştırılır
ActiveRecord::Base.connected_to(role: role_name) do  
  pages = Page.order(created_at: :desc).limit(10)  
end  

Rack streaming işleme

  • Rack yanıtı streaming ise, bağlantı yönetimi için Rack::BodyProxy ve Fiber kullanılarak bağlantının güvenli biçimde kapatılması sağlanır
connected_to_context_fiber = Fiber.new do  
  ActiveRecord::Base.connected_to(role: role_name) do  
    Fiber.yield  
  end  
end  
connected_to_context_fiber.resume  
  
status, headers, body = @app.call(env)  
body_with_close = Rack::BodyProxy.new(body) { connected_to_context_fiber.resume }  
  
[status, headers, body_with_close]  

Nihai middleware yapısı

  • Her istekte uygun DB bağlantısını bulup connected_to ile geçiş yapan ve yanıt tamamlandığında temizlik yapan Shardine::Middleware adlı bir middleware yazıldı
  • Rails projesinin config.ru dosyasında aşağıdaki gibi uygulanabilir
use Shardine::Middleware do |env|  
  site_name = env["SERVER_NAME"]  
  {adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}  
end  

Kalan konular

  • ActiveRecord 6'da henüz shard özelliği kullanılmamış olsa da, sonraki sürümlerde okuma/yazma ayrımı da mümkün olabilir
  • Tenant silinirken connection pool temizliği şu an gerekli olmadığından uygulanmadı
  • Gelecekte "çok sayıda küçük veritabanı" ile çalışan mimarilerin daha fazla ilgi görmesi muhtemel

1 yorum

 
GN⁺ 2025-04-29
Hacker News görüşleri
  • Yaklaşık 1 milyon kullanıcıyla "database-per-tenant" yaklaşımı kullanılıyor

    • Bu yaklaşım, okuma ağırlıklı uygulamalar için uygun ve çoğu tenant küçük olduğundan tablolarda çok fazla kayıt bulunmuyor; bu yüzden karmaşık join'ler bile çok hızlı
    • Asıl sorun, her bir veritabanını tek tek migrate etmek gerektiği için release süresinin ciddi biçimde uzayabilmesi
    • Schema veya veri drift'i oluşursa release durabiliyor ve bazı tenant'larda özelliklerin neden çalışmadığını bulmak gerekiyor
  • SQLite seviliyor, ancak mevcut OLTP veritabanlarının indekslerin bir kısmını bellekte tutmayıp unload etmesi gerekip gerekmediği merak ediliyor

    • Kullanıcı başına veritabanı kullanıldığında, aktif olmayan kullanıcılar ya da yalnızca başka instance'larda aktif olan kullanıcılar için bellekte hiçbir şey tutulmuyor
    • Bu, Mongo'nun JSON durumu ile benzer ve Postgres, Mongo'dan iki kat daha hızlı
  • Çoğu insan tenant başına veritabanına ihtiyaç duymaz; bu yaygın bir yaklaşım değil

    • Migration ve schema drift gibi dezavantajları dengeleyecek belirli kullanım durumları var
    • Kullanabiliyor olmanız, mutlaka kullanmanız gerektiği anlamına gelmez
    • Dikkatli ilerlemek ve gerçekten tenant başına veritabanına ihtiyaç duyduğunuzu bilmek gerekir
  • Ara bir yaklaşım olarak şunlar düşünülebilir

    • En büyük N tenant'ı belirlemek
    • Bu tenant'lar için DB'yi ayırmak
    • İlk N, IOPS, önem düzeyi (gelir açısından) vb. ölçütlere göre belirlenir
    • Veri modeli, her tenant'a ait satırların çıkarılabilmesine uygun tasarlanmalıdır
  • Tesadüfen Elixir için FeebDB üzerinde çalışılıyor

    • Bu, Ecto'ya bir alternatif olarak görülebilir ve binlerce veritabanı olduğunda iyi çalışmıyor
    • Başlangıçta daha çok eğlenceli bir deney olarak başladı, ancak geçmişte çalışılan her yerde bu mimari çok faydalı olurdu
    • Amaç, veritabanı-tenant yaklaşımının tipik sorunlarını ortadan kaldırmak veya azaltmak
    • Her veritabanı için tek yazar garantisi
    • Tüm tenant'lar için geliştirilmiş bağlantı yönetimi
    • Gerektiğinde migration ve backup desteği
    • Birden fazla DB üzerinde map/reduce/filter işlemleri desteği
    • Cluster dağıtım desteği
  • Forward Email, her posta kutusu/kullanıcı için şifrelenmiş sqlite db kullanarak benzer bir şey yapıyor

    • Kullanıcı bazlı korumayı farklılaştırmak için harika bir yöntem
  • İsim gerçekten çok iyi. Sean Connery'yi çağrıştırıyor

  • "database per tenant" workflow'u artık yeni sayılmaz

    • James Edward Gray, 2012'de RailsConf'ta bundan bahsetmişti
  • Geçmişte buna benzer bir şey kullanılmış ve bundan çok memnun kalınmış

    • Kullanıcı verilerini isterse tüm veritabanı olduğu gibi verilebiliyor
    • Kullanıcı hesabını silerse rm username.sql ile kolayca halledilebiliyor
    • Compliance çok daha kolay hale geliyor
  • Veriler birbirinden izole olduğunda ve tek bir tenant içinde ölçekleme sorunu olmadığında kötü bir tasarım yapmak zor

    • Neredeyse her şey işe yarar