- Rails में हर tenant के लिए अलग database इस्तेमाल करने वाली संरचना कैसे बनाई जाए और इस प्रक्रिया में आने वाली चुनौतियों का वर्णन
- ActiveRecord मूल रूप से single DB connection को ध्यान में रखकर डिज़ाइन किया गया है, इसलिए tenant-वार connection switch करना जटिल और कठिन है
- Rails 6 या उसके बाद के connected_to फ़ीचर का उपयोग करके runtime पर connection को dynamically switch करने का तरीका प्रस्तावित किया गया है
- SQLite3 छोटे आकार के, बड़ी संख्या में स्वतंत्र DB संभालने के लिए उपयुक्त है, इसलिए backup, debugging, deletion जैसे प्रबंधन कार्य आसान हो जाते हैं
- बड़े सिस्टम optimization पर विकसित हुए Rails infrastructure के विपरीत, छोटे और स्वतंत्र databases केंद्रित architecture भी संभव है, इस पर ज़ोर दिया गया है
हर tenant के लिए अलग database इस्तेमाल करने के कारण
- डेटा मॉडल के भीतर स्वतंत्र रूप से काम करने वाले tenant (
Site) यूनिट के आधार पर अलग करने से data isolation और management आसान हो जाता है
- tenant-वार डेटा को अलग DB में रखने से बड़े पैमाने पर site scaling या security issues में भी लाभ मिलता है
- SQLite का उपयोग करने पर server configuration के बिना सिर्फ एक फ़ाइल से database चलाया जा सकता है, इसलिए यह सरल और flexible है
Rails में कठिनाइयाँ
- SQLite का मूल
open/close काम बहुत सरल है, लेकिन ActiveRecord के भीतर connection management की संरचना काफ़ी जटिल है
- ActiveRecord का डिज़ाइन ऐसा है कि connections मॉडल से बंधे रहते हैं, इसलिए runtime पर tenant switch करना कठिन है
- connection pool, query cache, schema cache आदि सब connection पर निर्भर होते हैं, इसलिए हर बार connection बदलना बोझिल हो जाता है
Rails में multi-database management का इतिहास
- Rails 1:
ActiveRecord::Base स्तर पर DB निर्धारित किया जा सकता था
- Rails 3: connection pool पेश किया गया
- Rails 4:
connection_handling जोड़ा गया
- Rails 6:
connected_to पेश किया गया
- Rails 7:
connected_to फ़ीचर का विस्तार और sharding support
- फिर भी "runtime पर dynamically tenant जोड़ना/हटाना" जैसे scenarios का बुनियादी support अब भी नहीं है
tenant-वार database के फ़ायदे
- tenant-वार फ़ाइलों का ही backup या restore किया जा सकता है, इसलिए operations और debugging सरल हो जाते हैं
- tenant हटाना सिर्फ फ़ाइल delete (
unlink) करने जितना आसान है
- बड़े database servers दर्जनों terabytes के DB को optimize करते हैं, जबकि SQLite हज़ारों छोटे DB के लिए optimize किया गया है
- वास्तव में iCloud भी Cassandra के ऊपर लाखों छोटे SQLite DB स्टोर करने वाली संरचना अपनाता है
समस्या समाधान की प्रक्रिया
- मौजूदा तरीका (manual
establish_connection) multi-access environment में ConnectionNotEstablished error पैदा करता था
- Rails 6 के बाद के तरीके के अनुसार, connection pool को manually manage करने के बजाय इसे Rails पर छोड़ने वाली संरचना अपनाई गई
- हर tenant के लिए dynamic रूप से connection pool बनाया जाता है और काम को
connected_to block के भीतर wrap किया जाता है
- middleware का उपयोग करके request के समय ज़रूरी DB connection को dynamically तैयार और release करने वाले तरीके से सुधार किया गया
मुख्य code pattern
- connection pool की जाँच के बाद, अगर वह मौजूद न हो तो उसे बनाना
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
- connection बनने के बाद
connected_to block के भीतर सुरक्षित रूप से query चलाना
ActiveRecord::Base.connected_to(role: role_name) do
pages = Page.order(created_at: :desc).limit(10)
end
Rack streaming processing
- अगर Rack response streaming हो, तो connection management के लिए
Rack::BodyProxy और Fiber का उपयोग कर सुरक्षित रूप से connection बंद किया जाता है
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]
अंतिम middleware संरचना
- हर request पर उपयुक्त DB connection खोजकर
connected_to में switch करना और response समाप्त होने पर cleanup करने वाला middleware Shardine::Middleware तैयार किया गया
- इसे Rails project की
config.ru फ़ाइल में इस तरह लागू किया जा सकता है
use Shardine::Middleware do |env|
site_name = env["SERVER_NAME"]
{adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}
end
बचे हुए कार्य
- ActiveRecord 6 में अभी
shard फ़ीचर का उपयोग नहीं किया गया है, लेकिन बाद के versions में read/write separation भी संभव है
- tenant delete करते समय connection pool cleanup फ़ीचर की अभी ज़रूरत नहीं पड़ी, इसलिए इसे लागू नहीं किया गया
- आगे चलकर "कई छोटे databases" संभालने वाली architecture को और अधिक ध्यान मिलने की संभावना है
1 टिप्पणियां
Hacker News राय
लगभग 10 लाख उपयोगकर्ताओं के साथ "database-per-tenant" तरीका उपयोग में है
SQLite पसंद है, लेकिन यह जिज्ञासा है कि क्या पारंपरिक OLTP databases को index के कुछ हिस्सों को memory से unload करने की ज़रूरत होती है
ज़्यादातर लोगों को tenant-per-database की ज़रूरत नहीं होती, और यह सामान्य तरीका नहीं है
एक बीच का तरीका यह हो सकता है
संयोग से, Elixir के लिए FeebDB पर काम चल रहा है
Forward Email हर mailbox/user के लिए encrypted sqlite db का उपयोग करके इसी तरह का काम करता है
नाम बहुत शानदार है। Sean Connery की याद दिलाता है
"database per tenant" workflow अभी नया नहीं है
पहले कुछ ऐसा ही इस्तेमाल किया था, और बहुत संतुष्ट था
rm username.sqlसे आसानी से निपटा जा सकता हैजब data आपस में isolate हो और एक single tenant के भीतर scaling की समस्या न हो, तब गलत design करना मुश्किल होता है