الگوی بازگشتی: طراحی تخریب برازنده در میکروسرویس ها

الگوی بازگشتی: طراحی تخریب برازنده در میکروسرویس ها

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

بازگرداندن یک “خطای داخلی سرور 500” عمومی یا یک صفحه خالی به کاربران در لحظه ای که یک وابستگی منفرد از کار می افتد، تجربه کاربری ضعیفی است. در عوض، سیستم‌های ارتجاعی به گونه‌ای ساخته شده‌اند که در صورت بروز مشکل، به‌خوبی تخریب شوند.

اینجاست که الگوی بازگشتی وارد می شود. با تعریف یک مسیر اجرای ایمن و جایگزین در صورت عدم موفقیت فراخوانی سرویس اولیه، می توانید برنامه خود را حتی در حالت تخریب شده، کاربردی نگه دارید.

در این راهنما، الگوی Fallback، استراتژی‌های رایج برای اجرای آن، نحوه تعامل آن با سایر الگوهای انعطاف‌پذیری، و نحوه نوشتن منطق بازگشتی در جاوا (Resilience4j) و Go را بررسی خواهیم کرد.


قیاس دنیای واقعی: طرح پشتیبان گیری کافی شاپ

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

آیا کافی شاپ بلافاصله چراغ ها را خاموش می کند، درها را قفل می کند و همه مشتریان را به خانه می فرستد؟

البته نه. آنها یک استراتژی بازگشتی را اجرا می کنند:

  • اگر پول نقد در دست داشته باشند، می پرسند که آیا می توانید با پول نقد پرداخت کنید.
  • اگر مشتری دائمی هستید، باریستا ممکن است نام شما را بنویسد و سفارش را در دفتر کل ثبت کند و از شما بخواهد در بازدید بعدی پرداخت کنید.
  • ممکن است از یک کارتخوان آفلاین استفاده کنند که رمز کارت را به صورت محلی ذخیره می کند و بعداً وقتی اینترنت بازیابی می شود، پرداخت را پردازش می کند.
نمودار معماری الگوی بازگشتی که شکست سرویس اولیه و مسیریابی به یک کنترل کننده بازگشتی برای بازیابی داده های ذخیره شده/پشتیبان را نشان می دهد.

در طراحی نرم افزار:

  • سفارش قهوه درخواست مشتری است.
  • ترمینال کارت سرویس پایین دستی اولیه است (به عنوان مثال، یک API دروازه پرداخت).
  • قطع اینترنت یک مهلت زمانی شبکه یا خرابی سرویس است.
  • Ledger / Offline Reader مسیر اجرای بازگشتی است.

استراتژی های متداول بازگشتی

بسته به منطق کسب و کار و بحرانی بودن سرویس شکست خورده، می توانید از چندین استراتژی بازگشتی انتخاب کنید:

1. مقادیر پیش فرض استاتیک

ساده ترین استراتژی، بازگرداندن یک مقدار ایستا از پیش پیکربندی شده است. این برای ویژگی های غیر بحرانی که نمایش داده های خالی یا پیش فرض قابل قبول است بسیار موثر است.

  • مثال: اگر سرویس شخصی‌سازی نمایه با مشکل مواجه شد، یک تصویر آواتار پیش‌فرض و یک تبریک عمومی را برگردانید.
  • مثال: اگر یک سرویس توصیه ناموفق بود، به جای ارسال خطا، یک لیست خالی یا یک لیست سخت کد شده از پرفروش ترین های جهانی را برگردانید.

2. پاسخ های ذخیره شده در حافظه پنهان (کهنه-در حالی که-تأیید مجدد)

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

  • مثال: اگر سرویس موجودی محصول کاهش یابد، مقدار موجودی موجود در انبار را که مربوط به 5 دقیقه قبل ذخیره شده است، به همراه یک پیام UI ظریف نشان دهید که ممکن است داده ها به طور کامل به روز نباشند.
  • مثال: اگر سرویس تنظیمات کاربر با مشکل مواجه شد، به جای مسدود کردن جریان ورود به سیستم، نمایه کاربر ذخیره شده را بارگیری کنید.

3. سرویس جایگزین (چند ارائه دهنده)

هنگام اجرای یک عملیات حیاتی که باید موفقیت آمیز باشد، می توانید یک ارائه دهنده خدمات ثانویه را به عنوان پشتیبان پیکربندی کنید.

  • مثال: اگر درگاه پرداخت اولیه شما (به عنوان مثال، Stripe) یک خطای 5xx را برمی گرداند یا زمان آن تمام شده است، مکانیسم بازگشتی بلافاصله درخواست تراکنش را به یک دروازه ثانویه (مثلاً PayPal یا Adyen) هدایت می کند.
  • مثال: اگر یک API کدگذاری جغرافیایی با مشکل مواجه شد، به یک ارائه دهنده نقشه ثانویه برگردید.

4. صف برای بعد (بافر ناهمزمان)

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

  • مثال: اگر یک سرویس اعلان ایمیل خاموش است، بار اعلان را در یک صف نامه مرده یا یک جدول پایگاه داده محلی بنویسید. یک کارگر پس‌زمینه از این صف می‌خواند و ایمیل‌ها را پس از سالم شدن مجدد سرویس اعلان ارسال می‌کند.

The Resilience Trio: دوباره امتحان کنید در مقابل مدار شکن در مقابل فال بک

برای ساختن یک معماری بسیار انعطاف پذیر، باید الگوی Fallback را با الگوهای تعداد مجدد و Circuit Breaker ترکیب کنید. آنها یک خط دفاعی سه لایه را تشکیل می دهند:

الگو نقش اقدام سناریو
تکرار مجدد الگو اشکالات گذرا کوتاه مدت را برطرف می کند. درخواست را پس از یک تاخیر کوتاه تکرار می کند (بازگشت + جیتر). افت مختصر بسته شبکه، بازنشانی سوکت.
مدار شکن از فرسودگی منابع جلوگیری می کند. سفرها برای مسدود کردن تماس‌ها با سرویسی که از کار افتاده است، باز می‌شود (سریع). توقف مداوم سرویس، بن بست پایگاه داده.
الگوی بازگشتی تجربه کاربر را حفظ می کند. هنگامی که تماس اولیه با شکست مواجه می شود یا مسدود می شود، یک عمل جایگزین را اجرا می کند. هنگامی که تلاش های مجدد تمام شد یا مدار شکن باز است.

چگونه آنها با هم کار می کنند

  1. یک درخواست ورودی به دروازه سرویس می رسد.
  2. درخواست از Circuit Breaker عبور می کند.
  3. اگر Circuit Breaker بسته باشد، درخواست به wrapper امتحان مجدد می رود که تماس واقعی را برقرار می کند.
  4. اگر یک خطای گذرا رخ دهد، مکانیسم Retry دوباره تماس را انجام می دهد.
  5. اگر همه تلاش‌های مجدد با شکست مواجه شوند، یا اگر Circuit Breaker قبلاً باز بوده است (در ذخیره‌سازی منابع به سرعت شکست می‌خورد)، Fallback Handler شکست را قطع می‌کند و پاسخ کاهش یافته/در حافظه پنهان را برمی‌گرداند.

پیاده سازی کد

بیایید ببینیم چگونه می توانیم منطق بازگشتی را در جاوا و Go پیاده سازی کنیم.

1. جاوا (Resilience4j)

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

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;

@Service
public class ProductService {

    private final InventoryClient inventoryClient;
    private final CacheManager cacheManager;

    public ProductService(InventoryClient inventoryClient, CacheManager cacheManager) {
        this.inventoryClient = inventoryClient;
        this.cacheManager = cacheManager;
    }

    // Bind this method to a circuit breaker. If it fails or is open, route to fallback
    @CircuitBreaker(name = "inventoryService", fallbackMethod = "getInventoryFallback")
    public List<String> getProductInventory(String category) {
        return inventoryClient.fetchStockByCategory(category);
    }

    // Fallback method must have the same return type and accept the same parameters,
    // plus a Throwable parameter containing the error that triggered it
    public List<String> getInventoryFallback(String category, Throwable throwable) {
        System.err.println("Primary inventory service failed: " + throwable.getMessage());
        
        // Attempt to fetch from local cache (Strategy 2)
        List<String> cachedStock = cacheManager.get("inventory:" + category, List.class);
        if (cachedStock != null) {
            return cachedStock;
        }

        // Return static empty list if cache is empty (Strategy 1)
        return Collections.emptyList();
    }
}

2. برو (گولنگ)

در Go، می‌توانیم با استفاده از الگوهای برنامه‌نویسی کاربردی، دکوراتورهای بازگشتی تمیز بنویسیم، که به ما امکان می‌دهد هر کنترل‌کننده اجرایی را با یک کنترل‌کننده ثانویه بپیچیم.

package main

import (
	"context"
	"errors"
	"fmt"
	"time"
)

// Request represents a simple payload
type Request struct {
	UserID string
}

// Response represents the returned data
type Response struct {
	Data   string
	Status string
}

// ServiceFunc represents our primary function signature
type ServiceFunc func(ctx context.Context, req Request) (Response, error)

// WithFallback wraps a service function with fallback logic
func WithFallback(primary ServiceFunc, fallback ServiceFunc) ServiceFunc {
	return func(ctx context.Context, req Request) (Response, error) {
		res, err := primary(ctx, req)
		if err != nil {
			fmt.Printf("[Warning] Primary call failed: %v. Running fallback...\n", err)
			return fallback(ctx, req)
		}
		return res, nil
	}
}

func main() {
	// 1. Define primary service that occasionally fails
	primaryService := func(ctx context.Context, req Request) (Response, error) {
		return Response{}, errors.New("database connection timeout (504)")
	}

	// 2. Define fallback service that retrieves cached data
	fallbackService := func(ctx context.Context, req Request) (Response, error) {
		// Simulating cached read
		return Response{
			Data:   fmt.Sprintf("Stale cached profile data for user %s", req.UserID),
			Status: "DEGRADED (CACHED)",
		}, nil
	}

	// 3. Wrap them together
	resilientService := WithFallback(primaryService, fallbackService)

	// 4. Execute
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	req := Request{UserID: "user_992"}
	response, err := resilientService(ctx, req)
	if err != nil {
		fmt.Printf("Operation completely failed: %v\n", err)
	} else {
		fmt.Printf("Response Received:\n - Status: %s\n - Data: %s\n", response.Status, response.Data)
	}
}

بهترین روش ها برای الگوی بازگشتی

  1. مسیر بازگشتی را بدون وابستگی نگه دارید: منطق برگشتی نباید به همان سیستم ها یا زیرساخت های پایین دستی متکی باشد که به تازگی شکست خورده اند. اگر پایگاه داده شما خراب باشد، بازگشت به پرس و جو پایگاه داده دیگری در همان سرور نیز احتمالاً با شکست مواجه خواهد شد.
  2. اجرای سریع: منطق بازگشتی باید به سرعت اجرا شود. از محاسبات پیچیده یا تماس‌های تودرتوی آهسته در مسیرهای بازگشتی خود اجتناب کنید. هدف این است که پاسخ را در سریع ترین زمان ممکن به کاربر برگردانید.
  3. **هشدار و مانیتور **: همیشه اجراهای برگشتی و شمارنده های تله متری افزایشی را ثبت کنید. اگر سیستم شما مسیرهای بازگشتی را اجرا می کند، به این معنی است که سیستم در حالت تخریب شده کار می کند. برای اینکه بدانید چه زمانی نرخ بازگشتی از آستانه عادی فراتر می رود، به هشدار نیاز دارید.
  4. رابط کاربری را انعطاف پذیر کنید: از نزدیک با مهندسان فرانت اند کار کنید تا مطمئن شوید که رابط کاربری به گونه ای طراحی شده است که بارهای برگشتی (مانند مجموعه های خالی یا حالت های جزئی) را بدون شکستن طرح سمت کلاینت بپذیرد.

نتیجه گیری

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