System Design

تسلط بر تراکنش‌های توزیع‌شده در طراحی سیستم

در معماری نرم‌افزار مدرن، گذار از برنامه‌های تک‌تکه (Monolithic) به میکروسرویس‌ها پیچیدگی قابل‌توجهی را در زمینه یکپارچگی داده‌ها ایجاد کرده است. وقتی یک عملیات واحد به چندین سرویس گسترده می‌شود، که هر کدام پایگاه داده مخصوص خود را دارند، اطمینان از اینکه یا تمام مراحل با موفقیت انجام شوند یا همه آن‌ها شکست بخورند، به یک چالش بزرگ تبدیل می‌شود. این مفهوم که به عنوان تراکنش توزیع‌شده شناخته می‌شود، برای حفظ یکپارچگی داده‌ها در سیستم‌های پیچیده حیاتی است. بدون استراتژی‌های مناسب، شما در معرض خطر فساد داده‌ها، از دست رفتن سفارشات یا مغایرت‌های مالی قرار می‌گیرید که می‌تواند تأثیر شدیدی بر عملیات کسب‌وکار داشته باشد.

درک چالش‌های اصلی

قبل از غوطه‌ور شدن در راه‌حل‌ها، درک دلیل دشواری تراکنش‌های توزیع‌شده ضروری است. در یک پایگاه داده واحد، ویژگی‌های ACID (ذره‌ای بودن، سازگاری، جداسازی، پایداری) توسط موتور پایگاه داده به صورت بی‌نقص مدیریت می‌شوند. با این حال، در یک محیط توزیع‌شده، این ویژگی‌ها رایگان نیستند. شما اغلب مجبور به انجام مبادله‌هایی می‌شوید که توسط قضیه CAP دیکته می‌شوند، که بیان می‌کند یک سیستم توزیع‌شده تنها می‌تواند دو مورد از سه مورد زیر را تضمین کند: سازگاری (Consistency)، در دسترس بودن (Availability) و تحمل خطای پارتیشن (Partition Tolerance). علاوه بر این، تأخیر شبکه و شکست‌های احتمالی نودها به این معنی است که یک دستور `COMMIT` ساده دیگر کافی نیست. شما باید پروتکل‌هایی را پیاده‌سازی کنید که شکست‌های جزئی را به صورت شایسته مدیریت کنند. اگر سرویس A موفق شود اما سرویس B شکست بخورد، شما به مکانیسمی برای بازگرداندن تغییرات سرویس A نیاز دارید که به آن تراکنش‌های جبرانی (Compensating Transactions) می‌گویند. نادیده گرفتن این چالش‌ها منجر به «الگوهای ضد‌طراحی توزیع‌شده» می‌شود که در آن‌ها داده‌ها در مرزهای سیستم شما ناسازگار می‌شوند.

پیاده‌سازی الگوی Saga

یکی از مقاوم‌ترین رویکردها برای مدیریت تراکنش‌های توزیع‌شده، الگوی Saga است. یک Saga تراکنش بزرگ را به دنباله‌ای از تراکنش‌های محلی می‌شکند، که هر کدام پایگاه داده را درون یک سرویس واحد به‌روز می‌کنند. اگر یک مرحله شکست بخورد، Saga سری از تراکنش‌های جبرانی را برای لغو تغییرات ایجاد شده توسط مراحل پیشین اجرا می‌کند. این رویکرد سازگاری نهایی (Eventual Consistency) را بر سازگاری قوی ترجیح می‌دهد که اغلب برای میکروسرویس‌های با در دسترس بودن بالا عملی‌تر است. الگوی Saga را می‌توان با استفاده از رویکرد مبتنی بر رقص (Choreography-based) (جایی که سرویس‌ها رویدادها را منتشر کرده و به آن‌ها واکنش نشان می‌دهند) یا رویکرد مبتنی بر ارکستراسیون (Orchestration-based) (جایی که یک هماهنگ‌کننده مرکزی جریان را هدایت می‌کند) پیاده‌سازی کرد. رویکرد ارکستراسیون معمولاً برای عیب‌یابی و نگهداری آسان‌تر است. در زیر یک مثال پایتون که یک Saga مبتنی بر ارکستراسیون ساده را برای فرآیند سفارش در تجارت الکترونیک نشان می‌دهد، آورده شده است:
class OrderSaga:
    def __init__(self, order_service, payment_service, inventory_service):
        self.order_service = order_service
        self.payment_service = payment_service
        self.inventory_service = inventory_service

    def execute(self, order_id, user_id, items):
        try:
            # مرحله ۱: ایجاد سفارش
            order = self.order_service.create(order_id, user_id)
            
            # مرحله ۲: پردازش پرداخت
            payment_status = self.payment_service.charge(order.amount, user_id)
            
            # مرحله ۳: رزرو موجودی
            self.inventory_service.reserve(items)
            
            # اگر همه موفق شوند، سفارش کامل است
            return order
            
        except Exception as e:
            # اجرای تراکنش‌های جبرانی
            self._compensate(order_id, items)
            raise e

    def _compensate(self, order_id, items):
        # معکوس کردن مراحل به ترتیب معکوس
        self.inventory_service.release(items)
        self.payment_service.refund(order_id)
        self.order_service.cancel(order_id)

