बल्कहेड पैटर्न: दोष-सहिष्णु माइक्रोसर्विसेज डिजाइन करना

बल्कहेड पैटर्न: दोष-सहिष्णु माइक्रोसर्विसेज डिजाइन करना

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

यदि कोई डाउनस्ट्रीम सेवा सुस्त या अनुत्तरदायी हो जाती है, तो आपकी अपस्ट्रीम सेवाओं के लिए आने वाले अनुरोध ढेर होने लगेंगे। यदि वे सभी एक ही मेमोरी, सीपीयू, या थ्रेड पूल साझा करते हैं, तो धीमी निर्भरता सभी उपलब्ध संसाधनों को जल्दी से समाप्त कर सकती है, जिससे आपका पूरा एप्लिकेशन क्रैश हो सकता है।

इस व्यापक विफलता को डोमिनोज़ प्रभाव के रूप में जाना जाता है। इसे रोकने के लिए, सिस्टम आर्किटेक्ट बल्कहेड पैटर्न का उपयोग करते हैं।

इस गाइड में, हम पता लगाएंगे कि बल्कहेड पैटर्न क्या है, यह कैसे काम करता है, और जावा (रेसिलिएंस4जे) और गो में सरल उपमाओं, वास्तुशिल्प अवधारणाओं और कोड उदाहरणों का उपयोग करके इसे कैसे कार्यान्वित किया जाए।


वास्तविक दुनिया सादृश्य: वॉटरटाइट शिप बल्कहेड्स

इस पैटर्न का नाम जहाज निर्माण उद्योग से आया है।

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

बल्कहेड पैटर्न आर्किटेक्चर आरेख साझा बनाम पृथक थ्रेड पूल दिखा रहा है

यदि जहाज किसी बाधा से टकराता है और उसका पतवार टूट जाता है, तो क्षतिग्रस्त डिब्बे में पानी भर जाएगा। हालाँकि, जलरोधी बल्कहेड्स के कारण, पानी उस एक डिब्बे में ही रुक जाता है। जहाज का बाकी हिस्सा सूखा और उत्साहित रहता है, जिससे यह तैरता रहता है और सुरक्षित रहता है।

बल्कहेड के बिना, पानी पूरे पतवार में स्वतंत्र रूप से बहेगा, अंततः जहाज डूब जाएगा।

सॉफ्टवेयर इंजीनियरिंग में:

  • जहाज आपका संपूर्ण एप्लिकेशन या सेवा है।
  • कम्पार्टमेंट पृथक संसाधन पूल (थ्रेड्स, कनेक्शन, सीपीयू) हैं।
  • हल उल्लंघन डाउनस्ट्रीम माइक्रोसर्विस में विफलता या मंदी है।
  • बाढ़ संसाधनों की कमी है।

समस्या: साझा संसाधन पूल और थ्रेड थकावट

यह समझने के लिए कि बल्कहेड क्यों आवश्यक हैं, आइए देखें कि जब संसाधनों को विश्व स्तर पर साझा किया जाता है तो क्या होता है।

एक एपीआई गेटवे या उपयोगकर्ता अनुरोधों को संभालने वाले वेब सर्वर की कल्पना करें। इसमें सभी इनकमिंग कॉल्स को प्रोसेस करने के लिए 100 थ्रेड्स का एकल वैश्विक थ्रेड पूल है। सर्वर तीन डाउनस्ट्रीम सेवाओं के साथ इंटरैक्ट करता है:

  1. कैटलॉग सेवा (तेजी से, उत्पाद सूची पढ़ता है)
  2. भुगतान सेवा (तेज़, प्रोसेस चेकआउट)
  3. सिफारिश सेवा (धीमी, वैयक्तिकृत वस्तुओं की गणना करती है)

आम तौर पर, सब कुछ ठीक काम करता है। लेकिन मान लीजिए कि अनुशंसा सेवा डेटाबेस गतिरोध से ग्रस्त है और 200 मिलीसेकंड के बजाय प्रतिक्रिया देने में 30 सेकंड लेना शुरू कर देती है।

