- 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
Hacker News görüşleri
Yaklaşık 1 milyon kullanıcıyla "database-per-tenant" yaklaşımı kullanılıyor
SQLite seviliyor, ancak mevcut OLTP veritabanlarının indekslerin bir kısmını bellekte tutmayıp unload etmesi gerekip gerekmediği merak ediliyor
Çoğu insan tenant başına veritabanına ihtiyaç duymaz; bu yaygın bir yaklaşım değil
Ara bir yaklaşım olarak şunlar düşünülebilir
Tesadüfen Elixir için FeebDB üzerinde çalışılıyor
Forward Email, her posta kutusu/kullanıcı için şifrelenmiş sqlite db kullanarak benzer bir şey yapıyor
İsim gerçekten çok iyi. Sean Connery'yi çağrıştırıyor
"database per tenant" workflow'u artık yeni sayılmaz
Geçmişte buna benzer bir şey kullanılmış ve bundan çok memnun kalınmış
rm username.sqlile kolayca halledilebiliyorVeriler birbirinden izole olduğunda ve tek bir tenant içinde ölçekleme sorunu olmadığında kötü bir tasarım yapmak zor