1 पॉइंट द्वारा GN⁺ 2024-09-26 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Wafris ने Rails middleware client v1 में उपयोगकर्ता-स्वामित्व वाले Redis store को v2 में SQLite-आधारित store से बदल दिया, ताकि deployment की जटिलता और request evaluation latency दोनों कम की जा सकें
  • Redis का शुरुआती चयन Heroku और Sidekiq जैसे Rails ecosystem की जड़ता से प्रभावित था, लेकिन वास्तविक संचालन में उपयोगकर्ताओं पर Redis admin की भूमिका भी आ गई
  • सभी inbound HTTP requests को rules से तुलना करने वाला read path मुख्य bottleneck था, जबकि reporting writes को धीमा, batch में, या async रूप में अलग किया जा सकता था
  • लोकल MacBook Air M2 पर 12 लाख range वाले dataset के साथ worst-case IP range lookup टेस्ट में SQLite ने लोकल Redis की तुलना में लगभग 3 गुना तेज performance दिखाई, और इसमें network latency शामिल नहीं थी
  • v2 समय या request count के आधार पर नए rules की जांच करता है, और बदलाव मिलने पर पूरा नया SQLite DB डाउनलोड करता है; इससे successful installations लगभग 3 गुना बढ़ीं

Redis-आधारित v1 में सामने आया deployment friction

  • Wafris एक open source web application firewall कंपनी है, जो Rails applications के लिए middleware client देती है
  • v1 client को application के साथ deploy होने वाला उपयोगकर्ता-स्वामित्व वाला Redis store चाहिए था
  • Redis के शुरुआती चयन पर Heroku में Redis को आसानी से जोड़ पाने का वातावरण, remote access की सुविधा, और Sidekiq जैसे सफल उदाहरणों का प्रभाव था
  • लेकिन वास्तविक user environments अधिक विविध थे, और कई उपयोगकर्ताओं को Redis deployment और configuration समस्याओं को debug करने में कठिनाई हुई
  • RailsWorld 2023 में भी Rails application के बगल में Redis server का होना स्वाभाविक मानने के प्रति नकारात्मक माहौल था

गति की समस्या का मूल कारण network latency

  • Redis पारंपरिक RDBMS की तुलना में तेज है, लेकिन अलग database होने के कारण connection, memory, और process management की ज़रूरत होती है
  • cloud environments में network latency request processing performance को सीधे प्रभावित करती है
    • Wafris को application में आने वाले हर HTTP request की stored rules से तुलना करनी होती है
    • v1 client को जितना भी तेज बनाया जाए, अगर application जिस network पर deploy है वह धीमा है, तो पूरी response धीमी हो सकती थी
  • Rails apps हमेशा एकल “majestic monolith” के रूप में deploy नहीं होते थे
    • कई zones में deploy की गई apps
    • overlapping responsibilities वाले कई servers में बंटी apps
    • आंशिक रूप से Rails और बाकी अन्य languages या frameworks के साथ deploy की गई apps
  • ऐसे operational environments में Redis का friction और बढ़ जाता था

Wafris request processing में read और write का विभाजन

  • Wafris Rails middleware के रूप में install होता है, जहां “IP 1.2.3.4 को block करो” जैसे rules सेट किए जाते हैं और incoming requests की उनसे तुलना की जाती है
  • सरल रूप में processing flow दो चरणों का है
    1. HTTP request की rules से तुलना, match होने पर 403, नहीं होने पर 200
    2. block, allow, pass जैसे processing results की reporting
  • पहला चरण यानी rule reads reporting writes की तुलना में कहीं अधिक महत्वपूर्ण है
    • requests को क्रमवार process होना पड़ता है
    • filtering काम न करे तो खराब requests पास हो सकती हैं
    • request evaluation उपयोगकर्ता द्वारा महसूस की जाने वाली site performance को प्रभावित करता है
    • reporting writes को धीमे, batch में, या async process किया जा सकता है

