পাঠ ২৭ · ৫১-এর মধ্যে · মডিউল ৭
Home / Courses / System Design / সার্কিট ব্রেকার ও রিট্রাই

সার্কিট ব্রেকার ও রিট্রাই প্যাটার্ন

Circuit breaker & retry patterns
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Cascading failure ঠিক কীভাবে একটি স্বাস্থ্যবান সার্ভিসকেও নিচে টেনে নামায়
  • Circuit Breaker-এর তিনটি স্টেট ও তাদের মধ্যে ট্রানজিশনের নিয়ম
  • Exponential backoff ও jitter দিয়ে retry storm এড়ানো
  • Python দিয়ে একটি সম্পূর্ণ কার্যকর Circuit Breaker ক্লাস ইমপ্লিমেন্ট করা এবং একটি flaky সার্ভিসের বিপরীতে টেস্ট করা

১ · Cascading Failure — একটি সুস্থ সার্ভিস কীভাবে ভেঙে পড়ে

মাইক্রোসার্ভিস আর্কিটেকচারে (L25) সার্ভিস A প্রায়ই সার্ভিস B-কে নেটওয়ার্কের মাধ্যমে কল করে। ধরুন সার্ভিস B হঠাৎ ধীরগতির হয়ে গেল বা ডাউন হয়ে গেল। এখন সার্ভিস A-এর প্রতিটি রিকোয়েস্ট B-এর জন্য অপেক্ষা করতে থাকবে — থ্রেড, কানেকশন-পুল স্লট, মেমরি সবই আটকে থাকবে। কিছুক্ষণের মধ্যেই A নিজেও তার সব রিসোর্স হারিয়ে ফেলবে এবং A-কে যারা কল করে তাদের কাছেও ধীর/অকার্যকর দেখাবে। এভাবে একটি সার্ভিসের সমস্যা পুরো সিস্টেমে ছড়িয়ে পড়ে — cascading failureCascading Failureএকটি সার্ভিসের ব্যর্থতা বা ধীরগতি ধীরে ধীরে তাকে কল করা প্রতিটি আপস্ট্রিম সার্ভিসকেও ব্যর্থ করে দেয় — সমস্যাটি শৃঙ্খলের মতো ছড়িয়ে পড়ে।।

মূল সমস্যা

সার্ভিস A কোডের দিক থেকে সম্পূর্ণ সুস্থ — কোনো বাগ নেই। তবু সে ব্যর্থ হয়, কারণ সে একটি অসুস্থ ডিপেন্ডেন্সিকে বারবার কল করে অপেক্ষা করছে। সমাধান হলো A-কে শেখানো — "যদি B বারবার ব্যর্থ হয়, তাহলে আর কল করো না, দ্রুত ব্যর্থ হও (fail fast) এবং B-কে সেরে ওঠার সময় দাও।"

২ · Circuit Breaker — তিনটি স্টেট

Circuit BreakerCircuit Breakerএকটি ওয়াপার যা একটি ডিপেন্ডেন্সিতে করা কলের ব্যর্থতার হার ট্র্যাক করে এবং ব্যর্থতা বেশি হলে সাময়িকভাবে কল করা বন্ধ করে দেয়, যাতে ব্যর্থ সার্ভিসটি সেরে ওঠার সুযোগ পায় ও কলকারী নিজে বেঁচে থাকে। ঘরের বৈদ্যুতিক সার্কিট ব্রেকারের মতোই কাজ করে — বিপদ দেখলে সার্কিট "কেটে" দেয়। এর তিনটি স্টেট আছে:

Closed (স্বাভাবিক)
সব কল স্বাভাবিকভাবে B-তে যায়। প্রতিটি ব্যর্থতা গণনা করা হয়; একটি সফল কলে গণনা রিসেট হয়।
Open (বন্ধ)
ব্যর্থতা একটি থ্রেশহোল্ড (যেমন ৩) পার হলে ব্রেকার "খুলে" যায় — B-কে আর কল না করে সাথে সাথেই ব্যর্থতা রিটার্ন করে (fail fast)।
Half-Open (পরীক্ষামূলক)
একটি recovery timeout শেষ হলে একটি মাত্র টেস্ট কল যেতে দেয় — সফল হলে Closed-এ ফিরে, ব্যর্থ হলে আবার Open হয়ে যায়।
Closed স্বাভাবিক Open দ্রুত ব্যর্থ Half-Open টেস্ট কল ৩ বার ব্যর্থ timeout শেষ টেস্ট কল সফল টেস্ট কল ব্যর্থ
Closed → Open → Half-Open → Closed চক্র। Half-Open-এ টেস্ট কল ব্যর্থ হলে আবার Open-এ ফিরে যায়।

৩ · Retry Pattern — Exponential Backoff ও Jitter

নেটওয়ার্ক কল প্রায়ই ক্ষণস্থায়ী কারণে ব্যর্থ হয় (সাময়িক ভিড়, একটি প্যাকেট হারানো) — এক্ষেত্রে সাথে সাথে আবার চেষ্টা করলে সফল হওয়ার সম্ভাবনা থাকে। কিন্তু সরল, তাৎক্ষণিক রিট্রাই বিপজ্জনক হতে পারে — যদি হাজার হাজার ক্লায়েন্ট একসাথে একটি ওভারলোডেড সার্ভিসে সাথে সাথে রিট্রাই করে, তাহলে সেই retry stormRetry Stormঅনেক ক্লায়েন্ট একই সময়ে একটি সমস্যাগ্রস্ত সার্ভিসে বারবার তাৎক্ষণিক রিট্রাই পাঠালে সেই সার্ভিসের ওপর লোড আরও বেড়ে যায় — সেরে ওঠার বদলে সমস্যা আরও গভীর হয়। সার্ভিসটিকে সেরে ওঠার বদলে আরও গভীর সংকটে ফেলে দেয়। সমাধান দুটি কৌশলের মিশ্রণ:

Exponential Backoff
প্রতিটি পরের রিট্রাইয়ের আগে অপেক্ষার সময় দ্বিগুণ করা হয় — ১s, ২s, ৪s, ৮s... সার্ভিসকে সেরে ওঠার সময় দেয়।
Jitter
অপেক্ষার সময়ে সামান্য এলোমেলো ভিন্নতা যোগ করা হয়, যাতে হাজার হাজার ক্লায়েন্ট একসাথে (synchronized wave) রিট্রাই না করে।
Circuit Breaker ও Retry একসাথে ব্যবহার করা হয় — Retry ক্ষণস্থায়ী, বিচ্ছিন্ন ব্যর্থতা সামলায়; Circuit Breaker দীর্ঘস্থায়ী, ধারাবাহিক ব্যর্থতায় দ্রুত হাল ছেড়ে দিয়ে পুরো সিস্টেমকে বাঁচায়। একটি Open ব্রেকারের পেছনে রিট্রাই করা অর্থহীন — তাই বাস্তব সিস্টেমে সাধারণত Retry-এর বাইরের স্তরে Circuit Breaker বসানো হয়।

৪ · Python দিয়ে Circuit Breaker ইমপ্লিমেন্ট করা

নিচের কোডে একটি CircuitBreaker ক্লাস আছে যা পরপর ব্যর্থতা গণনা করে, থ্রেশহোল্ড (৩) পার হলে Open হয়ে যায়, এবং একটি নির্দিষ্ট সিমুলেটেড সময় (timestamp) পার হওয়ার পর Half-Open হয়ে একটি টেস্ট কল অনুমতি দেয়। একটি flaky_service ফাংশন আছে যা প্রথম ৩ বার ব্যর্থ হয়, তারপর থেকে সফল হয় — বাস্তব সময়ের বদলে আমরা একটি সাজানো (deterministic) সময়ের তালিকা ব্যবহার করছি যাতে রেজাল্ট প্রতিবার একই থাকে।

Python
class CircuitBreaker:
    def __init__(self, failure_threshold=3, recovery_timeout=5):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.state = "CLOSED"
        self.opened_at = None

    def call(self, func, current_time):
        # Open অবস্থায়, timeout না পেরোনো পর্যন্ত সার্ভিসকে একদমই কল করা হয় না
        if self.state == "OPEN":
            if current_time - self.opened_at >= self.recovery_timeout:
                self.state = "HALF_OPEN"
            else:
                return "REJECTED (fast-fail, ব্রেকার OPEN)"

        try:
            func()
        except Exception:
            self.failure_count += 1
            if self.state == "HALF_OPEN" or self.failure_count >= self.failure_threshold:
                self.state = "OPEN"
                self.opened_at = current_time
            return f"FAILURE (state={self.state}, failures={self.failure_count})"
        else:
            self.failure_count = 0
            self.state = "CLOSED"
            return "SUCCESS (state=CLOSED)"


call_counter = {"n": 0}

def flaky_service():
    # প্রথম ৩ বার ব্যর্থ হয়, এরপর থেকে সবসময় সফল
    call_counter["n"] += 1
    if call_counter["n"] <= 3:
        raise RuntimeError("service unavailable")


breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=5)
timeline = [0, 1, 2, 3, 10, 11]   # সিমুলেটেড সময় (সেকেন্ড)

for t in timeline:
    result = breaker.call(flaky_service, t)
    print(f"t={t:>2}  ->  {result}")

print()
print("সার্ভিসকে মোট কতবার আসলে কল করা হয়েছে:", call_counter["n"], "(t=3 সময় ব্রেকার OPEN থাকায় কল হয়নি)")

    
কোডটি কী প্রমাণ করছে

t=0,1,2-তে সার্ভিস ব্যর্থ হয় এবং t=2-এর পর ব্যর্থতা থ্রেশহোল্ড (৩) পার হয়ে ব্রেকার OPEN হয়ে যায়। t=3-এ ব্রেকার এখনও OPEN (মাত্র ১ সেকেন্ড পার হয়েছে, recovery_timeout=৫) — তাই flaky_service একদমই কল হয় না, সাথে সাথে REJECTED রিটার্ন হয়। t=10-এ ৮ সেকেন্ড পার হয়ে গেছে বলে ব্রেকার HALF_OPEN-এ যায় ও একটি টেস্ট কল অনুমতি দেয় — এতক্ষণে flaky_service-এর ৪র্থ কল, যা সফল হয় — ব্রেকার আবার CLOSED-এ ফিরে আসে। t=11-এ স্বাভাবিকভাবে সফল হয়। সার্ভিস মোট মাত্র ৫ বার কল হয়েছে, ৬ বার নয় — কারণ t=3-এর কলটি ব্রেকার আটকে দিয়েছিল।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ Circuit Breaker যখন Open থাকে, তখন কলকারী সার্ভিস A ব্যবহারকারীকে কী দেখাবে — একটি এরর, নাকি অন্য কিছু?

বাস্তবে সাধারণত এরর দেখানো এড়িয়ে fallback ব্যবহার করা ভালো — যেমন ক্যাশ করা পুরনো ডেটা (L19) দেখানো, একটি ডিফল্ট মান রিটার্ন করা, অথবা "এই ফিচারটি সাময়িকভাবে অনুপলব্ধ" বার্তা দেখানো। মূল কথা হলো — ব্রেকার Open থাকা অবস্থায় ব্যর্থ ডিপেন্ডেন্সিকে বারবার কল না করে দ্রুত একটি বিকল্প রেসপন্স দেওয়া, যাতে ব্যবহারকারী দীর্ঘ সময় অপেক্ষা না করে এবং A নিজের রিসোর্স নষ্ট না করে।

প্র ০২ Retry ছাড়া শুধু Circuit Breaker, অথবা Circuit Breaker ছাড়া শুধু Retry ব্যবহার করলে কী সমস্যা হতে পারে?

