62 पॉइंट द्वारा xguru 2023-11-09 | 9 टिप्पणियां | WhatsApp पर शेयर करें

Elixir as a fanout system

  • Discord में जब भी कोई घटना होती है, जैसे कोई message भेजा जाता है या कोई voice channel में शामिल होता है, तो उसी server (जिसे "guild" भी कहा जाता है) में online मौजूद सभी users के clients पर UI update करना पड़ता है
  • उस server में होने वाली हर चीज़ के केंद्रीय routing point के रूप में प्रति guild एक Elixir process का उपयोग किया जाता है, और जुड़े हुए प्रत्येक user client के लिए एक अलग process ("session") इस्तेमाल होता है
  • guild process उस guild के members के user sessions को track करता है और उन sessions तक operations को fanout करने का काम करता है
  • session को update मिलने पर वह उसे websocket connection के ज़रिए client तक पहुंचाता है
  • कुछ operations server के सभी लोगों पर लागू होते हैं, जबकि कुछ में permissions check करनी पड़ती हैं; इसलिए उस server के roles और channels की जानकारी के साथ-साथ user की role information भी जानना ज़रूरी होता है
  • guild की activity उस server के users की संख्या के अनुपात में होती है, और एक message को fanout करने के लिए आवश्यक काम उस server के online users की संख्या के अनुपात में बढ़ता है
    • यानी Discord server को संभालने के लिए आवश्यक workload server के scale के अनुसार चौथी घात की तरह बढ़ता है
    • अगर किसी server पर 1,000 लोग online हों और वे सभी एक-एक बार "मुझे jelly पसंद है" कहें, तो 10 लाख notifications process करनी होंगी
    • 10,000 लोग हों तो 10 करोड़ notifications बनती हैं, और 1 लाख लोग हों तो 1,000 करोड़ notifications पहुंचानी पड़ती हैं
  • कुल throughput की समस्या के अलावा, server बड़ा होने पर कुछ operations की speed भी धीमी हो सकती है
  • अगर यह महसूस कराना है कि server responsive है — जैसे message भेजते ही दूसरे लोग उसे देख सकें, या कोई voice channel join करे तो तुरंत जुड़ सके — तो लगभग हर operation को तेज़ी से process करना होगा
  • अगर किसी महंगे operation को पूरा करने में कुछ सेकंड लग जाएं, तो user experience खराब हो जाता है
  • इन समस्याओं के बावजूद, 1 करोड़ से अधिक members और उनमें से 10 लाख हमेशा online रहने वाले Midjourney server को support करना कैसे संभव हुआ?
    • सबसे पहले system की performance को समझना महत्वपूर्ण था
    • data इकट्ठा करने के बाद throughput और responsiveness दोनों सुधारने के मौके खोजे गए

सिस्टम की performance समझना

  • Wall time analysis:
    • Process.info(pid, :current_stacktrace) का उपयोग करके stack tracing
    • event processing loop को measure करके, प्रत्येक प्रकार के message के receive count और उन्हें process करने में लगे max/min/average/total time को record किया गया
    • जो tasks कुल समय का 1% से कम लेते थे, उन्हें तब तक नज़रअंदाज़ किया गया जब तक वे अत्यधिक bursty न हों
    • सस्ते operations को हटाकर सबसे महंगे operations पर फोकस किया गया
  • Process Heap Memory Analysis
    • memory का उपयोग कैसे हो रहा है, यह समझना भी महत्वपूर्ण था
    • हर item को अलग-अलग देखने के बजाय, बड़े map और list (struct नहीं) को sample करके अनुमानित memory usage बनाने के लिए एक helper library लिखी गई
    • इस library ने GC performance समझने में मदद की, और यह भी पता लगाने में कि कौन से fields optimization के लिए महत्वपूर्ण हैं और कौन से अंततः अप्रासंगिक
  • guild process समय कहां खर्च कर रहा है यह समझने के बाद, ऐसी रणनीति बनाई जा सकी जिससे guild process 100% busy न रहे
    • कुछ मामलों में inefficient implementation को अधिक efficient तरीके से rewrite करना ही काफी था
    • लेकिन यह तरीका एक सीमा तक ही मददगार था; इससे अधिक बुनियादी बदलाव की आवश्यकता थी

Passive sessions - अनावश्यक काम से बचना

  • throughput bottleneck हल करने के सबसे अच्छे तरीकों में से एक है काम की मात्रा कम करना
  • इसके लिए एक तरीका था client application की requirements पर विचार करना
  • मूल topology में सभी users उन सभी actions को receive करते थे जो वे अपने सभी जुड़े guilds में देख सकते थे
  • लेकिन कुछ users कई guilds में होते हैं, और हो सकता है वे कुछ guilds में क्या चल रहा है यह देखने के लिए क्लिक भी न करें
  • अगर user के क्लिक करने तक सब कुछ न भेजा जाए तो? तब हर message पर अलग-अलग permissions check नहीं करनी पड़ेंगी, और client को भेजा जाने वाला data भी काफी कम हो जाएगा
  • इसे उन्होंने 'Passive' connection कहा, और इसे उन 'Active' connections से अलग list में रखा जो पूरा data receive करते थे
  • परिणामस्वरूप, बड़े servers में user-guild connections का लगभग 90% passive connection निकला, जिससे fanout cost में 90% की कमी आई
  • इससे कुछ राहत मिली, लेकिन community के बढ़ते रहने के कारण यह स्वाभाविक रूप से पर्याप्त नहीं था
    (अगर workload 10 गुना घटे, तो maximum community scale पर लगभग 3 गुना लाभ मिल सकता है)

Relays - कई machines में fanout को बांटना

  • single-core throughput limit को scale करने की एक standard technique है काम को कई threads में बांटना (या Elixir की terminology में कई processes में)
  • इसी विचार के आधार पर guild और user sessions के बीच 'relay' नाम का एक system बनाया गया
  • sessions को संभालने का पूरा काम एक process में करने के बजाय, उसे कई relays में बांट दिया गया ताकि एक single guild बड़े community को serve करने के लिए अधिक resources का उपयोग कर सके
  • कुछ operations अब भी मुख्य guild process में ही करने पड़ते थे, लेकिन इससे सैकड़ों हजार members वाली communities को संभालना संभव हुआ
  • इसे लागू करने के लिए यह पहचानना पड़ा कि कौन से महत्वपूर्ण operations relay में होने चाहिए, कौन से guild में, और कौन से दोनों systems में किए जा सकते हैं
  • ज़रूरतें समझने के बाद, systems के बीच share किए जा सकने वाले logic को अलग निकालने के लिए refactoring शुरू की गई
    • उदाहरण के लिए, fanout कैसे करना है उससे जुड़ा अधिकांश logic एक ऐसी library में refactor किया गया जिसे guild और relay दोनों उपयोग कर सकें
    • कुछ logic जिसे इस तरह share नहीं किया जा सकता था, उसके लिए अलग solution चाहिए था; voice state management को मूल रूप से इस तरह लागू किया गया कि relay न्यूनतम बदलाव के साथ सभी messages को guild तक proxy करे
  • relay को पहली बार launch करते समय एक रोचक design decision यह था कि प्रत्येक relay state में पूरी member list शामिल की जाए
    • simplicity के लिहाज से यह अच्छा निर्णय था, क्योंकि ज़रूरी member info तुरंत उपलब्ध रहती थी
    • लेकिन Midjourney जैसे scale, जहां members की संख्या लाखों में है, वहां यह design धीरे-धीरे कम सार्थक होने लगा
  • न सिर्फ़ करोड़ों members की जानकारी की दर्जनों copies RAM में रखी जा रही थीं, बल्कि नया relay बनाने के लिए पूरी member info serialize करके नए relay को भेजनी पड़ती थी, जिससे guild दर्जनों सेकंड तक stall हो जाता था
  • इस समस्या को हल करने के लिए logic जोड़ा गया जिससे relay वास्तव में काम करने के लिए आवश्यक members की पहचान कर सके; और यह पूरी member list का बहुत छोटा हिस्सा निकला

Server responsiveness बनाए रखना

  • throughput limit के भीतर बने रहने के अलावा, server की responsiveness भी बनाए रखनी थी
  • यहां भी timing data को देखना उपयोगी साबित हुआ
  • कुल duration की तुलना में प्रति call अधिक समय लेने वाले operations पर फोकस करना अधिक प्रभावी था
  • Worker processes + ETS
    • responsiveness खराब होने के सबसे बड़े कारणों में से एक वे operations थे जिन्हें guild पर चलाना पड़ता था और जिनमें सभी members पर iterate करना पड़ता था
    • ऐसे मामले बहुत दुर्लभ थे, लेकिन होते थे। उदाहरण के लिए, अगर कोई @everyone ping करे, तो यह पता होना चाहिए कि server में कौन-कौन उस message को देख सकता है
    • लेकिन ऐसी checking में कई सेकंड लग सकते थे। इसे कैसे संभाला जाए?
    • आदर्श स्थिति यह थी कि guild दूसरे operations process करता रहे और यह logic साथ-साथ चले, लेकिन Elixir processes memory अच्छी तरह share नहीं करते। इसलिए दूसरा solution चाहिए था
    • Erlang/Elixir में processes के बीच share की जा सकने वाली memory में data रखने के लिए इस्तेमाल होने वाले tools में से एक है ETS
    • यह एक in-memory database है जो कई Elixir processes को सुरक्षित रूप से access करने की सुविधा देता है
    • process heap के data की तुलना में इसकी access efficiency थोड़ी कम है, लेकिन यह फिर भी बहुत तेज़ है। साथ ही process heap का size घटाने से garbage collection latency कम करने का लाभ भी मिलता है
    • member list को रखने के लिए एक hybrid structure बनाने का निर्णय लिया गया:
      • member list को ETS में रखा जाए ताकि दूसरे processes भी उसे पढ़ सकें, लेकिन recent changes (insert, update, delete) को process heap में भी रखा जाए
      • क्योंकि अधिकांश members लगातार update नहीं होते, इसलिए recent changes का set पूरी member list का बहुत छोटा हिस्सा होता है
    • अब ETS में रखे members का उपयोग करके worker process बनाया जा सकता था और महंगे operations के समय उसे काम करने के लिए ETS table identifier दिया जा सकता था
    • worker process महंगे हिस्से को संभाल सकता था जबकि guild बाकी operations जारी रखे। इसे करने का एक सरल तरीका भी दिया गया है (मूल लेख में code snippet)
    • इसका एक उपयोग तब हुआ जब guild process को एक machine से दूसरी machine पर handoff करना पड़ता था (आमतौर पर maintenance या deployment के लिए)
    • इस प्रक्रिया में नई machine पर guild को संभालने के लिए नया process बनाना, फिर पुराने guild process की state को नए process में copy करना, जुड़े हुए सभी sessions को नए guild process से reconnect करना, और इस दौरान जमा हुए backlog को process करना शामिल था
    • worker process के उपयोग से अधिकांश members (जो कई GB data हो सकते थे) को तब भी transfer किया जा सकता था जब मौजूदा guild process काम करता रहे; इससे हर handoff पर होने वाली कई मिनट की delay कम हो गई
  • Manifold offload
    • responsiveness सुधारने और throughput limits से आगे बढ़ने का एक और विचार था manifold को इस तरह extend करना कि guild process में fanout करने के बजाय, अलग 'sender' processes recipient nodes पर fanout करें
    • इससे न सिर्फ़ guild process का workload कम हो सकता था, बल्कि अगर guild और relay के बीच network connections में से कोई एक अस्थायी रूप से backup हो जाए, तब भी BEAM backpressure से सुरक्षा मिल सकती थी (BEAM वह virtual machine है जिस पर Elixir code चलता है)
    • सिद्धांत रूप से यह आसान समाधान लग रहा था, लेकिन दुर्भाग्य से इस feature (जिसे manifold offload कहा गया) को इस्तेमाल करने पर performance वास्तव में काफी गिर गई
    • ऐसा कैसे हो सकता है? सिद्धांततः workload घट रहा था, फिर process और busy क्यों हो गया?
    • गहराई से देखने पर पता चला कि अतिरिक्त काम का अधिकांश हिस्सा garbage collection से जुड़ा हुआ था
    • इसी बिंदु पर erlang.trace एक lifesaver की तरह सामने आया
    • इस function से हर बार guild process के garbage collection करने पर data इकट्ठा किया जा सकता था, जिससे यह समझ मिली कि GC कितनी बार हो रहा है और उसे trigger क्या कर रहा है
    • इस tracing information के आधार पर BEAM के garbage collection code को देखने पर पता चला कि manifold offload enabled होने पर major (full) GC की trigger condition virtual binary heap थी
    • virtual binary heap एक ऐसी सुविधा है जो process heap के अंदर न रखे गए strings द्वारा उपयोग की जा रही memory को release करने के लिए बनाई गई है, भले ही process को अन्यथा garbage collection की आवश्यकता न हो
    • दुर्भाग्य से, उनके usage pattern में इसका मतलब था कि कुछ सौ KB memory reclaim करने के लिए बार-बार garbage collection trigger हो रहा था, जबकि इसकी कीमत कई GB heap copy करना थी — जो स्पष्ट रूप से फायदे का सौदा नहीं था
    • अच्छी बात यह थी कि BEAM में process flag min_bin_vheap_size का उपयोग करके इस behavior को tune किया जा सकता है
    • इस value को कुछ MB तक बढ़ाने पर यह pathological garbage collection behavior गायब हो गया, और manifold offload चालू करने पर performance में बड़ा सुधार दिखाई दिया

