LMAX के अवरोधक पैटर्न कैसे काम करता है?


205

मैं विघटनकारी पैटर्न को समझने की कोशिश कर रहा हूं । मैंने इन्फोक्यू वीडियो देखा है और उनके पेपर पढ़ने की कोशिश की है। मैं समझता हूं कि इसमें एक रिंग बफ़र शामिल है, जो कि कैश लोकेलिटी का लाभ उठाने, नई मेमोरी के आवंटन को समाप्त करने के लिए एक बहुत बड़े सरणी के रूप में आरंभीकृत है।

ऐसा लगता है कि एक या एक से अधिक परमाणु पूर्णांक हैं जो पदों का ट्रैक रखते हैं। प्रत्येक 'ईवेंट' को एक अद्वितीय आईडी मिलती है और यह रिंग की स्थिति को रिंग के आकार, आदि के संबंध में अपने मापांक को खोजने के द्वारा पाया जाता है।

दुर्भाग्य से, मेरे पास कोई सहज ज्ञान नहीं है कि यह कैसे काम करता है। मैंने कई व्यापारिक अनुप्रयोग किए हैं और अभिनेता मॉडल का अध्ययन किया , SEDA को देखा, आदि।

अपनी प्रस्तुति में उन्होंने उल्लेख किया कि यह पैटर्न मूल रूप से राउटर्स कैसे काम करता है; हालाँकि मुझे इस बात का कोई अच्छा विवरण नहीं मिला है कि राउटर्स या तो कैसे काम करते हैं।

क्या बेहतर स्पष्टीकरण के लिए कुछ अच्छे संकेत हैं?

जवाबों:


210

Google कोड प्रोजेक्ट रिंग बफर के कार्यान्वयन पर एक तकनीकी पेपर का संदर्भ देता है , हालांकि यह थोड़ा सूखा, अकादमिक और कठिन है कि कोई यह सीखना चाहता है कि यह कैसे काम करता है। हालाँकि कुछ ब्लॉग पोस्ट ऐसे हैं जो इंटर्ल्स को अधिक पठनीय तरीके से समझाने लगे हैं। रिंग बफर की एक व्याख्या है जो विघटनकारी पैटर्न का मूल है, उपभोक्ता बाधाओं (विघ्नकर्ता से पढ़ने से संबंधित भाग) का विवरण और उपलब्ध कई उत्पादकों को संभालने पर कुछ जानकारी

डिस्प्रेटर का सबसे सरल वर्णन है: यह सबसे कुशल तरीके से थ्रेड्स के बीच संदेश भेजने का एक तरीका है। इसे एक कतार के विकल्प के रूप में इस्तेमाल किया जा सकता है, लेकिन यह SEDA और अभिनेताओं के साथ कई सुविधाएँ भी साझा करता है।

कतारों की तुलना में:

डिस्क्राइटर एक संदेश को दूसरे थ्रेड पर पारित करने की क्षमता प्रदान करता है, यदि आवश्यक हो तो इसे ब्लॉक कर सकता है (एक ब्लॉकिंग क्यू के समान)। हालांकि, 3 अलग-अलग अंतर हैं।

  1. डिस्क्राइबर का उपयोगकर्ता यह परिभाषित करता है कि एंट्री क्लास को बढ़ाकर और प्रचार करने के लिए एक कारखाना प्रदान करके संदेशों को कैसे संग्रहीत किया जाता है। यह या तो मेमोरी के पुन: उपयोग (कॉपी करने) या एंट्री के लिए किसी अन्य ऑब्जेक्ट का संदर्भ शामिल कर सकता है।
  2. डिस्क्राइबर में संदेश डालना 2-चरण की प्रक्रिया है, पहले रिंग बफर में एक स्लॉट का दावा किया जाता है, जो उपयोगकर्ता को एंट्री प्रदान करता है जिसे उपयुक्त डेटा से भरा जा सकता है। फिर प्रविष्टि को प्रतिबद्ध होना चाहिए, ऊपर उल्लिखित स्मृति के लचीले उपयोग के लिए अनुमति देने के लिए यह 2-चरण दृष्टिकोण आवश्यक है। यह वह प्रतिबद्धता है जो संदेश को उपभोक्ता के धागों से देखती है।
  3. यह उपभोक्ता की जिम्मेदारी है कि वह उन संदेशों पर नज़र रखे जो रिंग बफर से खाए गए हैं। इस जिम्मेदारी को रिंग बफर से दूर ले जाने से लेखन विवाद की मात्रा को कम करने में मदद मिली क्योंकि प्रत्येक थ्रेड अपने काउंटर को बनाए रखता है।

अभिनेताओं की तुलना में

अभिनेता मॉडल अधिकांश अन्य प्रोग्रामिंग मॉडल की तुलना में डिस्ट्रक्टोर के करीब है, खासकर यदि आप प्रदान किए गए बैचऑनसमर / बैचहैंडलर वर्गों का उपयोग करते हैं। ये वर्ग उपभोग किए गए अनुक्रम संख्या को बनाए रखने की सभी जटिलताओं को छिपाते हैं और महत्वपूर्ण घटनाओं के होने पर सरल कॉलबैक का एक सेट प्रदान करते हैं। हालाँकि, सूक्ष्म अंतर के एक जोड़े हैं।

  1. विघटनकर्ता 1 थ्रेड - 1 उपभोक्ता मॉडल का उपयोग करता है, जहां अभिनेता N: M मॉडल का उपयोग करते हैं अर्थात आपके पास जितने चाहें उतने अभिनेता हो सकते हैं और उन्हें थ्रेड की एक निश्चित संख्या में वितरित किया जाएगा (आमतौर पर 1 प्रति कोर)।
  2. बैचहैंडलर इंटरफ़ेस अतिरिक्त (और बहुत महत्वपूर्ण) कॉलबैक प्रदान करता है onEndOfBatch()। यह धीमे उपभोक्ताओं के लिए अनुमति देता है, उदाहरण के लिए I / O बैच घटनाओं को एक साथ थ्रूपुट में सुधार करने के लिए। अन्य एक्टर फ्रेमवर्क में बैचिंग करना संभव है, हालांकि लगभग सभी अन्य फ्रेमवर्क बैच के अंत में कॉलबैक प्रदान नहीं करते हैं, आपको बैच के अंत को निर्धारित करने के लिए टाइमआउट का उपयोग करने की आवश्यकता होती है, जिसके परिणामस्वरूप खराब विलंबता होती है।

SEDA की तुलना में

LMAX ने SEDA आधारित दृष्टिकोण को बदलने के लिए विघटनकारी पैटर्न का निर्माण किया।

  1. SEDA द्वारा प्रदान किया गया मुख्य सुधार समानांतर में काम करने की क्षमता था। ऐसा करने के लिए विघटनकर्ता कई उपभोक्ताओं को एक ही संदेश (एक ही क्रम में) को बहु-कास्टिंग का समर्थन करता है। यह पाइपलाइन में कांटा चरणों की आवश्यकता से बचा जाता है।
  2. हम उपभोक्ताओं को उनके बीच एक और कतारबद्ध चरण डाले बिना अन्य उपभोक्ताओं के परिणामों पर प्रतीक्षा करने की भी अनुमति देते हैं। एक उपभोक्ता केवल उस उपभोक्ता की अनुक्रम संख्या देख सकता है जिस पर वह निर्भर है। इससे पाइपलाइन में शामिल होने की अवस्था से बचा जा सकता है।

मेमोरी बैरियर की तुलना में

इसके बारे में सोचने का एक और तरीका एक संरचित, आदेशित स्मृति बाधा के रूप में है। जहाँ निर्माता अवरोध लेखन अवरोध बनाता है और उपभोक्ता अवरोध रीड बैरियर है।


1
धन्यवाद माइकल। आपके लेखन और आपके द्वारा दिए गए लिंक ने मुझे यह समझने में मदद की है कि यह कैसे काम करता है। बाकी, मुझे लगता है कि मुझे इसे बस डूबने देना है।
शाहबाज़

मेरे पास अभी भी प्रश्न हैं: (1) 'कमिट' कैसे काम करता है? (२) जब रिंग बफर भरा होता है, तो निर्माता कैसे पता लगाता है कि सभी उपभोक्ताओं ने डेटा देखा है ताकि निर्माता प्रविष्टियों का फिर से उपयोग कर सकें?
Qwertie