SQLite चुनने के कारण

  • Wafris का मुख्य bottleneck network I/O था, और SQLite documentation की यह पंक्ति चयन को प्रभावित करती है: “SQLite does not compete with client/server databases. SQLite competes with fopen()”
  • इस मान्यता के तहत कि केवल network round-trip हटाने से भी Redis-आधारित संरचना से बेहतर गति मिल सकती है, SQLite और Redis का benchmark किया गया
  • संदर्भ सामग्री इस प्रकार थी

Benchmark की सीमा और जानबूझकर रखी गई सीमाएँ

  • यह benchmark सामान्य database performance comparison नहीं था, बल्कि Wafris के hot path और worst-case query पर केंद्रित एक पक्षपाती परीक्षण था
  • worst-case query IP ranges और categories को map करने वाली “lexical decimal” data structure पर lookup थी
    • एक सरल उदाहरण के रूप में, IP address दो addresses के बीच की range में आता है या नहीं यह जांचकर country लौटाने वाला IP → country mapping लिया जा सकता है
    • यह संरचना लाखों rows के पैमाने की होती है, और IPv6 में प्रत्येक item बड़ा होता है
  • range lookups को पहले से compute करके दोनों stores में लिखा गया
  • pathological case में हर inbound HTTP request को request IP की तुलना इन ranges से करनी पड़ती थी
    1. उपयोगकर्ता-परिभाषित allow ranges
    2. उपयोगकर्ता-परिभाषित block ranges
    3. GeoIP ranges
    4. IP reputation ranges
  • यह query type इतनी महत्वपूर्ण थी कि अन्य queries या features को port किए बिना केवल इसी एक प्रकार का परीक्षण किया गया

टेस्ट विधि और परिणाम

  • टेस्ट लोकल MacBook Air M2 पर Homebrew से install किए गए Redis और लोकल SQLite DB के साथ किया गया
  • प्रोटोकॉल इस प्रकार था
    1. मौजूदा range dataset के 12 लाख items का उपयोग
    2. कई IP sets को एक ही क्रम में SQLite और Redis पर चलाया गया
    3. हर multiple पर test 5 बार चलाकर average लिया गया
  • Wafris के इस विशेष use case में SQLite ने लोकल Redis की तुलना में लगभग 3 गुना तेज performance दिखाई
  • यह परिणाम network latency को शामिल करने से पहले का था
  • टेस्ट जानबूझकर सरल configuration और real-world उपयोग की सीमाओं को दर्शाता था, इसलिए इसे सामान्य database comparison के रूप में व्यापक रूप से लागू करना कठिन है

चार्ट में न दिखने वाले operational अंतर

  • भले ही benchmark में SQLite की performance Redis से काफी खराब निकलती, फिर भी उसी datacenter या region में मौजूद Redis तक network latency के कारण वास्तविक environment में SQLite तेज हो सकती थी
  • Redis server को cluster या sharding के साथ मजबूत बनाने पर भी network bandwidth, connection count, और inter-region latency जैसी सीमाएँ बनी रहती हैं
  • SQLite हर compute instance पर local रूप से मौजूद होने के कारण, Wafris के इस use case में horizontal scaling cost लगभग समाप्त हो जाती है
  • onboarding भी SQLite के साथ अधिक सरल था
    • उपयोगकर्ता चाहे यह न भी जानें कि SQLite उपयोग हो रही है, वे gem को web app में जोड़कर चला सकते हैं
  • Redis में भी अतिरिक्त optimization की काफी गुंजाइश थी, लेकिन Wafris को उपयोगकर्ताओं को cache eviction policy जैसी बुनियादी settings बदलने के लिए भी लगातार राज़ी कर पाना मुश्किल था