9 टिप्पणियां

 
roxie 2023-11-18

Elixir ज़िंदाबाद

 
arfwene 2023-11-10

Passive session तकनीकी रूप से बहुत बड़ी चीज़ नहीं है, लेकिन यह एक अच्छा आइडिया लगता है.
इससे लोड काफ़ी हद तक कम किया जा सकता है.

मुझे लगता है कि सिर्फ Discord ही नहीं, दूसरी जगहों पर भी ऐसी सुविधा लागू की गई होगी; अलग-अलग services में इसके क्या फ़र्क हैं, यह जानने की जिज्ञासा है.

 
mhj5730 2023-11-10

बहुत शानदार है, कमाल

 
abhidhamma 2023-11-09

आजकल काफ़ी मशहूर nextjs की streaming SSR का अंतिम ठिकाना भी Elixir का Phoenix framework ही निकला। कई मायनों में Elixir आधुनिक programming languages की अग्रिम पंक्ति में दिखता है।

 
papillon 2023-11-09

Elixir ज़िंदाबाद

 
n1ghtc4t 2023-11-09

कुछ साल पहले Discord के tech blog को देखकर मैंने real-time service में Elixir अपनाया था, और development speed व reliability के मामले में मुझसे लेकर जिम्मेदार executive तक सभी बेहद संतुष्ट थे, इसलिए सेवा को सफलतापूर्वक लॉन्च करने की बहुत अच्छी यादें हैं।

 
kotlinc 2023-11-09

उम्मीद है कि Elixir और ज़्यादा लोकप्रिय हो जाएगा

 
[यह टिप्पणी छिपाई गई है.]
 
damtet 2023-11-10

आजकल ऐसा नहीं लगता कि Naver, Kakao जैसी बड़ी कंपनियाँ उस स्तर पर हैं; बल्कि मध्यम और छोटे startup ही ज़्यादातर Spring के एकाधिकार जैसे लगते हैं। उन startup के managers अधिकतर Spring विशेषज्ञ होते हैं, इसलिए शायद इसे टाला भी नहीं जा सकता।

हर inefficiency को पैसे और scale से हल किया जा सकता है। वैसे भी कंपनी को आखिरकार ज़्यादा समझ नहीं होती।