~/icsd.ir — bash
SYSTEM_ONLINE

پایداری و Resilience

در سیستم‌های توزیع‌شده، failure اجتناب‌ناپذیر است. شبکه قطع می‌شود، سرویس‌ها crash می‌کنند، disk پر می‌شود. سیستم باید برای failure طراحی شود — این مفهوم Resilience است.

۹.۱ مقدمه

در سیستم‌های توزیع‌شده، failure اجتناب‌ناپذیر است. شبکه قطع می‌شود، سرویس‌ها crash می‌کنند، disk پر می‌شود. سیستم باید برای failure طراحی شود — این مفهوم Resilience است.

هدف فصل: یادگیری الگوهای Resilience: Circuit Breaker، Retry، Timeout، Bulkhead، Rate Limiting، و Graceful Degradation.

۹.۲ ۸ تصور غلط درباره Distributed Computing

Peter Deutsch این تصورات اشتباه را در ۱۹۹۴ مطرح کرد. هنوز توسعه‌دهندگان آن‌ها را فراموش می‌کنند:

  1. شبکه قابل اعتماد است ❌
  2. Latency صفر است ❌
  3. Bandwidth بی‌نهایت است ❌
  4. شبکه امن است ❌
  5. Topology تغییر نمی‌کند ❌
  6. یک مدیر وجود دارد ❌
  7. هزینه transport صفر است ❌
  8. شبکه homogeneous است ❌

Resilience patterns برای مقابله با این واقعیات طراحی شده‌اند.

۹.۳ Circuit Breaker

الگو از electrical engineering — اگر یک سرویس مدام fail می‌کند، circuit «باز» می‌شود و درخواست‌های بعدی بدون تماس با سرویس fail می‌کنند. این از cascading failure جلوگیری می‌کند.

سه حالت Circuit Breaker


[CLOSED] ──5 failure──► [OPEN] ──30s timeout──► [HALF-OPEN]
   ▲                       │                          │
   │                       │                          │
   │                       ▼                          │
   │                  Reject all requests             │
   │                       │                          │
   │                       │       ┌─success─────────┘
   │                       │       │                  │
   └─────success───────────┴───────┘                  │
                                   │                  │
                                   └─failure─────────►[OPEN]

CLOSED: درخواست‌ها عبور می‌کنند، failure ها شمارش می‌شوند
OPEN: همه درخواست‌ها reject می‌شوند، تا timeout منقضی شود
HALF-OPEN: یک درخواست تست می‌شود، اگر موفق→CLOSED، اگر شکست→OPEN
    

پیاده‌سازی با pybreaker


import pybreaker
import httpx

# تعریف breaker
product_breaker = pybreaker.CircuitBreaker(
    fail_max=5,              # ۵ خطای متوالی → OPEN
    reset_timeout=30,        # بعد از ۳۰s → HALF-OPEN
    exclude=[ValueError],    # exception هایی که شمارش نمی‌شوند
    state_storage=pybreaker.CircuitRedisStorage(...),  # توزیع‌شده
)

@product_breaker
async def get_product(product_id: int):
    async with httpx.AsyncClient(timeout=5.0) as client:
        response = await client.get(
            f"http://product-service/products/{product_id}"
        )
        response.raise_for_status()
        return response.json()

# استفاده
try:
    product = await get_product(123)
except pybreaker.CircuitBreakerError:
    # circuit OPEN است
    return {"error": "Product service unavailable", "fallback": True}
except httpx.HTTPError:
    # خطای HTTP عادی
    raise
    

Circuit Breaker توزیع‌شده

در محیط چند instance، state circuit breaker باید مشترک باشد (Redis).


import redis.asyncio as aioredis

class DistributedCircuitBreaker:
    def __init__(self, name, redis, fail_max=5, reset_timeout=30):
        self.name = name
        self.redis = redis
        self.fail_max = fail_max
        self.reset_timeout = reset_timeout
    
    async def is_open(self):
        state = await self.redis.get(f"cb:{self.name}:state")
        return state == b"open"
    
    async def record_success(self):
        await self.redis.delete(f"cb:{self.name}:failures")
        await self.redis.set(f"cb:{self.name}:state", "closed")
    
    async def record_failure(self):
        failures = await self.redis.incr(f"cb:{self.name}:failures")
        await self.redis.expire(f"cb:{self.name}:failures", 60)
        if failures >= self.fail_max:
            await self.redis.setex(
                f"cb:{self.name}:state",
                self.reset_timeout,
                "open"
            )
    

۹.۴ Retry با Exponential Backoff

برای خطاهای موقت (network blip، rate limit)، retry می‌تواند مشکل را حل کند. اما retry نادرست می‌تواند سرور را overload کند.

الگوی صحیح Retry


import asyncio
import random
from typing import Callable
import httpx