تعهد دو مرحله‌ای: رویکرد سنتی

در حالی که الگوی Saga در میکروسرویس‌ها محبوب است، سناریوهایی وجود دارد که در آن‌ها سازگاری قوی غیرقابل مذاکره است. پروتکل تعهد دو مرحله‌ای (2PC) یک الگوریتم اجماع توزیع‌شده کلاسیک است که ذره‌ای بودن را در چندین نود تضمین می‌کند. در مرحله اول (آماده‌سازی)، هماهنگ‌کننده از تمام شرکت‌کنندگان می‌پرسد که آیا برای تعهد آماده هستند یا خیر. در مرحله دوم (تعهد)، اگر تمام شرکت‌کنندگان رأی «بله» بدهند، هماهنگ‌کننده پیام تعهد را ارسال می‌کند؛ در غیر این صورت، پیام لغو را ارسال می‌کند. اگرچه 2PC سازگاری قوی را تضمین می‌کند، اما معایب قابل توجهی دارد. این پروتکل مسدودکننده (Blocking) است، به این معنی که اگر هماهنگ‌کننده در حین تراکنش شکست بخورد، شرکت‌کنندگان ممکن است در یک وضعیت مبهم باقی بمانند. علاوه بر این، گردش‌های شبکه چندگانه تأخیر بالایی ایجاد می‌کنند که می‌تواند بر عملکرد سیستم تأثیر بگذارد. در نتیجه، 2PC به ندرت در برنامه‌های بومی ابری مدرن استفاده می‌شود، مگر اینکه برای سیستم‌های دفتر کل مالی یا سایر ذخایر داده حیاتی به شدت ضروری باشد.
// شبه‌کد برای تعهد دو مرحله‌ای
function twoPhaseCommit(coordinator, participants):
    // مرحله ۱: آماده‌سازی
    for participant in participants:
        result = participant.prepare()
        if result != READY:
            coordinator.send(ABORT)
            return

    // مرحله ۲: تعهد
    coordinator.send(COMMIT)
    for participant in participants:
        participant.commit()

بهترین شیوه‌ها برای پیاده‌سازی

هنگام طراحی سیستم خود، از تراکنش‌های توزیع‌شده مگر در مواردی که واقعاً ضروری است، پرهیز کنید. در عوض، سرویس‌های خود را به گونه‌ای طراحی کنید که به هم گره نخورده (Loosely Coupled) و بر سازگاری نهایی تکیه کنند. از معماری‌های رویداد-محور برای انتشار تغییرات وضعیت به صورت ناهمگام استفاده کنید. اطمینان حاصل کنید که تراکنش‌های جبرانی شما ایستا (Idempotent) هستند تا سناریوهای تلاش مجدد را به صورت ایمن مدیریت کنند. در نهایت، نظارت و هشداردهی قوی را پیاده‌سازی کنید تا ناسازگاری‌ها را زودتر تشخیص دهید و به شما امکان دهید تا در صورت شکست تلاش‌های مجدد خودکار، وظایف آشتی‌دهی دستی را اجرا کنید. با انتخاب دقیق بین مدل‌های سازگاری قوی مانند 2PC و مدل‌های انعطاف‌پذیر مانند Saga، می‌توانید سیستم‌های مقاومی بسازید که به طور مؤثر مقیاس‌پذیر باشند.
Share: