~/icsd.ir — bash
SYSTEM_ONLINE

Service Discovery

در محیط‌های داینامیک، instance های سرویس‌ها مدام scale up/down می‌شوند، crash می‌کنند یا restart می‌شوند. Service Discovery مکانیزمی است که به سرویس‌ها امکان می‌دهد به‌صورت داینامیک یکدیگر را پیدا کنند.

۵.۱ مقدمه

در محیط‌های داینامیک، instance های سرویس‌ها مدام scale up/down می‌شوند، crash می‌کنند یا restart می‌شوند. Service Discovery مکانیزمی است که به سرویس‌ها امکان می‌دهد به‌صورت داینامیک یکدیگر را پیدا کنند.


هدف این فصل: درک مشکل service discovery، آشنایی با ابزارهای Consul و Eureka، تفاوت Client-side و Server-side discovery، و پیاده‌سازی عملی در Python.

۵.۲ چرا Service Discovery لازم است؟

مشکل آدرس‌دهی استاتیک

در روش سنتی، آدرس IP و port سرویس‌ها در فایل پیکربندی hard-code می‌شد:


# روش قدیمی - مشکل‌ساز
USER_SERVICE_URL = "http://192.168.1.10:8001"
PRODUCT_SERVICE_URL = "http://192.168.1.11:8002"
    

مشکلات این روش:

  • Auto-scaling: با اضافه شدن instance جدید، آدرس جدید را کسی نمی‌داند
  • Container ها: Docker و Kubernetes IP های داینامیک می‌دهند
  • Rolling update: در حین deploy، instance ها مدام تغییر می‌کنند
  • Failure: اگر یک instance crash کند، باید از rotation حذف شود
  • Multi-region: در deployment های جغرافیایی متعدد پیچیده می‌شود

راه‌حل: Service Registry

یک سیستم مرکزی که register و discover سرویس‌ها را مدیریت می‌کند:


1. سرویس راه‌اندازی می‌شود
   ↓
2. خود را در Service Registry register می‌کند
   "I am user-service, my address is 10.0.0.5:8001"
   ↓
3. به‌صورت دوره‌ای heartbeat می‌فرستد
   "I am still alive!"
   ↓
4. اگر heartbeat نرسد، Registry آن را از لیست خارج می‌کند
   ↓
5. سرویس‌های دیگر از Registry آدرس را query می‌کنند
   "Where is user-service?"
   ↓
6. Registry آدرس(های) فعال را برمی‌گرداند
   "10.0.0.5:8001, 10.0.0.6:8001"
    

۵.۳ الگوهای Service Discovery

۱. Client-Side Discovery

Client مستقیماً با Registry صحبت می‌کند، آدرس را می‌گیرد و درخواست را به سرویس می‌فرستد. خود client مسئول load balancing است.


[Client] ──(1) Where is product-service?──► [Service Registry]
[Client] ◄─(2) [10.0.0.5, 10.0.0.6, 10.0.0.7]──── [Service Registry]
[Client] ──(3) GET /products────────────────────► [10.0.0.6:8002]
              (with own load balancing)
    

مزایا: سریع‌تر (یک hop کمتر)، load balancing انعطاف‌پذیر

معایب: client پیچیده‌تر، coupling به registry، باید برای هر زبان library داشت

مثال‌ها: Netflix Eureka + Ribbon

۲. Server-Side Discovery

Client با load balancer صحبت می‌کند، load balancer از registry آدرس می‌گیرد و درخواست را forward می‌کند.


[Client] ──(1) GET /products──► [Load Balancer / API Gateway]
                                        │
                                        │ (2) Where is product-service?
                                        ▼
                                 [Service Registry]
                                        │
                                        │ (3) [10.0.0.5, 10.0.0.6]
                                        ▼
                                 [Load Balancer]
                                        │
                                        │ (4) Forward
                                        ▼
                                 [10.0.0.6:8002]
    

مزایا: client ساده، centralized control، مستقل از زبان

معایب: یک hop اضافه، load balancer می‌تواند bottleneck شود

مثال‌ها: Kubernetes Services، AWS ELB، Consul + NGINX

۳. Self-Registration vs Third-Party Registration

Self-Registration

خود سرویس در شروع کار، در registry register می‌شود.

✅ کنترل بیشتر سرویس بر چرخه زندگی

❌ هر سرویس باید این منطق را پیاده کند

Third-Party Registration

یک sidecar یا orchestrator مسئول register کردن است.

✅ سرویس تمیز، جداسازی concern

❌ نیاز به infrastructure اضافی

۵.۴ HashiCorp Consul

Consul ابزار محبوبی برای service discovery، configuration و service mesh است. ویژگی‌های آن:

  • Service registry با health check
  • Key-Value store برای configuration
  • DNS و HTTP interface
  • Multi-datacenter
  • Consul Connect برای mTLS

اجرای Consul با Docker


# docker-compose.yml
version: "3.8"

services:
  consul:
    image: hashicorp/consul:1.17
    command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
    ports:
      - "8500:8500"  # UI
      - "8600:8600/udp"  # DNS
    volumes:
      - consul-data:/consul/data
    networks:
      - microservices

volumes:
  consul-data:

networks:
  microservices:
    external: true
    

پس از اجرا، UI در http://localhost:8500 در دسترس است.

Register کردن سرویس با Python


# service_registration.py
import consul
import socket
import atexit
import threading
import time

class ConsulRegistration:
    def __init__(
        self,
        service_name: str,
        service_port: int,
        consul_host: str = "consul",
        consul_port: int = 8500,
        tags: list = None,
    ):
        self.service_name = service_name
        self.service_port = service_port
        self.tags = tags or []
        self.service_id = f"{service_name}-{socket.gethostname()}"
        self.consul = consul.Consul(host=consul_host, port=consul_port)
        self._heartbeat_thread = None
        self._stop = threading.Event()
    
    def register(self):
        """register کردن سرویس"""
        # IP خودمان (در Docker، hostname container)
        host_ip = socket.gethostbyname(socket.gethostname())
        
        self.consul.agent.service.register(
            name=self.service_name,
            service_id=self.service_id,
            address=host_ip,
            port=self.service_port,
            tags=self.tags,
            check=consul.Check.http(
                url=f"http://{host_ip}:{self.service_port}/health",
                interval="10s",
                timeout="5s",
                deregister="1m"  # حذف بعد از ۱ دقیقه از کار افتادن
            )
        )
        print(f"Registered {self.service_id} at {host_ip}:{self.service_port}")
        atexit.register(self.deregister)
    
    def deregister(self):
        """خارج کردن سرویس"""
        try:
            self.consul.agent.service.deregister(self.service_id)
            print(f"Deregistered {self.service_id}")
        except Exception as e:
            print(f"Error deregistering: {e}")
    
    def discover(self, service_name: str) -> list:
        """پیدا کردن instance های یک سرویس"""
        _, services = self.consul.health.service(
            service=service_name,
            passing=True  # فقط healthy ها
        )
        return [
            {
                "id": s["Service"]["ID"],
                "address": s["Service"]["Address"],
                "port": s["Service"]["Port"],
                "tags": s["Service"]["Tags"],
            }
            for s in services
        ]

# استفاده در FastAPI
from fastapi import FastAPI

app = FastAPI()
registration = ConsulRegistration(
    service_name="product-service",
    service_port=8002,
    tags=["v1", "python"]
)

@app.on_event("startup")
async def startup():
    registration.register()

@app.on_event("shutdown")
async def shutdown():
    registration.deregister()

@app.get("/health")
async def health():
    return {"status": "healthy"}

@app.get("/products")
async def list_products():
    return [{"id": 1, "name": "Laptop"}]
    

Discovery از سرویس دیگر


# order_service/clients/product_client.py
import consul
import httpx
import random
from typing import Optional

class ServiceDiscoveryClient:
    """Client با service discovery و load balancing"""
    
    def __init__(self, consul_host: str = "consul", consul_port: int = 8500):
        self.consul = consul.Consul(host=consul_host, port=consul_port)
        self._cache = {}  # cache ساده
        self._cache_ttl = 30  # ثانیه
        self._cache_time = {}
    
    def _get_instances(self, service_name: str) -> list:
        """دریافت instance های healthy یک سرویس با cache"""
        import time
        now = time.time()
        
        # cache hit?
        if (service_name in self._cache and 
            now - self._cache_time.get(service_name, 0) < self._cache_ttl):
            return self._cache[service_name]
        
        # query consul
        _, services = self.consul.health.service(
            service=service_name,
            passing=True
        )
        instances = [
            f"http://{s['Service']['Address']}:{s['Service']['Port']}"
            for s in services
        ]
        
        self._cache[service_name] = instances
        self._cache_time[service_name] = now
        return instances
    
    async def call(
        self,
        service_name: str,
        method: str,
        path: str,
        **kwargs
    ) -> Optional[dict]:
        """فراخوانی یک سرویس با load balancing تصادفی"""
        instances = self._get_instances(service_name)
        if not instances:
            raise Exception(f"No healthy instances for {service_name}")
        
        # Random load balancing
        instance = random.choice(instances)
        url = f"{instance}{path}"
        
        async with httpx.AsyncClient(timeout=10.0) as client:
            response = await client.request(method, url, **kwargs)
            response.raise_for_status()
            return response.json()

# استفاده
client = ServiceDiscoveryClient()
product = await client.call(
    service_name="product-service",
    method="GET",
    path="/products/123"
)
    

Consul DNS

Consul یک DNS server هم دارد که بدون نیاز به HTTP، می‌توانید سرویس‌ها را resolve کنید:


# پیدا کردن آدرس‌های healthy
dig @consul -p 8600 product-service.service.consul

# با tag خاص
dig @consul -p 8600 v1.product-service.service.consul

# SRV record برای دریافت port
dig @consul -p 8600 product-service.service.consul SRV
    

Key-Value Store


# ذخیره configuration در Consul
c = consul.Consul()

# نوشتن
c.kv.put("config/product-service/db_host", "postgres-prod.internal")
c.kv.put("config/product-service/cache_ttl", "300")
c.kv.put("config/feature_flags/new_search", "true")

# خواندن
_, data = c.kv.get("config/product-service/db_host")
db_host = data["Value"].decode() if data else None

# Watch (تغییرات real-time)
import threading

def watch_config():
    index = None
    while True:
        index, data = c.kv.get(
            "config/product-service",
            index=index,  # blocking query
            recurse=True
        )
        if data:
            print("Config changed!")
            for item in data:
                print(f"  {item['Key']}: {item['Value']}")

threading.Thread(target=watch_config, daemon=True).start()
    

۵.۵ Netflix Eureka

Eureka یک service registry است که توسط Netflix توسعه داده شده. برای پروژه‌های Java محبوب است اما در Python هم استفاده می‌شود.

اجرای Eureka Server


# docker-compose.yml
version: "3.8"

services:
  eureka:
    image: steeltoeoss/eureka-server
    ports:
      - "8761:8761"
    environment:
      eureka.client.register-with-eureka: false
      eureka.client.fetch-registry: false
    networks:
      - microservices
    

Register در Python


# eureka_client.py
from py_eureka_client import eureka_client

# register
eureka_client.init(
    eureka_server="http://eureka:8761/eureka",
    app_name="product-service",
    instance_port=8002,
    renewal_interval_in_secs=30,  # heartbeat
    duration_in_secs=90,  # timeout
)

# Discovery
client = eureka_client.do_service(
    "USER-SERVICE",  # نام سرویس (uppercase در Eureka)
    "/api/users/123",
    return_type="json"
)
    

مقایسه با Consul:

  • Eureka: ساده‌تر، مخصوص Java/Spring Cloud
  • Consul: قدرتمندتر، multi-purpose، DNS support، KV store

۵.۶ Service Discovery در Kubernetes

اگر از Kubernetes استفاده می‌کنید، service discovery built-in است و نیاز به Consul/Eureka ندارید!

چطور کار می‌کند؟

  1. هر Service object در Kubernetes یک DNS name دریافت می‌کند
  2. kube-dns / CoreDNS این name ها را resolve می‌کند
  3. سرویس‌ها از طریق ClusterIP به یکدیگر دسترسی دارند
  4. Pod ها به‌صورت خودکار register می‌شوند (با label selector)

تعریف Service


# product-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: product-service
  namespace: shop
spec:
  selector:
    app: product
    tier: backend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8002
  type: ClusterIP

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product
      tier: backend
  template:
    metadata:
      labels:
        app: product
        tier: backend
    spec:
      containers:
      - name: product
        image: shop/product-service:latest
        ports:
        - containerPort: 8002
        livenessProbe:
          httpGet:
            path: /health
            port: 8002
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8002
          initialDelaySeconds: 5
          periodSeconds: 5
    

دسترسی از سرویس دیگر


# داخل Kubernetes - بدون نیاز به Consul!
import httpx

# DNS resolution خودکار
async def get_product(product_id: int):
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"http://product-service.shop.svc.cluster.local/products/{product_id}"
        )
        # یا ساده‌تر در همان namespace:
        response = await client.get(
            f"http://product-service/products/{product_id}"
        )
        return response.json()
    

Kubernetes به‌طور خودکار:

  • Load balancing بین Pod ها
  • Health check (با liveness/readiness probes)
  • حذف Pod های ناسالم از rotation
  • Self-healing (restart Pod های failed)

۵.۷ DNS-Based Service Discovery

ساده‌ترین روش discovery — استفاده از DNS که در همه پلتفرم‌ها وجود دارد.

روش‌های پیاده‌سازی

۱. Docker Swarm DNS

# docker-compose.yml
services:
  user-service:
    image: shop/user-service
    deploy:
      replicas: 3
    networks:
      - app

  order-service:
    image: shop/order-service
    networks:
      - app
    # دسترسی از طریق: http://user-service:8001
    # Docker DNS load balance می‌کند

networks:
  app:
    driver: overlay
    
۲. CoreDNS با plugin های سفارشی

# Corefile
cluster.local:53 {
    errors
    health
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
    }
    forward . /etc/resolv.conf
    cache 30
    loop
    reload
}
    
۳. SRV Records

SRV record port را هم برمی‌گرداند:


$ dig _http._tcp.product-service.consul SRV

;; ANSWER SECTION:
_http._tcp.product-service.consul. 0 IN SRV 1 1 8002 product-1.node.consul.
_http._tcp.product-service.consul. 0 IN SRV 1 1 8002 product-2.node.consul.
_http._tcp.product-service.consul. 0 IN SRV 1 1 8002 product-3.node.consul.
    

۵.۸ Health Checks

یک سرویس باید وضعیت خودش را به registry گزارش دهد. در غیر این صورت traffic به سرویس مرده می‌رود.

سطوح Health Check

🟢 Liveness

آیا process زنده است؟

اگر fail شود، restart می‌شود.

🟡 Readiness

آیا آماده traffic است؟

اگر fail شود، از LB حذف می‌شود (ولی restart نمی‌شود).

🔵 Startup

آیا startup تمام شده؟

برای سرویس‌های با startup طولانی.

پیاده‌سازی در FastAPI


# health.py
from fastapi import APIRouter, status, HTTPException
from sqlalchemy.exc import OperationalError
import redis.asyncio as aioredis
import httpx

router = APIRouter()

# Global state
app_state = {
    "ready": False,
    "started_at": None,
}

@router.get("/health/live", status_code=status.HTTP_200_OK)
async def liveness():
    """آیا process زنده است؟"""
    return {"status": "alive"}

@router.get("/health/ready")
async def readiness(db_engine, redis_client):
    """آیا آماده traffic هستیم؟"""
    checks = {}
    
    # Database
    try:
        with db_engine.connect() as conn:
            conn.execute("SELECT 1")
        checks["database"] = "ok"
    except OperationalError as e:
        checks["database"] = f"error: {e}"
        raise HTTPException(503, detail=checks)
    
    # Redis
    try:
        await redis_client.ping()
        checks["redis"] = "ok"
    except Exception as e:
        checks["redis"] = f"error: {e}"
        raise HTTPException(503, detail=checks)
    
    # Dependent services (با احتیاط - circular)
    try:
        async with httpx.AsyncClient(timeout=2.0) as client:
            r = await client.get("http://user-service/health/live")
            r.raise_for_status()
        checks["user_service"] = "ok"
    except Exception:
        checks["user_service"] = "degraded"
        # توجه: نیست مهم برای این سرویس باشیم
    
    return {"status": "ready", "checks": checks}

@router.get("/health/startup")
async def startup_check():
    """آیا startup تمام شده؟"""
    if not app_state["ready"]:
        raise HTTPException(503, detail="still starting")
    return {"status": "started"}
    

Best practices Health Check

  • سبک نگه دارید. health check نباید load زیادی روی سرویس بگذارد
  • سریع باشد. response باید زیر ۱ ثانیه باشد
  • فقط dependency های critical را چک کنید.
  • برای dependency های غیرضروری، graceful degradation.
  • liveness و readiness را جدا کنید.
  • Don’t cache: health check نباید cached باشد
  • Auth-free: health endpoints نیاز به auth ندارند

۵.۹ استراتژی‌های Load Balancing

Round Robin

هر درخواست به instance بعدی به ترتیب می‌رود.


instances = ["s1", "s2", "s3"]
counter = 0
def next_instance():
    nonlocal counter
    instance = instances[counter % len(instances)]
    counter += 1
    return instance
                

Random

یک instance به‌صورت تصادفی انتخاب می‌شود.


import random
def next_instance():
    return random.choice(instances)
                

Least Connections

instance با کمترین connection فعال انتخاب می‌شود.

مناسب برای request های با مدت متفاوت

Weighted Round Robin

instance های قوی‌تر weight بیشتری دارند.


s1: weight=3
s2: weight=2
s3: weight=1
ratio: 3:2:1
                

IP Hash

بر اساس hash IP کلاینت — یک کلاینت همیشه به یک سرور.

برای session affinity

Least Response Time

instance با کمترین latency انتخاب می‌شود.

پیشرفته‌ترین، نیاز به metrics

۵.۱۰ مقایسه ابزارها

ابزار Health Check KV Store Multi-DC Service Mesh پیچیدگی
Consul ✅ (Connect) متوسط
Eureka کم
etcd ❌ (manual) محدود کم
Zookeeper ❌ (manual) زیاد
Kubernetes ✅ (ConfigMap) محدود ✅ (با Istio) زیاد
AWS ELB/ALB کم (managed)
توصیه: اگر Kubernetes استفاده می‌کنید، نیاز به ابزار جدا ندارید. اگر از Docker Compose یا VM استفاده می‌کنید، Consul بهترین انتخاب است.

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

  1. Health check را جدی بگیرید. این مهم‌ترین قسمت service discovery است.
  2. Cache را با احتیاط استفاده کنید. TTL کوتاه (30 ثانیه) معمولاً مناسب است.
  3. Graceful Shutdown. قبل از خاموش شدن، deregister کنید.
  4. Circuit Breaker اضافه کنید. برای instance های ناسالم.
  5. Multiple registry replicas. registry نباید single point of failure باشد.
  6. Monitoring. تعداد instance ها، health، latency را track کنید.
  7. Service mesh را بررسی کنید. برای پروژه‌های بزرگ، Istio یا Linkerd بهتر هستند.
  8. DNS TTL را پایین تنظیم کنید. برای تغییرات سریع آدرس‌ها.
  9. Tag سرویس‌ها. ورژن، environment، region را در tag بگذارید.
  10. Configuration externalize. از KV store برای config استفاده کنید.

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

آنچه آموختیم:
  • Service Discovery امکان پیدا کردن داینامیک سرویس‌ها در محیط متغیر را فراهم می‌کند
  • Client-Side vs Server-Side Discovery — هر کدام مزایا و معایب خود را دارند
  • Consul: قدرتمند، multi-purpose، با KV store و service mesh
  • Eureka: ساده‌تر، مخصوص Spring Cloud / Java
  • Kubernetes: built-in service discovery، نیاز به ابزار جدا ندارد
  • Health check در سه سطح: liveness، readiness، startup
  • استراتژی‌های Load Balancing: Round Robin، Random، Least Connections و…
  • برای production، Consul یا Kubernetes Services ابزارهای اصلی هستند
در فصل بعد: پیام‌رسانی Async به‌صورت پیشرفته — RabbitMQ deep dive، Kafka deep dive، انتخاب درست بین آن‌ها، الگوهای Pub/Sub، Work Queue، RPC over messaging.

نمایش سایت

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

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