यहाँ क्या होता है:

  1. उपयोगकर्ता होम पेज पर आना जारी रखते हैं, जिससे अनुशंसा सेवा के लिए अनुरोध ट्रिगर होते हैं।
  2. सर्वर प्रत्येक अनुरोध के लिए वैश्विक पूल से एक थ्रेड निर्दिष्ट करता है।
  3. क्योंकि अनुशंसा सेवा धीमी है, ये थ्रेड्स प्रतिक्रियाओं की प्रतीक्षा में बैठे रहते हैं।
  4. कुछ ही सेकंड में, पूल के सभी 100 थ्रेड अनुशंसा सेवा पर प्रतीक्षा कर रहे हैं।
  5. जब कोई नया उपयोगकर्ता कैटलॉग को चेकआउट करने या देखने का प्रयास करता है, तो सर्वर के पास उनके अनुरोध को संसाधित करने के लिए कोई थ्रेड नहीं बचता है।

भले ही कैटलॉग और भुगतान सेवाएँ पूरी तरह से स्वस्थ हैं, वे अब पहुंच से बाहर हैं क्योंकि धीमी अनुशंसा सेवा ने साझा थ्रेड पूल को समाप्त कर दिया है। पूरा सिस्टम ऑफलाइन हो गया है.


समाधान: बल्कहेड पैटर्न

बल्कहेड पैटर्न संसाधन पूल को विभाजित करके इस समस्या को हल करता है ताकि एक क्षेत्र में विफलता दूसरे को प्रभावित न करे।

एकल वैश्विक पूल के बजाय, हम प्रत्येक सेवा या डाउनस्ट्रीम निर्भरता के लिए अलग, सीमित पूल आवंटित करते हैं।

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

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


बल्कहेड अलगाव के प्रकार

सॉफ़्टवेयर सिस्टम में बल्कहेड लागू करने के दो प्राथमिक तरीके हैं:

1. थ्रेड पूल अलगाव

इस मॉडल में, प्रत्येक डाउनस्ट्रीम निर्भरता को अपना स्वयं का समर्पित थ्रेड पूल और निष्पादन कतार सौंपी जाती है।

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

2. सेमाफोर अलगाव

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

सेमाफोर आइसोलेशन आरेख परमिट प्राप्त करने के बाद कार्यों को निष्पादित करने वाले अनुरोध थ्रेड दिखा रहा है
  • यह कैसे काम करता है: जब कोई अनुरोध शुरू होता है, तो यह सेमाफोर से परमिट प्राप्त करने का प्रयास करता है। यदि परमिट उपलब्ध है, तो यह कॉलिंग थ्रेड पर अनुरोध निष्पादित करता है और समाप्त होने पर परमिट जारी करता है। यदि कोई परमिट उपलब्ध नहीं है, तो अनुरोध तुरंत अस्वीकार कर दिया जाता है।
  • पेशेवर: वस्तुतः शून्य ओवरहेड के साथ बहुत हल्का, क्योंकि इसमें कोई थ्रेड संदर्भ स्विचिंग शामिल नहीं है।
  • नुकसान: कोई धागा पृथक्करण नहीं। यदि किसी कॉल को उचित टाइमआउट के बिना नेटवर्क सॉकेट पर ब्लॉक किया जाता है, तो यह अभी भी कॉलिंग थ्रेड को ब्लॉक कर सकता है।

कार्यान्वयन उदाहरण

आइए देखें कि दो लोकप्रिय बैक-एंड भाषाओं में बल्कहेड्स को कैसे लागू किया जाए।

1. जावा (रेज़िलिएंस4जे और स्प्रिंग बूट)

Resilience4j जावा के लिए डिज़ाइन किया गया एक हल्का, उपयोग में आसान दोष सहनशील पुस्तकालय है। नीचे बताया गया है कि आप स्प्रिंग बूट एप्लिकेशन में डाउनस्ट्रीम भुगतान सेवा के लिए बल्कहेड को कैसे कॉन्फ़िगर करते हैं।

कॉन्फ़िगरेशन (एप्लिकेशन.yml)

resilience4j.bulkhead:
  instances:
    paymentService:
      maxConcurrentCalls: 10
      maxWaitDuration: 10ms

resilience4j.threadpoolbulkhead:
  instances:
    paymentService:
      maxThreadPoolSize: 10
      coreThreadPoolSize: 5
      queueCapacity: 20

कोड कार्यान्वयन

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderService {

    private final RestTemplate restTemplate;

    public OrderService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    // Apply semaphore bulkhead
    @Bulkhead(name = "paymentService", fallbackMethod = "paymentFallback")
    public String processPayment(OrderDetails details) {
        return restTemplate.postForObject("http://payment-service/charge", details, String.class);
    }

    // Fallback method executed when the bulkhead is full
    public String paymentFallback(OrderDetails details, Throwable throwable) {
        return "Payment service is currently busy. Please try again later.";
    }
}

2. जाओ (गोलंग)

गो में, हमें आवश्यक रूप से एक भारी ढांचे की आवश्यकता नहीं है क्योंकि भाषा गोरोइन्स और बफर चैनल जैसे मूल समवर्ती प्राइमेटिव प्रदान करती है। हम बफ़र्ड चैनल का उपयोग करके एक साफ़ सेमाफोर बल्कहेड लागू कर सकते हैं:

package main

import (
	"errors"
	"fmt"
	"net/http"
	"time"
)

// Bulkhead represents a concurrency limiter
type Bulkhead struct {
	semaphore chan struct{}
}

// NewBulkhead initializes a bulkhead with a max concurrency limit
func NewBulkhead(maxConcurrency int) *Bulkhead {
	return &Bulkhead{
		semaphore: make(chan struct{}, maxConcurrency),
	}
}

// Execute runs the task if resource permit is available, otherwise returns error
func (b *Bulkhead) Execute(task func() error) error {
	select {
	case b.semaphore <- struct{}{}:
		// Acquired permit
		defer func() { <-b.semaphore }() // Release permit
		return task()
	default:
		// Bulkhead is full, reject immediately
		return errors.New("bulkhead is full: request rejected")
	}
}

func main() {
	// Allow maximum of 3 concurrent calls
	paymentBulkhead := NewBulkhead(3)

	mockTask := func() error {
		fmt.Println("Processing payment...")
		time.Sleep(2 * time.Second) // Simulate network delay
		return nil
	}

	// Simulate 5 rapid requests
	for i := 1; i <= 5; i++ {
		go func(reqID int) {
			err := paymentBulkhead.Execute(mockTask)
			if err != nil {
				fmt.Printf("Request %d failed: %v\n", reqID, err)
			} else {
				fmt.Printf("Request %d completed successfully\n", reqID)
			}
		}(i)
	}

	// Keep main alive to watch output
	time.Sleep(3 * time.Second)
}

बल्कहेड पैटर्न के लिए सामान्य उपयोग के मामले

यहां कुछ विशिष्ट परिदृश्य दिए गए हैं जहां बल्कहेड पैटर्न लागू करना महत्वपूर्ण है:

  • एपीआई गेटवे रूटिंग: विभिन्न बैकएंड सेवाओं के लिए मार्गों को अलग करना। यदि अनुशंसा सेवा बंद हो जाती है, तो गेटवे पर ऑर्डर सेवा मार्ग पूरी तरह से चालू रहते हैं।
  • डेटाबेस कनेक्शन पूल: डेटाबेस कनेक्शन पूल को सेवा या किरायेदार द्वारा विभाजित करना। एक किरायेदार से भारी विश्लेषणात्मक प्रश्नों की वृद्धि सभी उपलब्ध कनेक्शन हैंडल को ख़त्म नहीं करेगी, जिससे अन्य किरायेदारों के लिए लेन-देन संबंधी प्रश्न बच जाएंगे।
  • बहु-किरायेदार SaaS अनुप्रयोग: प्रीमियम बनाम मुफ़्त किरायेदारों के लिए गणना संसाधनों या निष्पादन कतारों को अलग करना। फ्री टियर रिसोर्स स्पाइक्स सीपीयू या मेमोरी के प्रीमियम टियर अनुरोधों को भूखा नहीं रखेगा।
  • तृतीय-पक्ष एपीआई एकीकरण: बाहरी भुगतान गेटवे, शिपिंग प्रदाताओं, या अधिसूचना इंजनों के लिए अलग HTTP क्लाइंट पूल समर्पित करना। यदि एक तृतीय-पक्ष सेवा धीमी हो जाती है, तो अन्य बाहरी इंटरैक्शन बिना किसी रुकावट के जारी रहते हैं।

क्यों काफ्का/संदेश दलाल बल्कहेड पैटर्न को प्रतिस्थापित नहीं कर सकते

एक सामान्य प्रश्न है: “यदि हमारे पास अपाचे काफ्का जैसे संदेश दलाल हैं, तो हमें बल्कहेड पैटर्न की आवश्यकता क्यों है? क्या हम अनुरोधों को बफर करने के लिए केवल कतारों का उपयोग नहीं कर सकते?”

जबकि संदेश दलाल सिस्टम को अलग कर देते हैं, वे बल्कहेड पैटर्न को प्रतिस्थापित नहीं कर सकते। यहाँ इसका कारण बताया गया है:

1. सिंक्रोनस बनाम एसिंक्रोनस संचार

काफ्का को एसिंक्रोनस, इवेंट-संचालित आर्किटेक्चर के लिए डिज़ाइन किया गया है। निर्माता किसी विषय पर संदेश भेजता है, और उपभोक्ता अंततः उस पर कार्रवाई करता है। हालाँकि, उपयोगकर्ता-सामना वाले अनुप्रयोगों को अक्सर सिंक्रोनस (अनुरोध-प्रतिक्रिया) संचार की आवश्यकता होती है (उदाहरण के लिए, उत्पाद कैटलॉग लोड करना या REST/gRPC एपीआई के माध्यम से क्रेडिट कार्ड चार्ज करना)। यहां काफ्का को पेश करने के लिए जटिल अनुरोध-उत्तर पैटर्न, उच्च विलंबता और ओवरहेड को जोड़ने की आवश्यकता होती है। बल्कहेड्स को विशेष रूप से वास्तविक समय में इन तुल्यकालिक निष्पादन थ्रेड्स की सुरक्षा के लिए डिज़ाइन किया गया है।

2. काफ्का उपभोक्ताओं के अंदर थ्रेड भुखमरी

भले ही आपका सिस्टम पूरी तरह से इवेंट-संचालित है और काफ्का का उपयोग करता है, फिर भी आपको बल्कहेड्स की आवश्यकता है! मान लीजिए कि एक एकल उपभोक्ता माइक्रोसर्विस कई काफ्का विषयों को सुनता है (उदाहरण के लिए, user-registrations और video-transcoding)। यदि उपभोक्ता धीमी video-transcoding नौकरियों के एक बड़े बैच को संसाधित करने के लिए अपने सभी आंतरिक कार्यकर्ता थ्रेड आवंटित करता है, तो उसे थ्रेड भुखमरी का अनुभव होगा। उपभोक्ता हल्के user-registrations संदेशों को संसाधित करने में सक्षम नहीं होगा, भले ही वह विभाजन स्वस्थ हो। काम को अलग करने के लिए आपको अभी भी उपभोक्ता सेवा के अंदर आंतरिक बल्कहेड्स (अलग थ्रेड पूल) की आवश्यकता है।

3. क्लाइंट-साइड ओवरहेड और फ़ेल-फ़ास्ट आवश्यकता

जब एक डाउनस्ट्रीम सेवा बंद हो जाती है, तो एक बल्कहेड कॉलिंग सेवा को तेजी से विफल होने देता है और तुरंत फ़ॉलबैक प्रतिक्रिया लौटाता है। यदि आप इसके बजाय काफ्का में सब कुछ कतारबद्ध करते हैं, तो कतार असीमित रूप से बढ़ सकती है, जिससे पुराने अनुरोध, उच्च मेमोरी खपत और सिस्टम के ठीक होने पर विलंबित टाइमआउट हो सकता है।

संक्षेप में, काफ्का नेटवर्क पर सिस्टम के बीच संचार को अलग करता है, जबकि बल्कहेड्स एक चल रहे एप्लिकेशन इंस्टेंस के भीतर संसाधन निष्पादन को अलग करता है। वे पूरक हैं, परस्पर अनन्य नहीं।


बल्कहेड्स का उपयोग करते समय सर्वोत्तम अभ्यास

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

निष्कर्ष

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