पुन: प्रयास पैटर्न: लचीली माइक्रोसर्विसेज का निर्माण
माइक्रोसर्विसेज आर्किटेक्चर में, सेवाएँ इन-मेमोरी कॉल के बजाय नेटवर्क पर संचार करती हैं। जबकि यह डिकॉउलिंग बड़े पैमाने पर क्षैतिज स्केलिंग और स्वतंत्र तैनाती को सक्षम बनाता है, यह एक बड़ी भेद्यता भी पेश करता है: नेटवर्क अविश्वसनीय है।
किसी भी समय, एक डाउनस्ट्रीम सेवा में एक संक्षिप्त नेटवर्क गड़बड़ी, एक अस्थायी सीपीयू स्पाइक, एक त्वरित डेटाबेस लॉक विवाद, या एक रोलिंग अपडेट पुनरारंभ का अनुभव हो सकता है। इन अस्थायी विफलताओं को क्षणिक दोष के रूप में जाना जाता है।
यदि आपकी सेवा तुरंत कोई त्रुटि उत्पन्न करती है और डाउनस्ट्रीम कॉल विफल होने पर अनुरोध विफल कर देती है, तो आप एक नाजुक उपयोगकर्ता अनुभव बनाते हैं। इसके बजाय, इनमें से कई क्षणिक त्रुटियों को एक क्षण प्रतीक्षा करके और पुनः प्रयास करके स्वचालित रूप से हल किया जा सकता है। यहीं पर पुनःप्रयास पैटर्न आता है।
इस गाइड में, हम रिट्री पैटर्न का पता लगाएंगे, यह हुड के तहत कैसे काम करता है, अनुभवहीन कार्यान्वयन के खतरे, और जावा (रेसिलिएंस4जे) और गो में इसे सही तरीके से कैसे लागू किया जाए।
वास्तविक दुनिया सादृश्य: एक व्यस्त लाइन को पुनः डायल करना
कल्पना कीजिए कि आप किसी मित्र को कॉल करने का प्रयास कर रहे हैं। आप उनका नंबर डायल करते हैं, लेकिन आपको व्यस्त सिग्नल मिलता है क्योंकि वे इस समय किसी अन्य कॉल पर हैं।
क्या आप तुरंत हार मान लेते हैं, उनका संपर्क हटा देते हैं और मान लेते हैं कि आप उनसे कभी दोबारा बात नहीं कर पाएंगे? बिल्कुल नहीं। आप फोन रख दें, एक मिनट रुकें और फिर से उनका नंबर डायल करें। यदि वे अभी भी व्यस्त हैं, तो आप दोबारा प्रयास करने से पहले पांच मिनट प्रतीक्षा कर सकते हैं।
अंततः, उनकी कॉल समाप्त हो जाती है, और आपका पुनः प्रयास सफल हो जाता है।
माइक्रोसर्विसेज में:
- कॉल एक डाउनस्ट्रीम सेवा के लिए एक एपीआई अनुरोध है।
- व्यस्त सिग्नल एक क्षणिक नेटवर्क त्रुटि या
503 Service Unavailableप्रतिक्रिया है। - रीडायल एक पुनः प्रयास का प्रयास है।
- प्रतीक्षा समय बैकऑफ़ अवधि है।
ख़तरा: अनुभवहीन पुनः प्रयास और “पुनः प्रयास तूफान”
पुनर्प्रयास तंत्र को लागू करना पहली नज़र में मामूली लगता है: बस अपने HTTP कॉल को for लूप में लपेटें और तब तक प्रयास करते रहें जब तक यह सफल न हो जाए। हालाँकि, एक सहज पुनः प्रयास कार्यान्वयन आसानी से एक छोटी सी हिचकी को एक विनाशकारी सिस्टम-व्यापी आउटेज में बदल सकता है।
एक डाउनस्ट्रीम सेवा की कल्पना करें जो यातायात में अचानक वृद्धि के कारण संघर्ष कर रही है। इसका डेटाबेस 99% सीपीयू उपयोग पर चल रहा है, और अनुरोधों का समय समाप्त होने लगा है।
यदि 100 ग्राहक सेवाएँ एक टाइमआउट का पता लगाती हैं और बिना प्रतीक्षा किए तुरंत 3 बार पुनः प्रयास करती हैं, तो वे पहले से ही कमजोर डाउनस्ट्रीम सेवा को प्रभावित करने वाले ट्रैफ़िक की मात्रा को अचानक तीन गुना कर देंगे। ट्रैफ़िक में इस अचानक वृद्धि को रीट्री स्टॉर्म (या थंडरिंग हर्ड समस्या) के रूप में जाना जाता है।
डाउनस्ट्रीम सेवा को ठीक होने में मदद करने के बजाय, आपके पुनः प्रयास इसे नीचे धकेलते रहेंगे, और इसे कभी भी गति पकड़ने से रोकेंगे।
समाधान: बैकऑफ़ और जिटर
पुनः प्रयास तूफानों को रोकने के लिए, एक लचीली प्रणाली को दो आवश्यक रणनीतियों का उपयोग करना चाहिए: बैकऑफ़ और जिटर।
1. बैकऑफ़ रणनीतियाँ
बैकऑफ यह तय करता है कि ग्राहक को बाद में पुनः प्रयास करने से पहले कितनी देर तक इंतजार करना चाहिए।
- फिक्स्ड बैकऑफ़: क्लाइंट प्रयासों के बीच निरंतर समय (उदाहरण के लिए, ठीक 200ms) की प्रतीक्षा करता है। सरल होते हुए भी, इसमें समकालिक ट्रैफ़िक स्पाइक्स का जोखिम रहता है।
- घातीय बैकऑफ़: प्रत्येक असफल प्रयास के साथ प्रतीक्षा समय तेजी से बढ़ता है (उदाहरण के लिए, 100ms, 200ms, 400ms, 800ms)। इससे डाउनस्ट्रीम सेवा को ठीक होने के लिए उत्तरोत्तर अधिक समय मिलता है क्योंकि विफलता बनी रहती है।
2. घबराहट (यादृच्छिकता)
घातीय बैकऑफ़ के साथ भी, यदि नेटवर्क ब्लिप के कारण ठीक उसी समय 1,000 अनुरोध विफल हो जाते हैं, तो सभी 1,000 क्लाइंट ठीक उसी बैकऑफ़ विलंब की गणना करेंगे। नतीजतन, वे सभी तरंगों में एक साथ पुनः प्रयास करेंगे, सिंक्रोनाइज़्ड स्पाइक्स के साथ डाउनस्ट्रीम सेवा को हिट करेंगे।
जिटर बैकऑफ विलंब में यादृच्छिक भिन्नता जोड़कर इसे हल करता है।
Without Jitter (Synchronized Waves):
Time: 0ms -> [1000 requests fail]
Time: 100ms -> [1000 retries hit simultaneously]
Time: 200ms -> [1000 retries hit simultaneously]
With Jitter (Distributed Traffic):
Time: 0ms -> [1000 requests fail]
Time: 92ms -> [85 retries]
Time: 105ms -> [120 retries]
Time: 118ms -> [95 retries]
... (Traffic is smoothed out over time)
पुनर्प्रयास को एक यादृच्छिक अंतराल पर फैलाकर, डाउनस्ट्रीम सेवा पर लोड को सुचारू कर दिया जाता है, जिससे यह शानदार तरीके से ठीक हो जाता है।
पुनः प्रयास का स्वर्णिम नियम: निष्क्रियता
किसी भी एपीआई एंडपॉइंट पर पुनः प्रयास लागू करने से पहले, आपको एक महत्वपूर्ण प्रश्न पूछना चाहिए: क्या यह ऑपरेशन निष्क्रिय है?
एक आइडेम्पोटेंट ऑपरेशन वह है जहां कई समान अनुरोध करने का प्रभाव एक ही अनुरोध के समान ही होता है।
- इडेम्पोटेंट: उपयोगकर्ता प्रोफ़ाइल पढ़ना (
GET /users/123), संपूर्ण ईमेल फ़ील्ड अपडेट करना (PUT /users/123/email), या कोई आइटम हटाना (DELETE /items/456)। - नॉन-इडेम्पोटेंट: एक नया ऑर्डर बनाना (
POST /orders), या क्रेडिट कार्ड शुल्क संसाधित करना (POST /payments)।
मान लीजिए आप किसी ग्राहक से $50 वसूलने के लिए POST /payments पर कॉल करते हैं। डाउनस्ट्रीम भुगतान गेटवे अनुरोध प्राप्त करता है, क्रेडिट कार्ड को सफलतापूर्वक चार्ज करता है, लेकिन फिर 200 OK प्रतिक्रिया आपको वापस भेजने से पहले एक नेटवर्क ब्लिप होता है।
आपकी सेवा एक टाइमआउट दर्ज करती है, मान लेती है कि कॉल विफल हो गई है, और स्वचालित रूप से पुनः प्रयास करती है। यदि भुगतान गेटवे डुप्लिकेट अनुरोधों को संभालने के लिए डिज़ाइन नहीं किया गया है, तो ग्राहक से दोगुना शुल्क लिया जाएगा।
[!चेतावनी] जब तक डाउनस्ट्रीम सेवा इडेम्पोटेंसी कुंजी (डुप्लिकेट लेनदेन का पता लगाने और त्यागने के लिए उपयोग किए जाने वाले अद्वितीय अनुरोध पहचानकर्ता) का समर्थन नहीं करती है, तब तक कभी भी गैर-इम्पोटेंट ऑपरेशंस का पुनः प्रयास न करें।
कार्यान्वयन उदाहरण
आइए देखें कि जावा और गो में रिट्री पैटर्न को कैसे लागू किया जाए।
1. जावा (रेज़िलिएंस4जे और स्प्रिंग बूट)
Resilience4j एक मजबूत, उच्च विन्यास योग्य पुनः प्रयास मॉड्यूल प्रदान करता है। बाहरी बिलिंग सेवा के लिए इसे कॉन्फ़िगर करने का तरीका यहां बताया गया है।
कॉन्फ़िगरेशन (application.yml)
resilience4j.retry:
instances:
billingService:
maxAttempts: 3
waitDuration: 100ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2.0
enableRandomizedWait: true
randomizedWaitFactor: 0.5
retryExceptions:
- org.springframework.web.client.ResourceAccessException
- io.netty.channel.ConnectTimeoutException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException # E.g., 400 Bad Request shouldn't be retried
कोड कार्यान्वयन
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class PaymentProcessor {
private final RestTemplate restTemplate;
public PaymentProcessor(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// Apply the configured retry policy with a fallback method
@Retry(name = "billingService", fallbackMethod = "billingFallback")
public String chargeUser(BillingRequest request) {
return restTemplate.postForObject("http://billing-service/charge", request, String.class);
}
// Executed when all retry attempts fail
public String billingFallback(BillingRequest request, Throwable throwable) {
return "Billing service is currently unavailable. Your transaction will be queued.";
}
}
2. जाओ (गोलंग)
गो में, हम मानक लाइब्रेरी टाइमर का उपयोग करके घातीय बैकऑफ़ और यादृच्छिक जिटर की विशेषता वाला एक सुंदर रिट्री रनर लिख सकते हैं।
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
// RetryConfig holds the policies for our retry attempts
type RetryConfig struct {
MaxAttempts int
MinBackoff time.Duration
MaxBackoff time.Duration
}
// Execute runs the operation using exponential backoff with full jitter
func Execute(ctx context.Context, config RetryConfig, operation func() error) error {
var err error
for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
err = operation()
if err == nil {
return nil // Success!
}
if attempt == config.MaxAttempts {
break
}
// Calculate exponential backoff
// wait = min(MaxBackoff, MinBackoff * 2^(attempt-1))
backoff := config.MinBackoff * (1 << (attempt - 1))
if backoff > config.MaxBackoff || backoff <= 0 {
backoff = config.MaxBackoff
}
// Apply Full Jitter: wait randomly between 0 and backoff
jitter := time.Duration(rand.Int63n(int64(backoff)))
fmt.Printf("[Attempt %d/%d] Failed: %v. Retrying in %v...\n", attempt, config.MaxAttempts, err, jitter)
select {
case <-time.After(jitter):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("operation failed after %d attempts: %w", config.MaxAttempts, err)
}
func main() {
config := RetryConfig{
MaxAttempts: 4,
MinBackoff: 100 * time.Millisecond,
MaxBackoff: 2000 * time.Millisecond,
}
// Mock function that fails 3 times and succeeds on the 4th
attempts := 0
mockAPI := func() error {
attempts++
if attempts < 4 {
return errors.New("network timeout (503)")
}
return nil
}
ctx := context.Background()
err := Execute(ctx, config, mockAPI)
if err != nil {
fmt.Printf("Final Outcome: %v\n", err)
} else {
fmt.Println("Final Outcome: Successfully connected on attempt", attempts)
}
}
पुनः प्रयास करें बनाम सर्किट ब्रेकर: कब किसका उपयोग करें?
डेवलपर्स अक्सर पुनः प्रयास पैटर्न को सर्किट ब्रेकर पैटर्न के साथ भ्रमित करते हैं। हालाँकि दोनों का लक्ष्य सिस्टम लचीलेपन में सुधार करना है, वे मौलिक रूप से विभिन्न प्रकार की विफलताओं को संभालते हैं:
| मीट्रिक | पुनः प्रयास करें पैटर्न | सर्किट ब्रेकर पैटर्न |
|---|---|---|
| प्राथमिक लक्ष्य | क्षणिक (अल्पकालिक) विफलताओं से स्वचालित रूप से उबर जाता है। | लगातार (दीर्घकालिक) विफलताओं को कॉल करने वाले को रोकने से रोकता है। |
| रणनीति | तुरंत या थोड़े बैकऑफ़ इंतज़ार के बाद पुनः प्रयास करें। | लक्ष्य सेवा लागू किए बिना तुरंत विफल हो जाएं। |
| डाउनस्ट्रीम प्रभाव | डाउनस्ट्रीम सेवा पर अस्थायी रूप से लोड बढ़ जाता है। | डाउनस्ट्रीम सेवा को ट्रैफ़िक से बचाता है, जिससे उसे पुनर्प्राप्त करने की अनुमति मिलती है। |
| विशिष्ट ट्रिगर | संक्षिप्त नेटवर्क डिस्कनेक्ट, डेटाबेस टाइमआउट, सॉकेट ड्रॉप। | डाउनस्ट्रीम सेवा पूरी तरह से बंद है, लगातार 500 या टाइमआउट लौटा रही है। |
पावर कपल: दोनों का संयोजन
उत्पादन प्रणालियों में, इन दो पैटर्न को एक साथ उपयोग करने के लिए डिज़ाइन किया गया है।
जब आप डाउनस्ट्रीम अनुरोध करते हैं, तो इसे पहले सर्किट ब्रेकर से गुजरना चाहिए, और फिर पुनः प्रयास करें रैपर से गुजरना चाहिए।
यदि कोई क्षणिक गड़बड़ी होती है, तो पुनः प्रयास तंत्र उसे रोकता है और पुनः प्रयास करता है। हालाँकि, यदि डाउनस्ट्रीम सेवा बंद है, तो विफलताएँ ढेर हो जाएँगी। सर्किट ब्रेकर लगातार विफलताओं का पता लगाता है, “ट्रिप्स” खुलता है, और भविष्य की सभी कॉलों को ब्लॉक कर देता है।
अब, जब आप सेवा को कॉल करने का प्रयास करते हैं, तो सर्किट ब्रेकर तुरंत तेजी से विफल हो जाता है, रिट्री लूप को पूरी तरह से बायपास कर देता है और आपके कंप्यूटिंग संसाधनों को बचा लेता है।
पुनः प्रयास पैटर्न के लिए सर्वोत्तम अभ्यास
- केवल क्षणिक त्रुटियों का पुन: प्रयास करें: HTTP स्थिति कोड का निरीक्षण करें।
503 Service Unavailable,429 Too Many Requests(यदि मौजूद है तोRetry-Afterहेडर का सम्मान करते हुए), और नेटवर्क टाइमआउट का पुन: प्रयास करें।400 Bad Request,401 Unauthorized, या404 Not Foundका पुनः प्रयास नहीं करें—ये पुनः प्रयास करने पर कभी सफल नहीं होंगे। - जिटर लागू करें: कभी भी यादृच्छिक जिटर लागू किए बिना एक निश्चित पुनः प्रयास टाइमर का उपयोग न करें।
- अधिकतम प्रयास सीमित करें: एक उचित सीमा (आमतौर पर 3 से 5 प्रयास) के बाद पुनः प्रयास करना बंद करें। असीमित पुनर्प्रयास संसाधनों का उपभोग करते हैं और विलंबता को कम करते हैं।
- कैस्केडिंग रिट्रीट से सावधान रहें: यदि सेवा ए सेवा बी को कॉल करती है (जो 3 बार रिट्रीट करती है), और सेवा बी सेवा सी को कॉल करती है (जो 3 बार रिट्रीट करती है), सी में विफलता के परिणामस्वरूप $3 \ गुना 3 = 9$ कैस्केडिंग कॉल हो सकती है। केवल उन्हीं सीमाओं पर पुनः प्रयास लागू करें जहां वे सबसे अधिक अर्थपूर्ण हों।
- हमेशा सख्त टाइमआउट सेट करें: सुनिश्चित करें कि आपका नेटवर्क टाइमआउट आपके बैकऑफ़ अवधि से कम है, इसलिए थ्रेड्स को अनिश्चित काल तक नहीं रखा जाता है।
निष्कर्ष
पुनःप्रयास पैटर्न माइक्रोसर्विस आर्किटेक्चर में नेटवर्क की अविश्वसनीयता के खिलाफ रक्षा की एक शक्तिशाली पहली पंक्ति है। जब एक्सपोनेंशियल बैकऑफ़, जिटर, और सर्किट ब्रेकर के साथ जोड़ा जाता है, तो आप अपने सिस्टम को कैस्केडिंग आउटेज से बचा सकते हैं और अपने उपयोगकर्ताओं के लिए एक सहज, स्व-उपचार अनुभव बना सकते हैं।