پایداری و Resilience
در سیستمهای توزیعشده، failure اجتنابناپذیر است. شبکه قطع میشود، سرویسها crash میکنند، disk پر میشود. سیستم باید برای failure طراحی شود — این مفهوم Resilience است.
۹.۱ مقدمه
در سیستمهای توزیعشده، failure اجتنابناپذیر است. شبکه قطع میشود، سرویسها crash میکنند، disk پر میشود. سیستم باید برای failure طراحی شود — این مفهوم Resilience است.
۹.۲ ۸ تصور غلط درباره Distributed Computing
Peter Deutsch این تصورات اشتباه را در ۱۹۹۴ مطرح کرد. هنوز توسعهدهندگان آنها را فراموش میکنند:
- شبکه قابل اعتماد است ❌
- Latency صفر است ❌
- Bandwidth بینهایت است ❌
- شبکه امن است ❌
- Topology تغییر نمیکند ❌
- یک مدیر وجود دارد ❌
- هزینه transport صفر است ❌
- شبکه 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
- Token Bucket: هر کاربر سطلی با token دارد، token ها با نرخ ثابت refill میشوند
- Leaky Bucket: صفی با نرخ پردازش ثابت
- Fixed Window: شمارش در پنجرههای زمانی ثابت (ساده ولی burst در مرز)
- Sliding Window Log: دقیقتر، حافظه بیشتر
- 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
۹.۱۱ بهترین تجربیات
- هیچگاه بدون timeout request نزنید.
- Circuit Breaker برای همه external calls.
- Retry فقط برای idempotent ها.
- Exponential backoff with jitter.
- Timeout budget. downstream همیشه کمتر.
- Bulkhead. isolation منابع.
- Health checks جدا. liveness vs readiness.
- Graceful Degradation. fallback های متعدد.
- Chaos Engineering. regular practice.
- 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