---
title: 服务发现
url: https://doc.liz6.com/distributed-systems/05-members-and-discovery/03-service-discovery
locale: zh
area: distributed-systems
tags:
- distributed-systems
- 成员与发现
date: 2026-06-30
modified: 2026-07-11
description: 在动态扩缩的集群里,调用方怎么知道该连哪个 IP?服务发现把"注册→健康检查→查询"做成标准流程:consul/etcd 存注册表,DNS-SD 和客户端负载均衡是两种不同的查询路径,health check 决定"这个实例还能不能接流量"。
---

# 服务发现

> 在动态扩缩的集群里,调用方怎么知道该连哪个 IP?服务发现把"注册→健康检查→查询"做成标准流程:consul/etcd 存注册表,DNS-SD 和客户端负载均衡是两种不同的查询路径,health check 决定"这个实例还能不能接流量"。

## 问题

服务实例的 IP:port 是动态变化的（扩缩容、滚动更新、故障迁移）。调用方如何知道"现在有哪些实例可用"？

## DNS-SD (Service Discovery via DNS)

利用成熟的 DNS 基础设施做服务发现：

```
服务: api-service → DNS SRV records:
  _api._tcp.example.com SRV 10 60 8080 instance-1.example.com
  _api._tcp.example.com SRV 10 40 8080 instance-2.example.com

client: dig SRV _api._tcp.example.com → [instance-1:8080, instance-2:8080]
```

优势：无需额外组件。局限：DNS TTL 决定变更延迟（通常 30-60s），没有原生健康检查。

## Consul/etcd 服务注册

服务实例启动 → register to Consul/etcd (with health check URL) → client 查询 → 返回 healthy instances:

<svg viewBox="0 0 720 340" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Consul/etcd 服务注册与发现:实例注册、健康检查、客户端查询">
  <defs><marker id="sdah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="340" fill="#ffffff"/>
  <text x="360" y="30" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Consul/etcd 服务注册与发现:注册 → 健康检查 → 客户端查询</text>

  <rect x="40" y="60" width="170" height="44" rx="8" fill="#e2e8f0"/>
  <text x="125" y="87" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">Instance-1</text>
  <rect x="40" y="124" width="170" height="44" rx="8" fill="#e2e8f0"/>
  <text x="125" y="151" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">Instance-2</text>

  <rect x="460" y="60" width="220" height="46" rx="8" fill="#4f46e5"/>
  <text x="570" y="88" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Consul Server</text>

  <rect x="460" y="136" width="220" height="64" rx="8" fill="#0d9488"/>
  <text x="570" y="162" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">Consul Agent</text>
  <text x="570" y="182" text-anchor="middle" font-size="11" fill="#ccfbf1">健康检查 GET /health</text>

  <rect x="250" y="238" width="220" height="48" rx="8" fill="#e2e8f0"/>
  <text x="360" y="266" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">Client</text>

  <line x1="210" y1="82" x2="458" y2="83" stroke="#475569" stroke-width="1.6" marker-end="url(#sdah)"/>
  <text x="335" y="70" text-anchor="middle" font-size="11" fill="#475569">register(host:8080)</text>
  <line x1="210" y1="146" x2="458" y2="90" stroke="#475569" stroke-width="1.6" marker-end="url(#sdah)"/>
  <text x="300" y="132" text-anchor="middle" font-size="11" fill="#475569">register(host:8080)</text>

  <line x1="570" y1="106" x2="570" y2="134" stroke="#475569" stroke-width="1.6" marker-end="url(#sdah)"/>

  <line x1="360" y1="236" x2="562" y2="202" stroke="#475569" stroke-width="1.6" marker-end="url(#sdah)"/>
  <text x="465" y="212" text-anchor="middle" font-size="11" fill="#475569">GET /v1/health/service/api</text>
  <text x="465" y="227" text-anchor="middle" font-size="10" fill="#64748b">(返回 healthy 实例列表)</text>

  <rect x="40" y="298" width="640" height="32" rx="8" fill="#eef2ff"/>
  <text x="360" y="318" text-anchor="middle" font-size="12" fill="#3730a3">register 只建立注册关系;真正决定「能不能被发现」的是 Consul Agent 持续的健康检查。</text>
</svg>

支持：HTTP API、DNS 接口（兼容 DNS-SD）、long-poll watch（变更推）。

## Client-side vs Server-side LB

```
Client-side:
  [Client] → 查询服务发现 → 得到 [I1, I2, I3] → 自己选一个 → 直连
  例子: gRPC (with Consul/etcd resolver), Finagle, netflix-eureka

Server-side:
  [Client] → LB (固定地址) → LB → [I1, I2, I3]
  例子: kube-proxy, nginx upstream, HAProxy
```

对比：client-side 少一跳（latency↓），但 client 要维护服务发现逻辑。Server-side client 无需感知后端变化，但 LB 成为瓶颈和单点。

## Health Check

- **Passive**: 从实际请求的 failure（HTTP 5xx, TCP RST）判断。不需要额外请求，但可能被偶尔的 transient error 误判
- **Active**: 定期 GET /health → 期望 200。可靠但增加负载
- **TTL-based (Consul)**: agent 定期向 Consul 报告健康状态。超时未收到 report → 标记 unhealthy → 从发现中移除

## 参考

- **Consul**: consul.io/docs/discovery
- **etcd**: etcd.io/docs
- **gRPC name resolution**: github.com/grpc/grpc/blob/master/doc/naming.md

*Keywords: service discovery, DNS-SD, Consul, etcd, health check, client-side LB, server-side LB*