SQLite पर स्विच के बाद आवश्यक structural बदलाव

  • Redis-आधारित v1 का update flow सरल था
    1. उपयोगकर्ता Wafris Hub में rules अपडेट करता है
    2. Wafris Hub उपयोगकर्ता के Redis store में rules अपडेट कर देता है
  • SQLite में Wafris Hub web server पर SQLite database को सीधे “push” नहीं कर सकता था
  • कुछ SQLite as a service providers ऐसा तरीका संभव बनाते हैं, लेकिन cost, performance, और security कारणों से यह Wafris के लिए उपयुक्त नहीं था
    • हर उपयोगकर्ता को इसे अलग से deploy करना पड़ता
    • ports खोलने पड़ते
    • inbound connections की अनुमति देनी पड़ती
  • SQLite-आधारित v2 का update flow इस प्रकार है
    1. उपयोगकर्ता Wafris Hub में rules अपडेट करता है
    2. client समय या request count के आधार पर नियमित अंतराल में updated rules जांचता है
    3. अगर rules बदले हों, तो client पूरी तरह नया SQLite database डाउनलोड करता है
  • इस संरचना ने user installation और configuration की ज़िम्मेदारी को काफी कम किया, और v2 client की successful installations लगभग 3 गुना बढ़ीं

distributed deployments में SQLite की संरचना

  • AWS, Heroku, Fly जैसे cloud environments में जब Rails apps auto-scale होती हैं, तो compute instances बढ़ते हैं लेकिन database उसी अनुपात में नहीं बढ़ता
  • जब requests 100 req/s से 10,000 req/s तक बढ़ती हैं, तो dyno, machine, EC2 instances तो बढ़ जाते हैं, लेकिन database bottleneck वही बना रह सकता है
  • Wafris के अवलोकन में Rails apps के failures का मुख्य कारण वास्तविक DDoS से अधिक credential stuffing या खराब bot traffic था
    • ऐसा traffic auto-scaling को trigger करता है
    • उसके बाद database connections खत्म हो सकते हैं और app गिर सकती है
  • अगर SQLite DB को हर compute instance पर sync कर दिया जाए, तो नए instance की सभी calls local रखी जा सकती हैं

write path को client से हटाना

  • पहले के tests में write operations को शामिल नहीं किया गया था, और Wafris यह भी नहीं मानता कि SQLite हर भूमिका के लिए उपयुक्त है
  • Wafris SQLite का उपयोग read-heavy role के लिए करता है, और write path को अलग से redesign करता है
  • v2 का reporting path इस तरह बदला गया
    • Wafris Hub से async connection के माध्यम से report करना
    • reporting data को batch में भेजना
    • client से database writes को पूरी तरह हटाना
  • यह संरचना अधिकांश अन्य services के लिए उपयुक्त न भी हो, लेकिन Wafris users की आसान deployment और तेज client की अपेक्षाओं के लिए उपयुक्त है
  • SQLite-आधारित v2 architecture ने पहले ही कई sites को attacks के दौरान online बनाए रखने में मदद की है, और Wafris के support workload तथा user inconvenience दोनों को कम किया है

1 टिप्पणियां

 
GN⁺ 2024-09-26
Hacker News की रायें
  • यह मॉडल सचमुच दिलचस्प है जिसमें हर application server के पास SQLite database file की एक copy होती है, और उसे तय अंतराल पर पूरी तरह replace कर दिया जाता है
    यहां इसका इस्तेमाल web application firewall rules के लिए किया जा रहा है, लेकिन यह feature flag configuration के लिए भी अच्छा फिट लगता है। Feature flags को प्रति request दर्जनों बार check किया जा सकता है, और अक्सर “user A group में है और IP B country में है” जैसी queries की जरूरत होती है, जिन्हें local SQLite अच्छी तरह संभाल सकता है। Feature flags के updates कुछ seconds, या उससे ज्यादा देर से propagate हों तो आम तौर पर स्वीकार्य होता है

    • यह तरीका CDN में भी इस्तेमाल होने वाला model है। सभी customers के certificates, HTTP routing rules आदि वाली global config file को एक single-file B-tree structure के रूप में update किया जाता है, फिर उस “bundle” को अक्सर सभी edge locations पर deploy किया जाता है
      हालांकि इस use case के लिए SQLite इस्तेमाल होते नहीं देखा है; आम तौर पर LMDB या Kyoto Cabinet जैसी DBM-style databases इस्तेमाल होती हैं
    • Airbnb में भी इसी model से translations, feature flags, configuration, search indexes आदि deploy किए गए थे। SQLite के बजाय Spotify द्वारा बनाया गया key-value file format Sparkey इस्तेमाल किया गया
      शुरुआत में हर server का Cron job संबंधित service का data fetch करता था, और Kubernetes पर जाने के बाद DaemonSet और host tagging/taints जैसी व्यवस्था से अलग-अलग data हर host पर download कराया गया, फिर यह ensure किया गया कि उस data का इस्तेमाल करने वाली service केवल उसी host पर चले। Ruby में इसे “hammerspace” कहा गया था: https://github.com/airbnb/hammerspace
    • Feature flag values को प्रति request कई बार lookup करने की समस्या, flag value को सिर्फ एक बार fetch करके request object में store करने के तरीके से हल की जाती है
      इससे एक request में महंगे lookup की cost दो या ज्यादा बार नहीं देनी पड़ती, और request processing के दौरान flag update होने से value inconsistent हो जाने का risk भी नहीं रहता
    • लंबे migration के दौरान feature flags के साथ काफी काम करने के बाद, मुझे यकीन हो गया है कि सचमुच उपयोगी primitive “SQLite जैसा, लेकिन हर change पर global version counter बढ़ता है, और version numbers के बीच के बदलाव ही fetch या apply किए जा सकते हैं” वाला तरीका है
      इसके ऊपर gossip network या explicit tree-shaped cache/distribution network जैसे कई तरह के distribution mechanisms लगाए जा सकते हैं। जिसके पास भी नया version हो, वह update दे सकता है
      मौजूदा DB version को application artifact में include करें, लेकिन app चलने के बाद उसे efficiently update किया जा सके। Feature flags या live configuration में code-level defaults के बजाय हाल की state पर fallback किया जा सकता है
      कोई भी client global counter को ETAG की तरह भेजकर सिर्फ changes पा सकता है, और थोड़े समय के network disconnect के बाद reconnect करना भी simple और सस्ता हो जाता है
      अगर कुछ मिनटों की history रखी जा सके, तो W3C Baggage header जैसी scheme से microservice calls में counter जोड़ा जा सकता है, ताकि कई services उसी version number के आधार पर flags/config/data evaluate कर सकें। Time-based evaluation feature न भी हो, generation number को logs में छोड़ना postmortem debugging में बहुत मदद करता है
    • Deployment के लिए SQLite शानदार है। यह idea कुछ हद तक Datasette project से प्रेरित लगता है, और बाद में reporting और data exploration के क्षेत्र में अब भी कुछ किया जा सकता है
  • Network latency के अलावा, Redis में मैंने देखा है कि read/write latency lookup किए जाने वाले keys की संख्या के लगभग linearly proportional होती है, और लेख के charts में भी कुछ वैसा ही दिखता है
    Monolithic app में Postgres और Redis को उनके use cases के हिसाब से साथ इस्तेमाल करने पर यह काफी अच्छा चलता था, लेकिन new features को shared Redis cluster में डाल देना बहुत आसान था। Redis single-threaded है, इसलिए 100k+ keys की bulk lookup करने वाला एक असावधान feature दूसरे features को भी धीमा कर सकता है
    इसलिए मैंने guideline सुझाई थी कि Redis तब खास तौर पर अच्छा है जब एक बार में एक key या छोटी fixed number of keys को read/write करना हो, जैसे popular endpoints के locks या rate limiting, जहां कई random features Redis इस्तेमाल करते हैं
    इस मामले में Redis simple single-key (IP address) lookup में चमकेगा, लेकिन range query expressions जैसी ज्यादा complex reads के लिए फिट नहीं लगता। Local Redis की तुलना में SQLite इतनी अच्छी तरह क्यों काम करता है, इसकी गहराई मुझे नहीं पता, इसलिए यह unexpected और दिलचस्प लगा

    • Redis अक्सर trap बन जाता है क्योंकि लोग उसके pros और cons को गलत समझ लेते हैं
      Redis को richer built-in operations वाले cache के रूप में देखना सबसे अच्छा है। ठीक से इस्तेमाल करने पर यह तेज और reliable है
      लेकिन जब आप ऐसी चीजें Redis में डालना शुरू करते हैं जो primary RDBMS में ठीक से fit नहीं बैठतीं, तो जल्द ही job queues, कई तरह के locks आदि बन जाते हैं, और आखिरकार performance cliff पार हो जाना या किसी और वजह से collapse होना बस समय की बात रह जाती है। Recovery process आमतौर पर messy हो जाती है, और अंत अक्सर कुछ data loss स्वीकार करने पर होता है
      यह pattern धीरे-धीरे और आसानी से हो जाता है, इसलिए बचने के लिए discipline चाहिए। SQLite performance सिर्फ network overhead से बचने के कारण नहीं है, बल्कि इसलिए भी है कि कई लोग serialization/deserialization cost को कम आंकते हैं। Redis protocol काफी minimal होने पर भी cost जमा होती जाती है, और SQLite में कई operations process-internal memory copies तक सीमित हो जाते हैं
  • कुछ हफ्ते पहले Neon के internal hackathon में मैंने एक छोटा Node.js server बनाया था, जो Redis के wire protocol RESP को Postgres queries में बदलता है
    यह बेहद मजेदार hacking project था: https://github.com/btholt/redis-to-postgres

  • यह server-side SQLite के लिए काफी fit एक खास use case लगता है, जहां database read-only है इसलिए replication की जरूरत नहीं है
    एक और alternative static file को memory में load करना हो सकता है, लेकिन इस case में data memory में लगातार रखने के लिए काफी बड़ा है, इसलिए SQLite अच्छा alternative लगता है

    • उम्मीद है लेख से यह बात पहुंची होगी, लेकिन यह किसी खास use case के लिए शानदार solution है, Redis या Postgres का 1:1 replacement नहीं
    • Static file को memory में load करने के तरीके से शुरू करके, common use cases में काम कराने के लिए जरूरी features धीरे-धीरे पर्याप्त मात्रा में जोड़ते जाएं, तो अंत में SQLite जैसी relational database बन जाती है
      फर्क यह है कि SQLite बेहद robust है, performance अच्छी है, और असाधारण रूप से अच्छी तरह tested है
  • RailsWorld 2023 में Redis को लेकर “खून की गंध सूंघ लेने” जैसा माहौल था—यह हिस्सा दिलचस्प लगा
    अपने करियर में मैंने production Rails app सिर्फ एक ही किया है, और उस app में Redis इस्तेमाल हुआ था, इसलिए आजकल का trend ठीक से नहीं पता। जानना चाहता हूं कि ecosystem business perspective से Redis से मुंह मोड़ रहा है, क्या यह license change की वजह से है, या फिर यह YAGNI के करीब वाली स्थिति है
    उस समय इसे Rescue के साथ indexing, transcoding जैसी asynchronous jobs schedule करने के लिए मुख्य रूप से इस्तेमाल किया था, और तब यह काफी अच्छा tool लगा था

    • थोड़ा-बहुत YAGNI के करीब है। Rails community में Redis बहुत इस्तेमाल होने की सबसे बड़ी वजह asynchronous jobs थीं, और ज्यादातर लोगों ने जो tool उठाया वह Sidekiq था
      2024 Rails community survey में भी Redis अब भी apps में सबसे ज्यादा इस्तेमाल होने वाला data store निकला
      लेकिन कई apps Redis इस्तेमाल तो करते हैं, पर असल में इसे Sidekiq जैसे use cases तक ही सीमित रखते हैं, real-time leaderboard या vector DB capabilities जैसी चीजों में नहीं लगाते, इसलिए actual usage pattern थोड़ा धुंधला है
    • मुझे लगता है यह पूरी तरह simplicity का मामला है
      आजकल reasonable traffic वाले Rails setup में अक्सर frontend server, SQL DB, key-value store (Redis या Memcached), और उसी key-value store की ओर point करने वाला cache store रखा जाता है। लेकिन API quota या rate limiting बनाए रखने वाले सामान्य key-value usage और cache usage patterns काफी अलग होते हैं, इसलिए झंझट बढ़ता है
      disk performance अब काफी तेज हो गई है और SQL performance भी बेहतर हुई है, इसलिए key-value store हटाकर traditional key-value use cases को SQL DB में और cache backend को disk पर अलग करने की दिशा में movement है। नए NVMe disks RAM के लगभग करीब तक तेज हैं और काफी सस्ते भी, इसलिए ज्यादा cache किया जा सकता है
    • Redis पहले expiration जैसी built-in capabilities वाली बहुत smart shared memory की तरह उपयोगी था। clustering करने पर इसे कई machines के बीच भी share किया जा सकता था
      लेकिन Kubernetes और Redis-as-a-service के दौर में वह “shared memory” किसी दूसरे rack की दूसरी VM पर होती है। उस point तक पहुंचने पर आप resource efficiency तो वैसे ही छोड़ चुके होते हैं, इसलिए मुझे लगता है बस S3 से files read/write करना बेहतर है
    • आजकल SSD तेज हैं, इसलिए ज्यादा लोगों को RocksDB इस्तेमाल करना चाहिए। SSD पर कहीं बड़ा key-value store चलाया जा सकता है
  • “Redis traditional RDBMS की तुलना में तेज है, लेकिन यह फिर भी connections, memory, processes आदि manage करने वाला database है, इसलिए stack को ज्यादा fragile बनाता है”—इस हिस्से पर मेरा मानना है कि toy-level से आगे के transactions handle करना शुरू करते ही, relational हो या non-relational, सभी databases को लगभग समान level की management और maintenance चाहिए होती है
    “तेज” वाला हिस्सा भी थोड़ा रोचक है। अगर joins की चिंता न करें, तो row inserts और lookups भी काफी तेज होते हैं

    • server environment में SQLite असाधारण रूप से तेज इसलिए है क्योंकि query करने या data लाने के लिए network call की जरूरत नहीं होती
      local disk से सीधे पढ़ने वाली बात समझ लें, तो joins या transactions की चर्चा का बहुत मतलब नहीं रह जाता। वह पहले से ही कई orders of magnitude तेज path है
    • SQLite में VACUUM नाम का एक operation है, जो कुछ-कुछ garbage collection जैसा दिखता है
      VACUUM कब चलाना चाहिए, इसके docs जब भी पढ़ता हूं तो confuse हो जाता हूं। आखिरी बार जब SQLite-based application deploy किया था, तो बस एक counter रखा था और बहुत सारी write operations के बाद VACUUM चलाने वाला तरीका अपनाया था
    • “सभी databases लगभग समान level की management और maintenance मांगते हैं” इस बात से सहमत नहीं हूं
      जिस database के लिए independent server process चलाना पड़े, वहां management और maintenance effort में फर्क आना ही है। extreme example में, SQLite और Oracle इस्तेमाल करने में सचमुच कोई फर्क नहीं है—यह मानना मुश्किल है
  • SQLite के ऊपर Redis को Go में फिर से implement करने वाले Redka में आपकी रुचि हो सकती है: https://github.com/nalgeon/redka

    • switch करने पर विचार करने लायक interesting था, लेकिन “benchmarks के मुताबिक Redka, Redis से कई गुना धीमा है” वाला हिस्सा देखकर हिचक हुई
      project शानदार है, यह सही है, पर इस level पर switch करने की incentive लगभग नहीं बचती
  • Redis का usage model सही समझ रहा हूं या नहीं, यह confirm करना चाहता हूं
    v1 में क्या WAF और Redis एक ही server पर थे, और जब client management panel में नया rule set करता था, तो वह rule management panel वाले server के Redis में जाता था, और Redis के internal synchronization mechanism की वजह से दुनिया भर के WAF के बगल में मौजूद local Redis instances तक rules update हो जाते थे? फिर कोई नया request किसी WAF पर आता, तो WAF updated Redis rules के आधार पर request/IP validate करता था—क्या flow यही था, यह confirm करना चाहता हूं
    v2 में क्या Redis cluster हटाकर सभी WAF servers के पास SQLite DB रखा गया, management panel से हर WAF+SQLite server तक नए rules फैलाने के लिए अलग synchronization mechanism बनाया गया, और नया request आने पर WAF updated SQLite rules से जल्दी validate करता है—क्या structure यही है?

  • सबसे बढ़िया वाक्य है: “SQLite client/server databases से compete नहीं करता। SQLite fopen() से compete करता है