Service Discovery(服務發現)
是什麼?
Service Discovery 是微服務架構中自動偵測服務實例的網路位置(IP + Port)的機制。當服務啟動、停止或擴展時,Service Discovery 確保其他服務能找到最新的可用實例。
ℹ️為什麼需要?
在容器化環境中,服務的 IP 和 Port 是動態分配的。每次部署、擴展、故障重啟都可能改變位置。硬編碼 IP 地址在微服務中完全行不通。
核心觀念
兩種模式
| 面向 | Client-Side Discovery | Server-Side Discovery | |------|----------------------|----------------------| | 機制 | Client 直接查詢 Registry,自己做負載均衡 | Client 請求 Load Balancer,LB 查詢 Registry | | 代表 | Netflix Eureka + Ribbon | AWS ALB + ECS、Kubernetes Service | | 優點 | 減少一層網路跳轉 | Client 不需要 Discovery 邏輯 | | 缺點 | 每個 Client 都要整合 SDK | 多一層 LB,但簡化 Client |
常見工具比較
| 工具 | 類型 | 健康檢查 | 一致性模型 | 適用環境 | |------|------|----------|-----------|---------| | Consul | Registry + KV Store | HTTP/TCP/gRPC | Raft(CP) | 跨平台 | | Eureka | Registry | Client Heartbeat | AP(最終一致) | Spring Cloud | | etcd | KV Store | TTL Lease | Raft(CP) | Kubernetes | | Kubernetes DNS | DNS-based | Pod Probe | — | Kubernetes |
健康檢查機制
- Heartbeat:服務定期向 Registry 發送心跳,超時未收到就標記為不健康
- Active Check:Registry 主動向服務發送健康檢查請求(HTTP GET /health)
- TTL(Time to Live):服務必須在 TTL 到期前更新註冊,否則自動移除
常見誤區
⚠️常犯錯誤
- 健康檢查只檢查服務是否活著(還要檢查它是否能正常處理請求)
- Registry 本身沒有高可用部署(Registry 掛了,所有服務都找不到彼此)
- 服務停止時沒有反註冊(Deregister),導致其他服務呼叫已關閉的實例
- 在 Kubernetes 環境中額外部署 Consul(K8s 內建的 Service Discovery 通常就夠用)
執行流程
服務啟動
服務啟動後向 Registry 註冊自己的 IP、Port、健康檢查端點
健康檢查
Registry 定期檢查服務是否健康,標記不健康的實例
服務查詢
Client 查詢 Registry 取得目標服務的可用實例清單
負載均衡
Client 或 LB 從可用實例中選擇一個發送請求
服務停止
服務 graceful shutdown 時主動反註冊
流程解讀:服務發現是一個持續運作的生命週期管理。服務啟動時「報到」(Register),持續「簽到」(Health Check),停止時「簽退」(Deregister)。Registry 維護一份即時的服務實例清單,其他服務透過查詢這份清單來找到目標服務。健康檢查確保清單中只有真正可用的實例。
程式碼範例
C# 版本
// Consul 服務註冊(ASP.NET Core)
using Consul;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConsulClient>(
new ConsulClient(c => c.Address = new Uri("http://consul:8500")));
var app = builder.Build();
// 啟動時註冊
app.Lifetime.ApplicationStarted.Register(async () =>
{
var consul = app.Services.GetRequiredService<IConsulClient>();
await consul.Agent.ServiceRegister(new AgentServiceRegistration
{
ID = $"order-service-{Environment.MachineName}",
Name = "order-service",
Address = "order-service",
Port = 80,
Check = new AgentServiceCheck
{
HTTP = "http://order-service:80/health",
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5),
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
}
});
});
// 停止時反註冊
app.Lifetime.ApplicationStopping.Register(async () =>
{
var consul = app.Services.GetRequiredService<IConsulClient>();
await consul.Agent.ServiceDeregister(
$"order-service-{Environment.MachineName}");
});
// 查詢服務
app.MapGet("/api/discover/{serviceName}", async (
string serviceName, IConsulClient consul) =>
{
var services = await consul.Health.Service(serviceName, tag: null,
passingOnly: true);
return services.Response.Select(s => new
{
s.Service.Address,
s.Service.Port
});
});TypeScript 版本
import Consul from "consul";
const consul = new Consul({ host: "consul", port: 8500 });
const serviceId = `order-service-${process.env.HOSTNAME}`;
// 註冊服務
async function register() {
await consul.agent.service.register({
id: serviceId,
name: "order-service",
address: "order-service",
port: 3001,
check: {
http: "http://order-service:3001/health",
interval: "10s",
timeout: "5s",
deregistercriticalserviceafter: "1m",
},
});
}
// 反註冊
async function deregister() {
await consul.agent.service.deregister(serviceId);
}
process.on("SIGTERM", async () => {
await deregister();
process.exit(0);
});
// 查詢服務並呼叫
async function callService(serviceName: string, path: string) {
const result = await consul.health.service({
service: serviceName,
passing: true,
});
if (result.length === 0) throw new Error(`No healthy ${serviceName}`);
// 簡單的 Round Robin 負載均衡
const index = Math.floor(Math.random() * result.length);
const { Address, Port } = result[index].Service;
return fetch(`http://${Address}:${Port}${path}`);
}Python 版本
import consul
import socket
import atexit
c = consul.Consul(host="consul", port=8500)
service_id = f"order-service-{socket.gethostname()}"
# 註冊
def register():
c.agent.service.register(
name="order-service",
service_id=service_id,
address="order-service",
port=8001,
check=consul.Check.http(
"http://order-service:8001/health",
interval="10s", timeout="5s",
deregister="1m"
)
)
# 反註冊
def deregister():
c.agent.service.deregister(service_id)
atexit.register(deregister)
# 查詢服務
def discover(service_name: str) -> list[tuple[str, int]]:
_, services = c.health.service(service_name, passing=True)
return [
(s["Service"]["Address"], s["Service"]["Port"])
for s in services
]
# Kubernetes 中的服務發現(內建 DNS)
# 不需要額外 Registry,直接用 Service DNS 名稱
# http://order-service.default.svc.cluster.local:80/api/orders
# 簡寫:http://order-service/api/orders(同 namespace)結構圖
圖中 Order Service 的兩個 instance 各自向 Service Registry(Consul)註冊並持續發送心跳。Payment Service 作為 Client 可以直接查詢 Registry 取得可用實例清單(Client-Side Discovery),或者透過 Load Balancer 間接存取(Server-Side Discovery)。Registry 確保 Client 永遠拿到最新的健康實例列表。
面試常見問題
Q: Client-Side 和 Server-Side Discovery 各自的優缺點?
A: Client-Side(如 Eureka + Ribbon)少了一層 LB 跳轉,延遲較低,但每個 Client 都要整合 Discovery SDK,增加了 Client 的複雜度。Server-Side(如 K8s Service + kube-proxy)Client 完全不用管 Discovery,但多了 LB 這一層。Kubernetes 環境中 Server-Side 是主流,因為 K8s 原生支援。
Q: Consul 和 Kubernetes DNS 的差異?何時需要額外的 Service Discovery?
A: K8s DNS 提供基本的服務名稱解析,足以應付大多數場景。Consul 額外提供跨叢集/跨雲的服務發現、Key-Value Store、Service Mesh 功能。如果系統全在一個 K8s 叢集內,通常不需要額外的 Discovery。跨叢集、跨雲、或混合環境時才需要 Consul。
Q: 服務發現中的一致性問題怎麼處理?
A: CP 系統(Consul、etcd)在網路分區時可能拒絕讀取,保證一致但犧牲可用性。AP 系統(Eureka)允許讀取可能過時的資料,保證可用但資料不一定最新。選擇取決於業務需求:金融交易偏 CP,一般 Web 服務偏 AP。
理解測驗
🤔 為什麼微服務需要 Service Discovery?
🤔 服務停止時最重要的動作是什麼?
🤔 在純 Kubernetes 環境中,通常使用哪種 Service Discovery?
重點整理
💡一句話記住
Service Discovery = 服務的 GPS,隨時知道每個服務在哪裡。 口訣:「啟動要註冊,運行要心跳,停止要反註冊」
| 概念 | 說明 | |------|------| | Service Registry | 維護所有服務實例位置的中央資料庫 | | Health Check | 定期確認服務是否健康可用 | | Client-Side | Client 查詢 Registry 自己做負載均衡 | | Server-Side | 透過 LB 間接存取,Client 不需 Discovery 邏輯 | | 核心原則 | 註冊、心跳、反註冊三步缺一不可 |