Python Programming

تسلط بر Asyncio در پایتون: پیاده‌سازی محدودکننده نرخ و کلید مدار در محیط تولید

برنامه‌نویسی ناهمگام در پایتون، با هدایت کتابخانه asyncio، نحوه ساخت برنامه‌های با عملکرد بالا و وابسته به I/O را متحول کرده است. با این حال، با همزمانی زیاد، مسئولیت زیادی نیز وجود دارد. وقتی صدها کوروتین همزمان درخواست ارسال می‌کنند، خطر غرق کردن خدمات پایین‌دستی، برخورد با محدودیت‌های نرخ API یا کرش کردن برنامه خودتان به دلیل شکست‌های زنجیره‌ای به طور نمایی افزایش می‌یابد. در این راهنما، ما بررسی خواهیم کرد که چگونه دو الگوی تاب‌آوری حیاتی—محدودکننده نرخ (Rate Limiting) و کلید مدار (Circuit Breakers)—را با استفاده از اکوسیستم ناهمگام پایتون پیاده‌سازی کنیم.

چالش همزمانی ناهمگام

در کد همگام (سینکرون)، یک درخواست کند، رشته (Thread) را مسدود می‌کند. در یک زمینه ناهمگام، می‌توانید هزاران وظیفه همزمان ایجاد کنید. اگر این وظایف با یک API خارجی تعامل داشته باشند که محدودیتی معادل ۶۰ درخواست در دقیقه را اعمال می‌کند، برنامه شما بلافاصله خطاهای HTTP 429 را دریافت خواهد کرد که منجر به اتلاف منابع و احتمالاً آسیب به اعتبار نزد ارائه‌دهنده خدمات می‌شود. علاوه بر این، اگر یک خدمات پایین‌دستی از دسترس خارج شود، انتظار فعال (Busy-waiting) یا تلاش مجدد بدون تأخیر می‌تواند منابع سرور شما را مستهلک کند و منجر به حمله انکار سرویس (DoS) برای کاربران خودتان شود.

بنابراین، تاب‌آوری تنها یک ویژگی نیست؛ بلکه یک الزام بنیادی برای برنامه‌های ناهمگام در سطح تولید است. ما این چالش‌ها را با ساخت یک RateLimiter و یک CircuitBreaker قابل استفاده مجدد از صفر، و اجتناب از وابستگی‌های سنگین تا حد امکان، حل خواهیم کرد.

پیاده‌سازی یک محدودکننده نرخ ناهمگام

یک محدودکننده نرخ، نرخ فراخوانی توابع را کنترل می‌کند. برای برنامه‌های ناهمگام، ما به مکانیزمی نیاز داریم که به کوروتین‌ها اجازه دهد تا وقتی به حد مجاز رسیدند، به طور کارآمد منتظر بمانند، به جای اینکه در یک حلقه بی‌پایان بچرخند. الگوریتم سطل توکن (Token Bucket) یک انتخاب محبوب است، اما برای سادگی و کارایی در پایتون، یک رویکرد مبتنی بر سمافور (Semaphore) برای محدودیت‌های پنجره ثابت یا مبتنی بر توکن به خوبی کار می‌کند.

در زیر یک پیاده‌سازی قوی از یک محدودکننده نرخ ناهمگام آورده شده است که از یک سمافور برای اعمال محدودیت‌های همزمانی و یک مکانیسم خواب (Sleep) برای اعمال محدودیت‌های مبتنی بر زمان استفاده می‌کند.

import asyncio
import time

class AsyncRateLimiter:
    def __init__(self, rate: float, per: float):
        """
        :param rate: تعداد فراخوانی‌های مجاز.
        :param per: دوره زمانی بر حسب ثانیه.
        """
        self.rate = rate
        self.per = per
        self.semaphore = asyncio.Semaphore(rate)
        self.last_call_time = 0.0
        self.lock = asyncio.Lock()

    async def acquire(self):
        # ابتدا، برای رعایت محدودیت‌های درخواست همزمان منتظر بمانید
        await self.semaphore.acquire()
        
        async with self.lock:
            elapsed = time.time() - self.last_call_time
            if elapsed < self.per:
                wait_time = self.per - elapsed
                await asyncio.sleep(wait_time)
            self.last_call_time = time.time()

    def __aenter__(self):
        return self

    def __aexit__(self, exc_type, exc_val, exc_tb):
        self.semaphore.release()
        return False

# مثال استفاده
async def fetch_data(session, url, limiter):
    async with limiter:
        # شبیه‌سازی یک درخواست API
        print(f"Fetching {url}")
        # await session.get(url)
        await asyncio.sleep(0.5)
        return "Data"

async def main():
    limiter = AsyncRateLimiter(rate=2, per=1.0) # 2 درخواست در ثانیه
    tasks = [fetch_data(None, "https://api.example.com", limiter) for _ in range(10)]
    await asyncio.gather(*tasks)

# asyncio.run(main())

در این مثال، semaphore تضمین می‌کند که بیش از rate وظیفه در طول دوره زمانی به طور همزمان اجرا نشوند. قفل و بررسی زمان‌بند تضمین می‌کنند که نرخ کلی رعایت شود. این امر از بن شدن برنامه شما توسط APIهای شخص ثالث جلوگیری می‌کند.

الگوی کلید مدار (Circuit Breaker)

در حالی که محدودکننده نرخ از خدمات پایین‌دستی محافظت می‌کند، الگوی کلید مدار از برنامه شما در برابر شکست‌های پایین‌دستی محافظت می‌کند. اگر یک خدمات به طور مداوم شکست بخورد، کلید مدار "باز" می‌شود، سریعاً شکست می‌خورد بدون اینکه تلاش برای فراخوانی انجام دهد، و به خدمات زمان می‌دهد تا بازیابی شود.

ما یک کلید مدار ساده با سه حالت پیاده‌سازی خواهیم کرد: بسته (عملیات عادی)، باز (شکست سریع) و نیمه‌باز (آزمایش بازیابی).

import asyncio
from enum import Enum
import time

class CircuitState(Enum):
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.state = CircuitState.CLOSED
        self.last_failure_time = 0

    async def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                print("Circuit breaker half-open, testing recovery...")
            else:
                raise Exception("Circuit breaker is OPEN")

        try:
            result = await func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

    def _on_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN
            print("Circuit breaker opened due to failures.")

با ادغام این الگو، برنامه شما قابلیت‌های خودترمیم‌سازی را کسب می‌کند. وقتی خدمات پایین‌دستی بازیابی می‌شود، حالت HALF_OPEN اجازه می‌دهد یک درخواست آزمایشی واحد عبور کند. اگر موفق شود، مدار بسته می‌شود؛ اگر شکست بخورد، دوباره باز می‌شود و از فشار بیشتر بر خدمات در حال مشکل جلوگیری می‌کند.

ترکیب الگوها برای تاب‌آوری در محیط تولید

در یک محیط تولید، این الگوها اغلب به صورت هماهنگ کار می‌کنند. یک الگوی معماری رایج شامل پیچیدن کلاینت HTTP خود با هر دو محدودکننده نرخ و کلید مدار است. این تضمین می‌کند که شما محدودیت‌های ارائه‌دهنده API را رعایت می‌کنید و همزمان منطق برنامه خود را از فروپاشی کامل محافظت می‌کنید.

هنگام ترکیب آن‌ها، همیشه ابتدا محدودکننده نرخ را اعمال کنید. این امر تعداد درخواست‌هایی را که تلاش می‌کنند از طریق کلید مدار عبور کنند، به حداقل می‌رساند و بار روی سیستم را کاهش می‌دهد. کلید مدار سپس به عنوان یک نگهبان نهایی در برابر شکست‌های پایدار عمل می‌کند. این استراتژی دفاع لایه‌ای برای ساخت میکروسرویس‌ها و پایپ‌لاین‌های داده با ظرفیت بالا در پایتون ضروری است.

نتیجه‌گیری

تسلط بر asyncio پایتون فراتر از درک صرف سینتکس است؛ این نیازمند قدردانی عمیق از تاب‌آوری سیستم است. با پیاده‌سازی محدودکننده‌های نرخ و کلیدهای مدار سفارشی، شما تضمین می‌کنید که برنامه‌های شما نه تنها سریع، بلکه قوی، قابل اعتماد و محترمانه نسبت به زیرساختی که به آن وابسته است باشند. این الگوها تجملات اختیاری نیستند—آن‌ها الزامات هر برنامه پایتون جدی در سطح تولید هستند. شروع به ادغام این شیوه‌ها در پروژه ناهمگام بعدی خود کنید تا نرم‌افزاری بسازید که در برابر فشارهای دنیای واقعی مقاومت کند.

Share: