طراحی سیستم و معماری نرمافزار: ساخت سیستمهای مقیاسپذیر
طراحی سیستم هنر ساخت سیستمهای مقیاسپذیر، قابلاعتماد و قابلنگهداری است. این راهنما اصول بنیادی و الگوهای پیشرفتهای را پوشش میدهد که غولهای فناوری برای پاسخگویی به میلیونها کاربر از آنها استفاده میکنند.
مفاهیم بنیادی
انواع مقیاسپذیری
Vertical Scaling (Scale Up)
├── Add more CPU, RAM, Storage to existing server
├── Simpler to implement
├── Hardware limits exist
└── Single point of failure
Horizontal Scaling (Scale Out)
├── Add more servers to the pool
├── Theoretically unlimited scaling
├── Requires distributed system design
└── More complex but more resilientقضیهٔ CAP
در یک سیستم توزیعشده تنها میتوانید دو مورد از این سه ویژگی را تضمین کنید:
- Consistency (سازگاری) - همهٔ نودها در یک لحظه دادهٔ یکسانی را میبینند
- Availability (دسترسپذیری) - هر درخواستی پاسخی دریافت میکند
- Partition Tolerance (تحمل پارتیشن) - سیستم با وجود خرابیهای شبکه به کار خود ادامه میدهد
<Callout type="info"> در عمل، در سیستمهای توزیعشده تحمل پارتیشن انتخابی نیست و همیشه باید برقرار باشد. انتخاب واقعی، در زمان بروز پارتیشن شبکه، میان سازگاری و دسترسپذیری است. </Callout>
CP Systems (Consistency + Partition Tolerance)
├── MongoDB (with majority write concern)
├── HBase
├── Redis Cluster
└── Use when: Financial transactions, inventory management
AP Systems (Availability + Partition Tolerance)
├── Cassandra
├── DynamoDB
├── CouchDB
└── Use when: Social media feeds, analytics, cachingاستراتژیهای توزیع بار (Load Balancing)
توزیع بار لایهٔ ۴ در برابر لایهٔ ۷
Layer 4 (Transport Layer)
├── Routes based on IP and TCP/UDP port
├── Faster, less CPU intensive
├── No content inspection
└── Use for: TCP/UDP traffic, gaming, streaming
Layer 7 (Application Layer)
├── Routes based on HTTP headers, URL, cookies
├── Content-aware routing
├── SSL termination
└── Use for: Web applications, API routingالگوریتمهای توزیع بار
# Round Robin - Simple rotation
servers = ['server1', 'server2', 'server3']
current = 0
def round_robin():
global current
server = servers[current]
current = (current + 1) % len(servers)
return server
# Weighted Round Robin - Based on server capacity
servers = [
{'host': 'server1', 'weight': 5}, # 50% traffic
{'host': 'server2', 'weight': 3}, # 30% traffic
{'host': 'server3', 'weight': 2}, # 20% traffic
]
# Least Connections - Route to server with fewest active connections
def least_connections(servers):
return min(servers, key=lambda s: s.active_connections)
# IP Hash - Consistent routing for same client
def ip_hash(client_ip, servers):
hash_value = hash(client_ip)
return servers[hash_value % len(servers)]
# Consistent Hashing - Minimizes redistribution when servers change
class ConsistentHash:
def __init__(self, nodes, virtual_nodes=150):
self.ring = {}
self.sorted_keys = []
for node in nodes:
for i in range(virtual_nodes):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def get_node(self, key):
hash_key = self._hash(key)
for ring_key in self.sorted_keys:
if hash_key <= ring_key:
return self.ring[ring_key]
return self.ring[self.sorted_keys[0]]الگوهای مقیاسپذیری پایگاه داده
رپلیکاهای خواندن (Read Replica)
┌─────────────┐
│ Primary │
│ (Writes) │
└──────┬──────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Replica 1 │ │ Replica 2 │ │ Replica 3 │
│ (Reads) │ │ (Reads) │ │ (Reads) │
└─────────────┘ └─────────────┘ └─────────────┘class DatabaseRouter:
def __init__(self, primary, replicas):
self.primary = primary
self.replicas = replicas
self.replica_index = 0
def get_connection(self, operation):
if operation in ('INSERT', 'UPDATE', 'DELETE'):
return self.primary
else:
# Round-robin across replicas
replica = self.replicas[self.replica_index]
self.replica_index = (self.replica_index + 1) % len(self.replicas)
return replicaشاردینگ پایگاه داده
Horizontal Sharding (by rows)
├── Range-based: users 1-1M → Shard1, 1M-2M → Shard2
├── Hash-based: hash(user_id) % num_shards
├── Directory-based: Lookup service maps keys to shards
└── Geographic: Users by region
Vertical Sharding (by columns/tables)
├── User data → Database A
├── Order data → Database B
└── Analytics → Database Cclass ShardRouter:
def __init__(self, shards):
self.shards = shards
self.num_shards = len(shards)
def get_shard(self, user_id):
# Consistent hashing for even distribution
shard_index = hash(str(user_id)) % self.num_shards
return self.shards[shard_index]
def get_shard_for_range_query(self, start_id, end_id):
# Range queries may need to hit multiple shards
shards_needed = set()
for user_id in range(start_id, end_id + 1):
shards_needed.add(self.get_shard(user_id))
return list(shards_needed)<Note title="ملاحظات شاردینگ"> شاردینگ به سیستم پیچیدگی اضافه میکند. ابتدا رپلیکاهای خواندن و کش را در نظر بگیرید و تنها زمانی سراغ شاردینگ بروید که همهٔ ظرفیت مقیاسپذیری عمودی را به کار گرفته باشید. </Note>
معماری کش
کش چندسطحی
Request Flow:
Client → CDN → Load Balancer → App Server → Cache → Database
↓ ↓ ↓ ↓
Static Session Application Query
Assets Affinity Cache Cache
Cache Levels:
├── L1: Browser Cache (client-side)
├── L2: CDN Cache (edge locations)
├── L3: Application Cache (Redis/Memcached)
├── L4: Database Query Cache
└── L5: Database Buffer Poolاستراتژیهای ابطال کش (Cache Invalidation)
class CacheManager:
def __init__(self, cache, db):
self.cache = cache
self.db = db
# Cache-Aside (Lazy Loading)
def get_user(self, user_id):
key = f"user:{user_id}"
# Try cache first
user = self.cache.get(key)
if user:
return user
# Cache miss - load from DB
user = self.db.get_user(user_id)
if user:
self.cache.set(key, user, ttl=3600)
return user
# Write-Through
def update_user_write_through(self, user_id, data):
# Update DB first
user = self.db.update_user(user_id, data)
# Then update cache
self.cache.set(f"user:{user_id}", user, ttl=3600)
return user
# Write-Behind (Async)
def update_user_write_behind(self, user_id, data):
key = f"user:{user_id}"
# Update cache immediately
self.cache.set(key, data, ttl=3600)
# Queue DB write for async processing
self.queue.push({
'operation': 'update_user',
'user_id': user_id,
'data': data
})
return data
# Cache Invalidation
def invalidate_user(self, user_id):
# Delete specific key
self.cache.delete(f"user:{user_id}")
# Invalidate related caches
self.cache.delete(f"user:{user_id}:orders")
self.cache.delete(f"user:{user_id}:preferences")
# Tag-based invalidation
self.cache.delete_by_tag(f"user:{user_id}")معماری میکروسرویسها
الگوهای ارتباط بین سرویسها
Synchronous Communication
├── REST APIs
│ └── Simple, stateless, HTTP-based
├── gRPC
│ └── High performance, binary protocol, streaming
└── GraphQL
└── Flexible queries, single endpoint
Asynchronous Communication
├── Message Queues (RabbitMQ, SQS)
│ └── Point-to-point, guaranteed delivery
├── Event Streaming (Kafka)
│ └── Pub/sub, event sourcing, replay capability
└── Event Bus
└── Loose coupling, broadcast eventsمعماری Service Mesh
┌─────────────────────────────────────────────────────────┐
│ Control Plane │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Config │ │ Discovery │ │ Certs │ │
│ │ Server │ │ Service │ │ Manager │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Service A │ │ Service B │ │ Service C │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ App │ │ │ │ App │ │ │ │ App │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Sidecar │ │ │ │ Sidecar │ │ │ │ Sidecar │ │
│ │ Proxy │◄┼─┼─► Proxy │◄┼─┼─► Proxy │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘الگوی Circuit Breaker
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if service recovered
class CircuitBreaker:
def __init__(
self,
failure_threshold=5,
recovery_timeout=30,
expected_exception=Exception
):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.expected_exception = expected_exception
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = None
self.success_count = 0
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if self._should_attempt_reset():
self.state = CircuitState.HALF_OPEN
else:
raise CircuitBreakerOpenException()
try:
result = func(*args, **kwargs)
self._on_success()
return result
except self.expected_exception as e:
self._on_failure()
raise e
def _on_success(self):
if self.state == CircuitState.HALF_OPEN:
self.success_count += 1
if self.success_count >= 3: # Require 3 successes to close
self.state = CircuitState.CLOSED
self.failure_count = 0
self.success_count = 0
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
def _should_attempt_reset(self):
return (
time.time() - self.last_failure_time >= self.recovery_timeout
)
# Usage
user_service_breaker = CircuitBreaker(
failure_threshold=5,
recovery_timeout=30
)
def get_user(user_id):
return user_service_breaker.call(
external_user_service.get,
user_id
)معماری رویدادمحور (Event-Driven)
Event Sourcing
from dataclasses import dataclass
from datetime import datetime
from typing import List
import json
@dataclass
class Event:
event_type: str
aggregate_id: str
data: dict
timestamp: datetime
version: int
class EventStore:
def __init__(self):
self.events = []
def append(self, event: Event):
self.events.append(event)
def get_events(self, aggregate_id: str) -> List[Event]:
return [e for e in self.events if e.aggregate_id == aggregate_id]
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self.status = None
self.items = []
self.total = 0
self.version = 0
def apply(self, event: Event):
if event.event_type == "OrderCreated":
self.status = "created"
self.items = event.data["items"]
self.total = event.data["total"]
elif event.event_type == "OrderPaid":
self.status = "paid"
elif event.event_type == "OrderShipped":
self.status = "shipped"
self.tracking_number = event.data["tracking_number"]
elif event.event_type == "OrderCancelled":
self.status = "cancelled"
self.version = event.version
@classmethod
def rebuild(cls, events: List[Event]) -> "Order":
if not events:
return None
order = cls(events[0].aggregate_id)
for event in events:
order.apply(event)
return order
# Usage
event_store = EventStore()
# Create order
event_store.append(Event(
event_type="OrderCreated",
aggregate_id="order-123",
data={"items": [{"sku": "ABC", "qty": 2}], "total": 99.99},
timestamp=datetime.now(),
version=1
))
# Rebuild order state from events
events = event_store.get_events("order-123")
order = Order.rebuild(events)CQRS (جداسازی مسئولیت دستورها و کوئریها)
┌─────────────────┐
│ Client │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Commands │ │ Queries │
│ (Write) │ │ (Read) │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Write │ Events │ Read │
│ Model │────────────►│ Model │
│ (Normalized)│ │(Denormalized│
└─────────────┘ └─────────────┘الگوهای دسترسپذیری بالا (High Availability)
Failover از نوع Active-Passive
Normal Operation:
┌─────────────┐ ┌─────────────┐
│ Active │────►│ Passive │
│ Server │ │ Server │
│ (Primary) │ │ (Standby) │
└─────────────┘ └─────────────┘
│
▼
[Traffic]
After Failover:
┌─────────────┐ ┌─────────────┐
│ Failed │ │ Active │
│ Server │ │ Server │
│ (Down) │ │ (Promoted) │
└─────────────┘ └─────────────┘
│
▼
[Traffic]Active-Active (Multi-Master)
┌─────────────┐ ┌─────────────┐
│ Active │◄───►│ Active │
│ Server 1 │ │ Server 2 │
└──────┬──────┘ └──────┬──────┘
│ │
└─────────┬─────────┘
│
┌──────┴──────┐
│Load Balancer│
└──────┬──────┘
│
[Traffic]جمعبندی
طراحی سیستم یعنی انجام مصالحههای آگاهانه (Trade-off) بر اساس نیازمندیها. هیچ راهحل واحدی برای همهٔ شرایط وجود ندارد؛ بهترین معماری به مقیاس مشخص شما، الزامات سازگاری، تخصص تیم و محدودیتهای کسبوکارتان بستگی دارد.
نکات کلیدی:
- قضیهٔ CAP و پیامدهای آن را درک کنید
- ساده شروع کنید و فقط در صورت نیاز مقیاس بدهید
- از کش بهصورت استراتژیک و در چند سطح استفاده کنید
- با Circuit Breaker و سازوکار retry، سیستم را برای مواجهه با خرابی طراحی کنید
- برای رسیدن به اتصال سست (Loose Coupling)، معماری رویدادمحور را در نظر بگیرید
- همهچیز را مانیتور کنید و برای مشاهدهپذیری (Observability) برنامه داشته باشید
این یادداشت ترجمهٔ فارسی نوشتهٔ خودم است — نسخهٔ اصلی (انگلیسی) در dev.to