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 |

健康檢查機制

常見誤區

⚠️常犯錯誤

  • 健康檢查只檢查服務是否活著(還要檢查它是否能正常處理請求)
  • Registry 本身沒有高可用部署(Registry 掛了,所有服務都找不到彼此)
  • 服務停止時沒有反註冊(Deregister),導致其他服務呼叫已關閉的實例
  • 在 Kubernetes 環境中額外部署 Consul(K8s 內建的 Service Discovery 通常就夠用)

執行流程

1

服務啟動

服務啟動後向 Registry 註冊自己的 IP、Port、健康檢查端點

2

健康檢查

Registry 定期檢查服務是否健康,標記不健康的實例

3

服務查詢

Client 查詢 Registry 取得目標服務的可用實例清單

4

負載均衡

Client 或 LB 從可用實例中選擇一個發送請求

5

服務停止

服務 graceful shutdown 時主動反註冊

流程解讀:服務發現是一個持續運作的生命週期管理。服務啟動時「報到」(Register),持續「簽到」(Health Check),停止時「簽退」(Deregister)。Registry 維護一份即時的服務實例清單,其他服務透過查詢這份清單來找到目標服務。健康檢查確保清單中只有真正可用的實例。

程式碼範例

C# 版本

csharp
// 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 版本

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 版本

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 1)
register + heartbeat
Order Service (instance 2)
register + heartbeat
Service Registry (Consul)
Payment Service (Client)
query instances
Load Balancer

圖中 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 邏輯 | | 核心原則 | 註冊、心跳、反註冊三步缺一不可 |

你可能也想看

Saga PatternCircuit Breaker

按 ← → 鍵切換課程