async def retry_with_backoff(
    func: Callable,
    max_attempts: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
    jitter: bool = True,
    retryable_exceptions: tuple = (httpx.TimeoutException, httpx.ConnectError),
):
    """Retry با exponential backoff و jitter"""
    last_exception = None
    
    for attempt in range(max_attempts):
        try:
            return await func()
        except retryable_exceptions as e:
            last_exception = e
            
            if attempt == max_attempts - 1:
                break  # last attempt failed
            
            # Exponential backoff: 1s, 2s, 4s, 8s, ...
            delay = min(base_delay * (2 ** attempt), max_delay)
            
            # Jitter: تصادفی کردن برای جلوگیری از thundering herd
            if jitter:
                delay = delay * (0.5 + random.random())
            
            logger.warning(
                f"Attempt {attempt + 1} failed: {e}. "
                f"Retrying in {delay:.2f}s..."
            )
            await asyncio.sleep(delay)
    
    raise last_exception

# استفاده
async def fetch_product(product_id):
    async def _fetch():
        async with httpx.AsyncClient(timeout=5.0) as client:
            r = await client.get(f"/products/{product_id}")
            r.raise_for_status()
            return r.json()
    
    return await retry_with_backoff(_fetch, max_attempts=3)
    

کِی NOT to Retry

  • Non-idempotent operations: POST بدون idempotency key — retry می‌تواند duplicate ایجاد کند
  • 4xx errors: 400، 401، 403 — تغییر نخواهند کرد
  • 5xx errors بدون transient nature: 501 (Not Implemented)

کتابخانه tenacity


from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type((httpx.TimeoutException, httpx.ConnectError)),
)
async def fetch_product(product_id):
    async with httpx.AsyncClient(timeout=5.0) as client:
        r = await client.get(f"/products/{product_id}")
        r.raise_for_status()
        return r.json()
    

۹.۵ Timeout

«درخواست بدون timeout = درخواست برای فاجعه»

سطوح Timeout

  • Connect Timeout: زمان برقراری TCP connection (پیشنهاد: 2-5s)
  • Read Timeout: زمان دریافت پاسخ (پیشنهاد: 5-30s)
  • Write Timeout: زمان ارسال درخواست
  • Total Timeout: کل زمان

import httpx

# تنظیم timeout دقیق
timeout = httpx.Timeout(
    connect=5.0,   # برقراری اتصال
    read=30.0,     # خواندن پاسخ
    write=10.0,    # ارسال درخواست
    pool=2.0,      # دریافت connection از pool
)

async with httpx.AsyncClient(timeout=timeout) as client:
    response = await client.get("https://api.example.com/data")
    

Timeout Budget

اگر سرویس A با timeout=10s، سرویس B را می‌خواند، و B با timeout=10s سرویس C را می‌خواند، اگر C کند است، A قبل از B timeout می‌شود! باید budget تقسیم شود.


A → B → C
A timeout: 10s
B timeout: 7s   (3s margin برای A)
C timeout: 4s   (3s margin برای B)
    

۹.۶ Bulkhead Pattern

Bulkhead یعنی «دیوار جدا کننده» (مثل کشتی). جدا کردن منابع برای جلوگیری از اینکه یک مشکل کل سیستم را down کند.

مثال: جدا کردن thread pool


from concurrent.futures import ThreadPoolExecutor
import asyncio

# هر سرویس thread pool خود را دارد
pools = {
    "user_service": ThreadPoolExecutor(max_workers=10),
    "product_service": ThreadPoolExecutor(max_workers=20),
    "payment_service": ThreadPoolExecutor(max_workers=5),  # critical
}

async def call_service(service: str, func):
    pool = pools[service]
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(pool, func)

# اگر product_service کند است، بقیه سرویس‌ها تحت تأثیر نیستند
    

Connection Pool جدا


# هر downstream یک client با pool خود
clients = {
    "user": httpx.AsyncClient(
        base_url="http://user-service",
        limits=httpx.Limits(max_connections=20),
        timeout=10.0,
    ),
    "product": httpx.AsyncClient(
        base_url="http://product-service",
        limits=httpx.Limits(max_connections=50),
        timeout=10.0,
    ),
}
    

۹.۷ Rate Limiting

محدود کردن تعداد درخواست‌ها برای محافظت از سرویس و منصفانه بودن.

الگوریتم‌های Rate Limiting

  1. Token Bucket: هر کاربر سطلی با token دارد، token ها با نرخ ثابت refill می‌شوند
  2. Leaky Bucket: صفی با نرخ پردازش ثابت
  3. Fixed Window: شمارش در پنجره‌های زمانی ثابت (ساده ولی burst در مرز)
  4. Sliding Window Log: دقیق‌تر، حافظه بیشتر
  5. Sliding Window Counter: ترکیب fixed window با interpolation

پیاده‌سازی Token Bucket با Redis


import time

class TokenBucketLimiter:
    def __init__(self, redis, capacity: int, refill_rate: float):
        self.redis = redis
        self.capacity = capacity
        self.refill_rate = refill_rate  # tokens per second
    
    async def allow(self, key: str) -> bool:
        now = time.time()
        bucket_key = f"bucket:{key}"
        
        # Lua script برای atomic operation
        lua_script = """
        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local rate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        
        local data = redis.call('HMGET', key, 'tokens', 'last_refill')
        local tokens = tonumber(data[1]) or capacity
        local last_refill = tonumber(data[2]) or now
        
        -- Refill
        local elapsed = now - last_refill
        tokens = math.min(capacity, tokens + elapsed * rate)
        
        if tokens >= 1 then
            tokens = tokens - 1
            redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
            redis.call('EXPIRE', key, 3600)
            return 1
        else
            return 0
        end
        """
        
        result = await self.redis.eval(
            lua_script, 1, bucket_key, 
            self.capacity, self.refill_rate, now
        )
        return result == 1

# استفاده
limiter = TokenBucketLimiter(redis, capacity=100, refill_rate=10)  # 100 token, 10/sec
if await limiter.allow(f"user:{user_id}"):
    # process request
    pass
else:
    raise HTTPException(429, "Rate limit exceeded")
    

۹.۸ Fallback و Graceful Degradation

وقتی یک سرویس در دسترس نیست، به جای fail کامل، یک پاسخ alternative ارائه دهید.


async def get_product_recommendations(user_id: int):
    try:
        # محاسبه شخصی‌سازی شده با ML
        return await recommendation_service.get_personalized(user_id)
    except (ServiceUnavailable, TimeoutError):
        # Fallback 1: کش
        cached = await cache.get(f"recs:{user_id}")
        if cached:
            return cached
        
        # Fallback 2: محصولات محبوب عمومی
        try:
            return await product_service.get_popular()
        except Exception:
            # Fallback 3: لیست hard-coded
            return DEFAULT_RECOMMENDATIONS
    

الگوهای Graceful Degradation

  • Cached fallback: داده قدیمی بهتر از هیچ
  • Default response: پاسخ پیش‌فرض
  • Reduced functionality: feature های غیرضروری disable
  • Read-only mode: در صورت مشکل DB
  • Static page: صفحه ساده به جای dynamic

۹.۹ Health Check و Liveness

سرویس باید بتواند به سیستم بگوید «من زنده‌ام» یا «من مشکل دارم».


@app.get("/health/live")
async def liveness():
    """آیا process زنده است؟ — اگر no، Kubernetes restart می‌کند"""
    return {"status": "alive"}

@app.get("/health/ready")
async def readiness():
    """آیا آماده traffic هستیم؟ — اگر no، از LB حذف می‌شود"""
    checks = {}
    overall_ok = True
    
    # DB
    try:
        await db.execute("SELECT 1")
        checks["database"] = "ok"
    except Exception:
        checks["database"] = "fail"
        overall_ok = False
    
    # Redis
    try:
        await redis.ping()
        checks["redis"] = "ok"
    except Exception:
        checks["redis"] = "fail"
        # Redis اختیاری است، fallback داریم
    
    # External service
    try:
        async with httpx.AsyncClient(timeout=2.0) as c:
            r = await c.get("http://payment-service/health/live")
        checks["payment"] = "ok" if r.status_code == 200 else "degraded"
    except Exception:
        checks["payment"] = "degraded"
    
    if not overall_ok:
        raise HTTPException(503, detail=checks)
    return {"status": "ready", "checks": checks}
    

۹.۱۰ Chaos Engineering

تست resilience با عمداً ایجاد failure در production. Netflix پایه‌گذار این رویکرد است.

ابزارها

  • Chaos Monkey: instances را به‌صورت تصادفی kill می‌کند
  • Chaos Mesh: chaos engineering برای Kubernetes
  • Litmus: open source chaos platform
  • Gremlin: commercial

سناریوهای تست

  • قطع کردن سرویس‌ها به‌صورت تصادفی
  • اضافه کردن latency به شبکه
  • پر کردن disk
  • قطع کردن DB
  • افزایش CPU/Memory

۹.۱۱ بهترین تجربیات

  1. هیچ‌گاه بدون timeout request نزنید.
  2. Circuit Breaker برای همه external calls.
  3. Retry فقط برای idempotent ها.
  4. Exponential backoff with jitter.
  5. Timeout budget. downstream همیشه کمتر.
  6. Bulkhead. isolation منابع.
  7. Health checks جدا. liveness vs readiness.
  8. Graceful Degradation. fallback های متعدد.
  9. Chaos Engineering. regular practice.
  10. Monitoring لازم. circuit state، retry count، timeout rate.

۹.۱۲ خلاصه فصل

آنچه آموختیم:
  • ۸ تصور غلط درباره distributed computing
  • Circuit Breaker — جلوگیری از cascading failure
  • Retry با Exponential Backoff و Jitter
  • Timeout Budget برای call chain
  • Bulkhead Pattern — isolation منابع
  • Rate Limiting با Token Bucket
  • Fallback و Graceful Degradation
  • Health Check در دو سطح: liveness و readiness
  • Chaos Engineering برای تست resilience
در فصل بعد: Docker و Containerization — اساس deployment میکروسرویس‌ها در محیط‌های مدرن.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.