@ क्वर्टी, शायद एक नया सवाल पोस्ट करने लायक है।
माइकल बार्कर

1
अंतिम बुलेट बिंदु (संख्या 2) का पहला वाक्य पढ़ने के बजाय SEDA की तुलना में नहीं होना चाहिए "हम उपभोक्ताओं को अन्य उपभोक्ताओं के परिणामों पर प्रतीक्षा करने की अनुमति देते हैं, जिनके बीच एक और कतारबद्ध चरण" पढ़ने "के लिए हम भी अनुमति देते हैं उपभोक्ताओं को अन्य उपभोक्ताओं के परिणामों पर इंतजार करना बिना उन दोनों के बीच एक और कतार चरण डाल करने के लिए हो रही "(यानी।" के साथ के बिना "" द्वारा प्रतिस्थापित किया जाना चाहिए ")?
रनवे

@runeks, हाँ यह होना चाहिए।
माइकल बार्कर

135

पहले हम इसे प्रदान करने वाले प्रोग्रामिंग मॉडल को समझना चाहते हैं।

एक या एक से अधिक लेखक हैं। एक या एक से अधिक पाठक हैं। प्रविष्टियों की एक पंक्ति है, जो पूरी तरह से पुरानी से नई (चित्र के रूप में बाएं से दाएं) का आदेश दिया गया है। राइट राइट पर राइटर्स नई एंट्रीज जोड़ सकते हैं। प्रत्येक पाठक प्रविष्टियों को क्रमिक रूप से बाएं से दाएं पढ़ता है। पाठक अतीत के लेखकों को नहीं पढ़ सकते, जाहिर है।

प्रविष्टि हटाने की कोई अवधारणा नहीं है। मैं उपभोगताओं की छवि से बचने के लिए "उपभोक्ता" के बजाय "रीडर" का उपयोग करता हूं। हालाँकि हम समझते हैं कि अंतिम पाठक के बाईं ओर की प्रविष्टियाँ बेकार हो जाती हैं।

आम तौर पर पाठक समवर्ती और स्वतंत्र रूप से पढ़ सकते हैं। हालाँकि हम पाठकों के बीच निर्भरता की घोषणा कर सकते हैं। पाठक निर्भरताएं मनमाने ढंग से चक्रीय ग्राफ हो सकती हैं। यदि पाठक B पाठक A पर निर्भर करता है, तो पाठक B पिछले पाठक A को नहीं पढ़ सकता है।

रीडर निर्भरता इसलिए उत्पन्न होती है क्योंकि रीडर ए प्रविष्टि में एनोटेट कर सकता है, और रीडर बी उस एनोटेशन पर निर्भर करता है। उदाहरण के लिए, A किसी प्रविष्टि पर कुछ गणना करता है, और परिणाम aको प्रविष्टि में फ़ील्ड में संग्रहीत करता है । ए तब आगे बढ़ता है, और अब बी प्रविष्टि को पढ़ सकता है, और aए संग्रहीत का मूल्य । यदि पाठक C A पर निर्भर नहीं है, तो C को पढ़ने का प्रयास नहीं करना चाहिए a

यह वास्तव में एक दिलचस्प प्रोग्रामिंग मॉडल है। प्रदर्शन के बावजूद, अकेले मॉडल बहुत सारे अनुप्रयोगों का लाभ उठा सकता है।

बेशक, LMAX का मुख्य लक्ष्य प्रदर्शन है। यह प्रविष्टियों की एक पूर्व-आवंटित रिंग का उपयोग करता है। अंगूठी काफी बड़ी है, लेकिन यह बाध्य है ताकि सिस्टम को डिजाइन क्षमता से परे लोड नहीं किया जाएगा। यदि अंगूठी भरी हुई है, तो लेखक तब तक प्रतीक्षा करेगा जब तक कि धीमे पाठक अग्रिम और कमरा न बना लें।

कचरा संग्रह लागत को कम करने के लिए, एंट्री ऑब्जेक्ट्स पूर्व-आवंटित और हमेशा के लिए रहते हैं। हम नई प्रविष्टि ऑब्जेक्ट सम्मिलित नहीं करते हैं या पुरानी प्रविष्टि ऑब्जेक्ट हटाते हैं, इसके बजाय, कोई लेखक पहले से मौजूद प्रविष्टि के लिए कहता है, उसके फ़ील्ड्स पॉप्युलेट करें, और पाठकों को सूचित करें। यह स्पष्ट 2-चरण कार्रवाई वास्तव में केवल एक परमाणु कार्रवाई है

setNewEntry(EntryPopulator);

interface EntryPopulator{ void populate(Entry existingEntry); }

पूर्व-आबंटित प्रविष्टियों का अर्थ आसन्न प्रविष्टियाँ (बहुत अधिक संभावना) आसन्न मेमोरी कोशिकाओं में पाया जाता है, और क्योंकि पाठक क्रमिक रूप से प्रविष्टियाँ पढ़ते हैं, यह CPU कैश का उपयोग करने के लिए महत्वपूर्ण है।

और लॉक से बचने के लिए बहुत सारे प्रयास, कैस, यहां तक ​​कि मेमोरी बैरियर (जैसे कि केवल एक लेखक है तो एक गैर-वाष्पशील अनुक्रम चर का उपयोग करें)

पाठकों के डेवलपर्स के लिए: विभिन्न एनोटेटिंग पाठकों को अलग-अलग क्षेत्रों में लिखना चाहिए, लेखन विवाद से बचने के लिए। (वास्तव में उन्हें अलग-अलग कैश लाइनों को लिखना चाहिए।) एनोटेट करने वाले पाठक को कुछ भी नहीं छूना चाहिए जो अन्य गैर-निर्भर पाठक पढ़ सकते हैं। यही कारण है कि मैं कहता हूं कि ये पाठक प्रविष्टियों को संशोधित करने के बजाय, प्रविष्टियों को एनोटेट करते हैं।


2
मुझे ठीक लगता है। मुझे एनोटेट शब्द का उपयोग पसंद है।
माइकल बार्कर

21
+1 यह एकमात्र उत्तर है जो यह वर्णन करने का प्रयास करता है कि विघटनकारी पैटर्न वास्तव में कैसे काम करता है, जैसा कि ओपी ने पूछा है।
G-Wiz

1
यदि अंगूठी भरी हुई है, तो लेखक तब तक प्रतीक्षा करेगा जब तक कि धीमे पाठक अग्रिम और कमरा न बना लें। - एक समस्या w / गहरी फीफो कतारों में उन्हें बहुत आसानी से भरी जा रही है, क्योंकि वे वास्तव में वापस दबाव का प्रयास नहीं करते हैं जब तक कि वे भरवां न हो जाएं और विलंबता पहले से ही अधिक है।
bestsss

1
@irreputable क्या आप लेखक पक्ष के लिए भी ऐसी ही व्याख्या लिख ​​सकते हैं?
बुची

मुझे यह पसंद है लेकिन मुझे यह मिला "एक लेखक पहले से मौजूद प्रविष्टि के लिए पूछता है, अपने खेतों को आबाद करता है, और पाठकों को सूचित करता है। यह स्पष्ट 2-चरण की कार्रवाई वास्तव में एक परमाणु कार्रवाई है" भ्रामक और संभवतः गलत है? कोई "सूचना" सही नहीं है? यह भी परमाणु नहीं है यह सिर्फ एक प्रभावी / दृश्यमान लेखन है, सही है? महान जवाब सिर्फ भाषा जो अस्पष्ट है?
हैवगुएस


17

मैंने वास्तव में वास्तविक स्रोत का अध्ययन करने के लिए समय निकाला, सरासर जिज्ञासा से बाहर, और इसके पीछे का विचार काफी सरल है। इस पोस्ट को लिखने के समय सबसे हाल का संस्करण 3.2.1 है।

पूर्व-आवंटित ईवेंट्स को सहेजने के लिए एक बफर है जो उपभोक्ताओं के डेटा को पढ़ने के लिए रखेगा।

बफर इसकी लंबाई के झंडे (पूर्णांक सरणी) के सरणी द्वारा समर्थित है जो बफर स्लॉट की उपलब्धता का वर्णन करता है (विवरण के लिए आगे देखें)। सरणी को एक जावा # AtomicIntegerArray की तरह एक्सेस किया जाता है, इसलिए इस अन्वेषण के उद्देश्य के लिए आप इसे एक मान सकते हैं।

किसी भी संख्या में निर्माता हो सकते हैं। जब निर्माता बफर को लिखना चाहता है, तो एक लंबी संख्या उत्पन्न होती है (जैसा कि एटॉमिकलॉन्ग # getAndIncrement को कॉल करने में, विघटनकर्ता वास्तव में अपने स्वयं के कार्यान्वयन का उपयोग करता है, लेकिन यह उसी तरीके से काम करता है)। आइए, इस जेनरेट किए गए प्रोड्यूसर को लंबे समय तक कॉल करें। एक समान तरीके से, एक ConsumerCallId उत्पन्न होता है जब एक उपभोक्ता ENDS बफर से एक स्लॉट को पढ़ता है। सबसे हाल ही में ConsumerCallId पहुँचा है।

(यदि कई उपभोक्ता हैं, तो सबसे कम आईडी वाला कॉल चुना जाता है।)

इन आईडी की तुलना तब की जाती है, और यदि दोनों के बीच अंतर कम है कि बफर पक्ष, निर्माता को लिखने की अनुमति है।

(यदि निर्माता हाल ही के उपभोक्ताकॉल आई + बफरसाइज से अधिक है, तो इसका मतलब है कि बफर भरा हुआ है, और निर्माता तब तक बस इंतजार करने के लिए मजबूर है जब तक कि कोई स्पॉट उपलब्ध नहीं हो जाता।)

तब निर्माता को उसके कॉल आईड के आधार पर बफर में स्लॉट दिया जाता है (जो कि prducerCallId modulo बफरसाइज़ है, लेकिन चूंकि बफरसाइज़ हमेशा 2 की शक्ति होती है (बफर निर्माण पर लागू सीमा), जिस एक्टल ऑपरेशन का उपयोग किया जाता है, वह है ))। यह तब उस स्लॉट में ईवेंट को संशोधित करने के लिए स्वतंत्र है।

(वास्तविक एल्गोरिथ्म थोड़ा और अधिक जटिल है, जिसमें अनुकूलन के उद्देश्यों के लिए एक अलग परमाणु संदर्भ में हाल के उपभोक्ता आईडी को शामिल करना शामिल है)

जब घटना को संशोधित किया गया था, तो परिवर्तन "प्रकाशित" है। ध्वज सरणी में संबंधित स्लॉट को प्रकाशित करते समय अद्यतन ध्वज से भरा जाता है। फ्लैग वैल्यू लूप की संख्या है (निर्माताकैलिड को बफरसेज द्वारा विभाजित किया गया है (फिर से बफरसिज़ 2 की शक्ति है, वास्तविक ऑपरेशन एक सही बदलाव है)।

इसी तरह से उपभोक्ताओं की संख्या हो सकती है। जब भी कोई उपभोक्ता बफर का उपयोग करना चाहता है, तो एक ConsumerCallId उत्पन्न होता है (यह निर्भर करता है कि उपभोक्ताओं को निष्क्रिय करने के लिए कैसे जोड़ा गया था, आईडी पीढ़ी में प्रयुक्त परमाणु को साझा किया जा सकता है या उनमें से प्रत्येक के लिए अलग हो सकता है)। यह ConsumerCallId तब सबसे हाल ही में किए गए ProductCallId की तुलना में है, और यदि यह दो से कम है, तो पाठक को प्रगति करने की अनुमति है।

(इसी तरह अगर निर्माताकॉल आईड भी उपभोक्ताकॉल आइडी के लिए है, तो इसका मतलब है कि बफर समान है और उपभोक्ता को इंतजार करने के लिए मजबूर किया जाता है। विघटनकारी निर्माण के दौरान प्रतीक्षा के तरीके को एक वैस्टस्ट्रेगी द्वारा परिभाषित किया जाता है।)

व्यक्तिगत उपभोक्ताओं (अपने स्वयं के आईडी जनरेटर के साथ) के लिए, अगली चीज की जाँच बैच उपभोग करने की क्षमता है। बफर में स्लॉट्स की जांच उपभोक्ता से संबंधित एक से की जाती है (हाल ही में निर्माता के लिए) के रूप में एक ही तरीके से सूचकांक निर्धारित किया जाता है, हाल ही में एक निर्माता से संबंधित है।

वे उपभोक्ता सरणी में उत्पन्न ध्वज मूल्य के खिलाफ ध्वज सरणी में लिखे गए ध्वज मूल्य की तुलना करके एक लूप में जांच की जाती है। यदि झंडे मेल खाते हैं तो इसका मतलब है कि स्लॉट्स को भरने वाले उत्पादकों ने अपने बदलाव शुरू कर दिए हैं। यदि नहीं, तो लूप टूट गया है, और उच्चतम प्रारंभ किया गया changeId वापस आ गया है। ChangeId में ConsumerCallId से प्राप्त किए गए स्लॉट को बैच में उपभोग किया जा सकता है।

यदि उपभोक्ताओं का एक समूह (साझा आईडी जनरेटर वाले) एक साथ पढ़ता है, तो हर एक केवल एक ही कॉलआईड लेता है, और केवल उस एकल कॉलआईड के लिए स्लॉट की जाँच की जाती है और वापस आ जाता है।


7

से इस अनुच्छेद :

विघटनकारी पैटर्न एक बैचिंग कतार है जो एक पूर्व-आवंटित हस्तांतरण वस्तुओं से भरा एक गोलाकार सरणी (यानी रिंग बफर) द्वारा समर्थित है जो क्रम के माध्यम से उत्पादकों और उपभोक्ताओं को सिंक्रनाइज़ करने के लिए मेमोरी-बैरियर का उपयोग करता है।

मेमोरी-बैरियर की व्याख्या करना कठिन है और त्रिशा के ब्लॉग ने इस पोस्ट के साथ मेरी राय में सबसे अच्छा प्रयास किया है: http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast। एचटीएमएल

लेकिन अगर आप निम्न-स्तरीय विवरणों में गोता नहीं लगाना चाहते हैं, तो आप बस यह जान सकते हैं कि जावा में मेमोरी-बैरियर volatileकीवर्ड के माध्यम से और के माध्यम से कार्यान्वित किए जाते हैं java.util.concurrent.AtomicLong। विघटनकारी पैटर्न अनुक्रम AtomicLongएस होते हैं और लॉक के बजाय मेमोरी-बैरियर के माध्यम से उत्पादकों और उपभोक्ताओं के बीच आगे-पीछे होते हैं।

मैं यह आसान कोड के माध्यम से एक अवधारणा को समझने की, तो नीचे दिए गए कोड एक सरल है HelloWorld से CoralQueue , एक disruptor पैटर्न CoralBlocks जिसके साथ मैं संबद्ध कर रहा हूँ के द्वारा किया कार्यान्वयन है। नीचे दिए गए कोड में आप देख सकते हैं कि कैसे विघटनकारी पैटर्न बैचिंग को लागू करता है और रिंग-बफर (यानी परिपत्र सरणी) दो थ्रेड्स के बीच कचरा-मुक्त संचार की अनुमति कैसे देता है:

package com.coralblocks.coralqueue.sample.queue;

import com.coralblocks.coralqueue.AtomicQueue;
import com.coralblocks.coralqueue.Queue;
import com.coralblocks.coralqueue.util.MutableLong;

public class Sample {

    public static void main(String[] args) throws InterruptedException {

        final Queue<MutableLong> queue = new AtomicQueue<MutableLong>(1024, MutableLong.class);

        Thread consumer = new Thread() {

            @Override
            public void run() {

                boolean running = true;

                while(running) {
                    long avail;
                    while((avail = queue.availableToPoll()) == 0); // busy spin
                    for(int i = 0; i < avail; i++) {
                        MutableLong ml = queue.poll();
                        if (ml.get() == -1) {
                            running = false;
                        } else {
                            System.out.println(ml.get());
                        }
                    }
                    queue.donePolling();
                }
            }

        };

        consumer.start();

        MutableLong ml;

        for(int i = 0; i < 10; i++) {
            while((ml = queue.nextToDispatch()) == null); // busy spin
            ml.set(System.nanoTime());
            queue.flush();
        }

        // send a message to stop consumer...
        while((ml = queue.nextToDispatch()) == null); // busy spin
        ml.set(-1);
        queue.flush();

        consumer.join(); // wait for the consumer thread to die...
    }
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.