শুধু Retry (ব্রেকার ছাড়া) থাকলে একটি দীর্ঘস্থায়ী ব্যর্থতায় প্রতিটি ক্লায়েন্ট বারবার রিট্রাই করতে থাকবে — এটাই retry storm তৈরি করে এবং ভুক্তভোগী সার্ভিসকে সেরে উঠতেই দেয় না। শুধু Circuit Breaker (রিট্রাই ছাড়া) থাকলে একটি একবারের, ক্ষণস্থায়ী নেটওয়ার্ক গ্লিচেও সাথে সাথে ব্যর্থতা গণনা বাড়বে এবং অপ্রয়োজনে ব্রেকার Open হয়ে যেতে পারে, যদিও একটি সাধারণ রিট্রাইতেই কাজ হয়ে যেত। দুটো একসাথে থাকলে রিট্রাই ছোট সমস্যা সামলায়, আর ব্রেকার বড় সমস্যায় সীমা টানে।

প্র ০৩ Half-Open স্টেটে ব্রেকার কেন একবারে একাধিক টেস্ট কল না দিয়ে মাত্র একটি (বা খুব কম) কল অনুমতি দেয়?

যদি Half-Open অবস্থায় একসাথে অনেক কল ছেড়ে দেওয়া হয় এবং সার্ভিস B এখনও পুরোপুরি সেরে না ওঠে, তাহলে সেই একগাদা টেস্ট কলই আবার B-কে ওভারলোড করে দিতে পারে — যা ঠিক সেই সমস্যাই তৈরি করবে যা ব্রেকার প্রতিরোধ করতে চেয়েছিল। একটি বা খুব কম সংখ্যক টেস্ট কল দিয়ে "নিরাপদে যাচাই" করাই লক্ষ্য — সফল হলে ধীরে ধীরে বেশি ট্রাফিক ছেড়ে দেওয়া হয়, ব্যর্থ হলে সাথে সাথে আবার Open-এ ফিরে যাওয়া হয়।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে failure_threshold ২-তে এবং recovery_timeout ১০-এ বদলে দিলে timeline-এর কোন কোন t-এ ব্রেকারের স্টেট বদলাবে তা হাতে হিসাব করুন, তারপর কোডে বদলে মিলিয়ে দেখুন।

    threshold=২ হলে t=0,1-এই দুটি ব্যর্থতাতেই (২ বার) ব্রেকার t=1-এ OPEN হয়ে যাবে (আগে t=2-এ হতো)। t=2,3 উভয়েই REJECTED হবে যেহেতু recovery_timeout=১০ (৯ সেকেন্ড অতিক্রান্ত না হওয়া পর্যন্ত)। t=10-এ মাত্র ৯ সেকেন্ড পেরিয়েছে (opened_at=1) — এখনও ১০ পূর্ণ হয়নি বলে REJECTED থাকবে। t=11-এ ১০ সেকেন্ড পূর্ণ হয়ে HALF_OPEN হবে ও টেস্ট কল (৩য়, যা সফল — কারণ flaky_service ৩ বারের পর সফল হয়) সফল হয়ে CLOSED-এ ফিরবে।

  2. ডিজাইন করুন: একটি পেমেন্ট সার্ভিসের জন্য Retry Pattern ডিজাইন করুন — সর্বোচ্চ কতবার রিট্রাই করবেন এবং কেন? পেমেন্টের ক্ষেত্রে অন্ধভাবে রিট্রাই করা কেন বিশেষভাবে ঝুঁকিপূর্ণ (L38-এর ধারণার সাথে সংযোগ করুন)?

    সাধারণত ৩-৫ বার রিট্রাই যথেষ্ট, exponential backoff + jitter সহ, তারপর হাল ছেড়ে ব্যবহারকারীকে জানানো উচিত। পেমেন্টে অন্ধ রিট্রাই বিশেষভাবে ঝুঁকিপূর্ণ কারণ প্রথম রিকোয়েস্টটি হয়তো আসলে সফল হয়েছিল কিন্তু রেসপন্সটি হারিয়ে গেছে — একটি নতুন রিট্রাই ছাড়া idempotency key (L38) ব্যবহার না করলে গ্রাহকের টাকা দুইবার কাটা যেতে পারে। তাই পেমেন্ট রিট্রাইয়ে সবসময় একটি idempotency key পাঠানো উচিত যাতে সার্ভার ডুপ্লিকেট রিকোয়েস্ট চিনে একবারই প্রসেস করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
API গেটওয়ে ও সার্ভিস ডিসকভারি