Python Programming

الگوهای هم‌زمانی پایتون برای میکروسرویس‌های وابسته به I/O در مقابل پردازش‌محور: یک مطالعه موردی تولیدی

در منظره میکروسرویس‌های مدرن، ویژگی‌های عملکردی پایتون اغلب تصمیمات معماری را تعیین می‌کنند. توسعه‌دهندگان اغلب با همان معضل روبرو می‌شوند: وقتی یک سرویس تحت بار کاری کند می‌شود، چگونه آن را مقیاس‌پذیر کنیم؟ پاسخ نه در خرید سخت‌افزار بیشتر، بلکه در درک تمایز بنیادین بین وظایف وابسته به I/O و وظایف پردازش‌محور و انتخاب الگوی هم‌زمانی مناسب نهفته است.

این پست به عمق یک مطالعه موردی تولیدی می‌پردازد و پیاده‌سازی یک دروازه API با کارایی بالا (وابسته به I/O) را در مقایسه با یک سرویس تبدیل داده (پردازش‌محور) بررسی می‌کند. ما بررسی می‌کنیم که چه زمانی باید از asyncio، threading و multiprocessing استفاده کرد تا اطمینان حاصل شود که میکروسرویس‌های پایتون شما هم مقاوم و هم کارآمد هستند.

تمایز بنیادین: I/O در مقابل CPU

برای معماری صحیح، ابتدا باید بار کاری را دسته‌بندی کرد. وظایف وابسته به I/O بیشتر زمان خود را صرف انتظار برای پاسخ منابع خارجی—مانند پایگاه‌های داده، APIها یا سیستم‌های فایل—می‌کنند. در طول این زمان‌های انتظار، CPU بیکار می‌ماند. وظایف پردازش‌محور (CPU-bound)، در مقابل، شامل محاسبات سنگین مانند پردازش تصویر، رمزنگاری یا تجمیع داده‌های پیچیده هستند که CPU را درگیر نگه می‌دارند.

قفل تفسیرگر سراسری (GIL) پایتون محدودیت اصلی است. GIL تضمین می‌کند که تنها یک رشته در هر زمان می‌تواند بایت‌کد پایتون را اجرا کند. این بدان معناست که threading نمی‌تواند وظایف پردازش‌محور را تسریع کند، اما برای سناریوهای وابسته به I/O که در آن GIL در حین فراخوانی‌های سیستمی آزاد می‌شود، کاملاً مناسب است.

مطالعه موردی ۱: دروازه وابسته به I/O

تصور کنید یک میکروسرویس به عنوان یک دروازه API عمل می‌کند. وظیفه اصلی آن دریافت درخواست‌ها، تجمیع داده‌ها از پنج سرویس بالادستی مختلف و بازگرداندن یک پاسخ یکپارچه است. این یک مشکل کلاسیک وابسته به I/O است. اگر از یک رشته واحد استفاده می‌کردیم، سرویس در حین انتظار برای کوئری‌های پایگاه داده مسدود می‌شد و منجر به تاخیر (latency) ضعیف می‌شد.

در یک محیط تولیدی، asyncio استاندارد طلایی در اینجا است. این کتابخانه به ما اجازه می‌دهد هزاران اتصال هم‌زمان را با حداقل هزینه حافظه با استفاده از یک حلقه رویداد مدیریت کنیم.

import asyncio
import aiohttp

async def fetch_data(session, url):
    async with session.get(url) as response:
        return await response.json()

async def process_request(request_id):
    urls = [
        "http://api.users-service",
        "http://api.orders-service",
        "http://api.inventory-service"
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_data(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    return {"request_id": request_id, "data": results}

async def main():
    tasks = [process_request(i) for i in range(100)]
    await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())

توجه کنید که چگونه asyncio.gather به ما اجازه می‌دهد چندین عملیات I/O را به صورت هم‌زمان اجرا کنیم. در حالی که اولین درخواست در انتظار شبکه است، حلقه رویداد زمینه را به درخواست دوم تغییر می‌دهد، که تضمین می‌کند CPU در طول انتظارهای شبکه هرگز بیکار نمی‌ماند.

مطالعه موردی ۲: پردازشگر داده‌های پردازش‌محور

این را با سرویسی مقایسه کنید که فشرده‌سازی سنگین تصویر را انجام می‌دهد یا مدل‌های استنتاج یادگیری ماشین پیچیده را روی داده‌های ورودی اجرا می‌کند. در اینجا، آزاد کردن GIL کافی نیست زیرا رشته‌ها در حال محاسبه هستند، نه انتظار. استفاده از threading یا حتی asyncio در اینجا کارایی را بهبود نمی‌بخشد و ممکن است به دلیل هزینه تغییر زمینه، عملکرد را تخریب کند.

برای وظایف پردازش‌محور، باید کاملاً از GIL با استفاده از چندین فرآیند عبور کنیم. concurrent.futures.ProcessPoolExecutor یا asyncio.create_subprocess_exec استراتژی‌های قابل قبولی هستند. مثال زیر یک رویکرد آماده برای تولید را با استفاده از پشته‌های فرآیندی برای برون‌سپاری محاسبات سنگین نشان می‌دهد.

import concurrent.futures
import os
import hashlib

def heavy_computation(data):
    # شبیه‌سازی کارهای سنگین پردازشی
    result = data
    for _ in range(100000):
        result += 1
    return hashlib.sha256(str(result).encode()).hexdigest()

def cpu_bound_service(data_batch):
    # برون‌سپاری کارهای CPU به فرآیندهای جداگانه سیستم عامل
    with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
        results = list(executor.map(heavy_computation, data_batch))
    return results

# استفاده در زمینه هم‌زمان (async)
async def handle_batch(batch_id):
    data = [os.urandom(1024) for _ in range(10)]
    # اجرای کارهای پردازش‌محور در پشته رشته/فرآیند برای جلوگیری از مسدود شدن حلقه رویداد
    loop = asyncio.get_event_loop()
    with concurrent.futures.ProcessPoolExecutor() as pool:
        results = await loop.run_in_executor(pool, cpu_bound_service, data)
    return results

با استفاده از run_in_executor همراه با ProcessPoolExecutor، اطمینان حاصل می‌کنیم که حلقه رویداد آزاد می‌ماند تا درخواست‌های HTTP ورودی جدید را مدیریت کند، در حالی که کارهای سنگین در فرآیندهای موازی انجام می‌شود.

ملاحظات تولیدی

هنگام استقرار این الگوها، برنامه خود را به دقت پایش کنید. برای asyncio، مراقب فراخوانی‌های مسدودکننده (blocking) درون توابع هم‌زمان باشید که می‌تواند کل حلقه رویداد را متوقف کند. برای multiprocessing، به هزینه serialisation (توالی‌سازی) هنگام انتقال ساختارهای داده بزرگ بین فرآیند اصلی و فرآیندهای کارگر توجه داشته باشید.

نتیجه‌گیری

انتخاب الگوی هم‌زمانی مناسب برای میکروسرویس‌های پایتون حیاتی است. برای بارهای کاری وابسته به I/O، asyncio را بپذیرید تا کارایی را با حداقل مصرف منابع به حداکثر برسانید. برای وظایف پردازش‌محور، از multiprocessing برای عبور از GIL و استفاده از تمام هسته‌های CPU موجود بهره ببرید. با درک این الگوها و اعمال آن‌ها به معماری سرویس خاص خود، می‌توانید سیستم‌های مقیاس‌پذیر و با عملکرد بالا را برای ترافیک تولیدی بسازید.

Share: