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 ندارید!
چطور کار میکند؟
- هر
Serviceobject در Kubernetes یک DNS name دریافت میکند - kube-dns / CoreDNS این name ها را resolve میکند
- سرویسها از طریق ClusterIP به یکدیگر دسترسی دارند
- 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) |
۵.۱۱ بهترین تجربیات
- Health check را جدی بگیرید. این مهمترین قسمت service discovery است.
- Cache را با احتیاط استفاده کنید. TTL کوتاه (30 ثانیه) معمولاً مناسب است.
- Graceful Shutdown. قبل از خاموش شدن، deregister کنید.
- Circuit Breaker اضافه کنید. برای instance های ناسالم.
- Multiple registry replicas. registry نباید single point of failure باشد.
- Monitoring. تعداد instance ها، health، latency را track کنید.
- Service mesh را بررسی کنید. برای پروژههای بزرگ، Istio یا Linkerd بهتر هستند.
- DNS TTL را پایین تنظیم کنید. برای تغییرات سریع آدرسها.
- Tag سرویسها. ورژن، environment، region را در tag بگذارید.
- 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 ابزارهای اصلی هستند