1 पॉइंट द्वारा GN⁺ 2025-04-29 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 टिप्पणियां

 
GN⁺ 2025-04-29
Hacker News राय
  • लगभग 10 लाख उपयोगकर्ताओं के साथ "database-per-tenant" तरीका उपयोग में है

    • यह तरीका read-heavy apps के लिए उपयुक्त है, और अधिकांश tenants छोटे होते हैं, टेबलों में बहुत ज़्यादा records नहीं होते, इसलिए जटिल joins भी बहुत तेज़ रहते हैं
    • मुख्य समस्या यह है कि हर database को अलग-अलग migrate करना पड़ता है, इसलिए release time काफ़ी बढ़ सकता है
    • अगर schema या data drift हो जाए, तो release रुक सकता है, और फिर यह पता लगाना पड़ता है कि कुछ tenants में feature काम क्यों नहीं कर रहा
  • SQLite पसंद है, लेकिन यह जिज्ञासा है कि क्या पारंपरिक OLTP databases को index के कुछ हिस्सों को memory से unload करने की ज़रूरत होती है

    • user-per-database इस्तेमाल करने पर inactive users या वे users जो सिर्फ़ दूसरे instances पर active हैं, उनके लिए memory में कुछ भी बनाए नहीं रखना पड़ता
    • यह Mongo की JSON स्थिति जैसा है, और Postgres, Mongo से दोगुना तेज़ है
  • ज़्यादातर लोगों को tenant-per-database की ज़रूरत नहीं होती, और यह सामान्य तरीका नहीं है

    • कुछ खास cases होते हैं जहाँ migration और schema drift जैसी कमियों की भरपाई हो जाती है
    • सिर्फ़ इसलिए कि इसे इस्तेमाल किया जा सकता है, इसका मतलब यह नहीं कि इसे ज़रूर इस्तेमाल करना चाहिए
    • सावधानी से आगे बढ़ना चाहिए और यह स्पष्ट होना चाहिए कि tenant-per-database की ज़रूरत है
  • एक बीच का तरीका यह हो सकता है

    • top N tenants की पहचान करें
    • इन tenants के लिए DB अलग करें
    • top N का फैसला IOPS, importance (revenue के हिसाब से) आदि के आधार पर किया जाए
    • data model इस तरह डिज़ाइन होना चाहिए कि हर tenant से संबंधित rows निकाली जा सकें
  • संयोग से, Elixir के लिए FeebDB पर काम चल रहा है

    • इसे Ecto के विकल्प के रूप में देखा जा सकता है, और जब हज़ारों databases हों तो Ecto अच्छी तरह काम नहीं करता
    • यह मुख्य रूप से एक मज़ेदार experiment के रूप में शुरू हुआ, लेकिन पहले जिन-जिन जगहों पर काम किया, वहाँ यह architecture बहुत मददगार होता
    • लक्ष्य है database-tenant approach की सामान्य समस्याओं को हटाना या कम करना
    • हर database के लिए single writer की गारंटी
    • सभी tenants के लिए बेहतर connection management
    • ज़रूरत पड़ने पर migration और backup support
    • कई DBs पर map/reduce/filter operations का समर्थन
    • cluster deployment support
  • Forward Email हर mailbox/user के लिए encrypted sqlite db का उपयोग करके इसी तरह का काम करता है

    • user-by-user protection को अलग करने का यह एक शानदार तरीका है
  • नाम बहुत शानदार है। Sean Connery की याद दिलाता है

  • "database per tenant" workflow अभी नया नहीं है

    • James Edward Gray ने 2012 RailsConf में इस पर बात की थी
  • पहले कुछ ऐसा ही इस्तेमाल किया था, और बहुत संतुष्ट था

    • अगर user अपना data चाहता है, तो पूरा database दिया जा सकता है
    • अगर user अपना account delete करे, तो rm username.sql से आसानी से निपटा जा सकता है
    • compliance बहुत आसान हो जाती है
  • जब data आपस में isolate हो और एक single tenant के भीतर scaling की समस्या न हो, तब गलत design करना मुश्किल होता है

    • लगभग सब कुछ काम करेगा