服务注册与发现
为什么需要服务注册与发现
在微服务架构中,一个系统往往由数十甚至上百个服务组成,每个服务通常会部署多个实例以实现高可用和水平扩展。传统做法是将服务提供者的 IP 地址硬编码在服务消费者中,这种方式存在诸多问题:
硬编码 IP 的问题:
1. 运维效率低 — 实例扩缩容后需要手动修改所有调用方的配置
2. 可用性差 — 实例宕机后调用方无法感知,请求仍会发往已宕机的节点
3. 负载均衡困难 — 无法在多个实例间智能分配请求
4. 环境割裂 — 开发、测试、生产环境的 IP 配置各自维护,容易出错引入服务注册中心后,服务提供者启动时自动注册自己的地址,服务消费者通过注册中心动态获取可用地址列表,实现了服务提供者与服务消费者的解耦。
无注册中心: 有注册中心:
消费者 ─→ 硬编码 IP:8080 消费者 ─→ 注册中心 ─→ 提供者 A (192.168.1.1:8080)
│ ├─ 提供者 B (192.168.1.2:8080)
│ └─ 提供者 C (192.168.1.3:8080)
│
健康检查 ←───────── 提供者 (自动注册)注册中心的核心功能
| 功能 | 说明 |
|---|---|
| 服务注册 | 服务提供者启动时向注册中心上报自身信息(IP、端口、服务名、元数据等) |
| 服务发现 | 服务消费者从注册中心获取目标服务的可用实例列表 |
| 健康检查 | 注册中心定期探测服务实例的健康状态,剔除异常节点 |
| 监听通知 | 注册中心在服务列表发生变化时主动推送给订阅方(或由客户端轮询) |
核心交互流程
1. 启动注册: Provider 向 Registry 发送注册请求
2. 心跳续约: Provider 定期向 Registry 发送心跳(如 30 秒一次)
3. 服务发现: Consumer 从 Registry 拉取 Provider 列表
4. 监听变化: Consumer 订阅 Provider 列表变更事件
5. 剔除下线: Registry 检测到 Provider 心跳超时,将其移出列表并通知 ConsumerCAP 理论与注册中心选型
CAP 简述
| 属性 | 含义 |
|---|---|
| Consistency | 一致性,所有节点在同一时刻看到的数据相同 |
| Availability | 可用性,每次请求都能获得非错误的响应 |
| Partition Tolerance | 分区容忍性,节点间网络中断时系统仍能正常运行 |
CAP 定理指出,分布式系统最多只能同时满足其中两项。由于网络分区(P)是不可避免的,实际选型是在 CP(强一致)和 AP(高可用)之间权衡。
AP vs CP 的选择
AP 优先(如 Eureka):
- 注册中心不可用时,服务消费者仍可使用本地缓存的服务列表
- 牺牲一致性(短时读到不准确的服务列表)
- 适合:追求可用性的业务系统
CP 优先(如 Zookeeper / Consul):
- 网络分区时注册中心不可写入,保证数据一致
- 牺牲可用性(分区期间无法注册或发现)
- 适合:对数据一致性要求极高的基础设施主流注册中心对比
| 特性 | Eureka | Nacos | Consul | Zookeeper |
|---|---|---|---|---|
| CAP | AP | AP+CP 可切换 | CP | CP |
| 一致性协议 | 无(最终一致) | Distro(AP)/ Raft(CP) | Raft | ZAB |
| 健康检查 | 客户端心跳 | 心跳 + TCP/HTTP 探测 | TCP/HTTP/gRPC + 心跳 | 长连接 Session + 心跳 |
| 自我保护 | 支持 | 支持 | 不支持 | 不支持 |
| 服务发现 | 客户端主动拉取 + 缓存 | 拉取 + UDP 推送 | 拉取 + 长轮询 | 临时节点 + Watcher |
| 配置管理 | 不支持 | 支持(一体化方案) | 支持(KV Store) | 支持(KV 节点) |
| 多数据中心 | 不支持原生 | 支持 | 支持 | 需额外开发 |
| K8s 集成 | 不友好 | 友好 | 友好 | 不友好 |
| 运维复杂度 | 低 | 中 | 中 | 高 |
| 语言/社区 | Java/Netflix | Java/阿里 | Go/HashiCorp | Java/Apache |
Eureka
简介
Eureka 是 Netflix 开源的服务注册中心,属于 Spring Cloud Netflix 体系的核心组件,AP 优先设计。
架构原理
Eureka Server(集群) Eureka Server(集群)
↕ 相互注册、复制 ↕ 相互注册、复制
│ │
└─────── 心跳续约 ──────────────┘
↕
┌─────── 服务消费者 ─────────────┐
│ 1. 拉取注册表(拉 + 缓存) │
│ 2. 增量拉取(增量同步) │
└────────────────────────────────┘- Peer-to-Peer 模式:每个 Server 节点既是注册中心也是其他节点的副本,节点间相互注册复制数据
- 最终一致性:任意节点写入后异步复制到其他节点,允许短暂不一致
- 自我保护机制:当短时间内心跳丢失比例超过阈值(默认 85%)时,Eureka 不再剔除服务,而是进入自我保护模式
自我保护机制
正常情况:
每分钟心跳成功数 ≥ 阈值 → 正常剔除过期实例
自我保护触发:
每分钟心跳成功数 < 阈值(连续 15 分钟)→ 停止剔除实例
→ 保护那些因网络抖动而短暂失联的客户端
→ 待心跳恢复后自动退出自我保护模式配置示例
# application.yml — Eureka Server
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 不注册自己
fetch-registry: false # 不拉取注册表
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
enable-self-preservation: true # 开启自我保护
renewal-percent-threshold: 0.85 # 心跳阈值比例
eviction-interval-timer-in-ms: 5000 # 剔除扫描间隔(毫秒)# application.yml — Eureka Client
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
fetch-registry: true
register-with-eureka: true
instance:
lease-renewal-interval-in-seconds: 30 # 心跳间隔
lease-expiration-duration-in-seconds: 90 # 过期时间(需 3 个心跳间隔以上)Nacos
简介
Nacos(Dynamic Naming and Configuration Service)是阿里开源的一站式微服务基础设施,同时提供服务注册与发现和配置管理能力。支持 AP 和 CP 两种模式动态切换。
架构原理
Nacos Server
├── AP 模式(Distro 协议):
│ - 每个节点独立处理写入,异步同步
│ - 适合服务发现场景(最终一致即可)
│ - 性能高,可用性强
│
└── CP 模式(Raft 协议 + JRaft):
- 写入需 Leader 确认,强一致性
- 适合配置管理场景(数据必须一致)
- 性能适中,一致性高
Client 端:
- 定时拉取注册表(本地缓存)
- 支持 UDP 推送变更
- 1.x 使用心跳续约,2.x 使用 gRPC 长连接临时实例与持久化实例
| 实例类型 | 注册方式 | 健康检查 | 下线策略 |
|---|---|---|---|
| 临时实例 | 心跳续约 | 心跳超时 15 秒剔除 | Server 自动剔除 |
| 持久化实例 | API 注册 | TCP/HTTP 探测 | 手动删除 |
配置示例
# application.yml — Nacos Server(单机模式)
server:
port: 8848
spring:
datasource:
platform: mysql
url: jdbc:mysql://localhost:3306/nacos?useSSL=false
username: root
password: root# application.yml — Nacos Client
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: dev # 命名空间隔离
group: DEFAULT_GROUP # 分组隔离
ephemeral: true # 临时实例(默认)
heart-beat-interval: 5000 # 心跳间隔(毫秒)
heart-beat-timeout: 15000 # 心跳超时(毫秒)# AP / CP 模式切换(nacos 集群)
nacos:
core:
ap: true # true = AP(Distro),false = CP(Raft)Consul
简介
Consul 是 HashiCorp 开源的服务网格解决方案,提供服务注册与发现、健康检查、KV Store、多数据中心 和 安全服务通信 等功能。基于 Raft 协议 实现强一致性(CP)。
架构原理
Consul 集群
┌─────────────── Leader ───────────────┐
│ - 处理所有写入请求 │
│ - Raft 日志复制 │
├───────────── Follower ───────────────┤
│ - 处理读取请求(可配置一致性级别) │
│ - 参与 Leader 选举 │
└──────────────────────────────────────┘
↕ Serf Gossip 协议(成员管理)
↕ 跨数据中心 WAN Gossip
Client Agent(每个节点部署一个)
- 代理服务注册与健康检查
- 缓存服务列表
Agent 模式: 每个节点部署 Consul Agent,服务注册到本地 Agent,Agent 与 Server 通信
无 Agent 模式: 服务直接调用 Consul Server API(K8s 中常用)健康检查类型
| 检查类型 | 配置方式 | 适用场景 |
|---|---|---|
| Script/Shell | 执行脚本检查 | 自定义检查逻辑 |
| TCP | 尝试连接指定端口 | 检查端口是否监听 |
| HTTP | 返回 200 状态码 | 检查 HTTP 端点 |
| gRPC | gRPC 健康检查协议 | gRPC 服务 |
| TTL | 服务端心跳更新 | 由应用自身上报健康状态 |
多数据中心
dc1 (北京) dc2 (上海)
Consul Server ──WAN Gossip── Consul Server
↕ ↕
Agent ... Agent ...- 每个数据中心独立 Raft 集群
- 跨数据中心通过 WAN Gossip 协议交换摘要信息
- 服务发现默认优先本地数据中心
配置示例
# consul.hcl — Consul Server
server = true
bootstrap_expect = 3
data_dir = "/opt/consul/data"
bind_addr = "0.0.0.0"
client_addr = "0.0.0.0"
ui = true
# Raft 相关
raft_protocol = 3
# 多数据中心
datacenter = "dc1"
primary_datacenter = "dc1"
# 自动加入集群
retry_join = ["10.0.1.1", "10.0.1.2", "10.0.1.3"]// 服务注册(HTTP API)
{
"Name": "user-service",
"ID": "user-service-1",
"Address": "10.0.1.10",
"Port": 8080,
"Tags": ["api", "v1"],
"Check": {
"HTTP": "http://10.0.1.10:8080/actuator/health",
"Interval": "10s",
"Timeout": "5s",
"DeregisterCriticalServiceAfter": "30s"
}
}# Spring Cloud Consul 配置
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: user-service
health-check-path: /actuator/health
health-check-interval: 10s
prefer-agent-address: true
tags: api, v1Zookeeper
简介
Zookeeper 是 Apache 开源的分布式协调服务,基于 ZAB 协议 实现强一致性(CP)。虽然不是专门的服务注册中心,但由于其临时节点和Watcher 机制天然适合服务发现场景,被 Dubbo 等框架广泛使用。
架构原理
Zookeeper 集群(Leader + Follower)
┌─────────────── Leader ───────────────┐
│ - 处理所有写入(ZAB 原子广播) │
│ - 事务请求的编排者 │
├─────────── Follower ─────────────────┤
│ - 处理读取请求(直接返回本地数据) │
│ - 参与 Leader 选举 │
│ - 转发写入请求到 Leader │
└──────────────────────────────────────┘
数据模型(树形命名空间):
/services
├── /user-service
│ ├── /192.168.1.1:8080 (临时节点,ephemeral)
│ └── /192.168.1.2:8080 (临时节点,ephemeral)
└── /order-service
├── /192.168.1.3:8080 (临时节点,ephemeral)
└── /192.168.1.4:8080 (临时节点,ephemeral)服务注册与发现机制
服务注册:
1. 在 /services/{service-name} 下创建临时顺序节点
2. 节点数据存放 IP:Port 信息
3. 客户端与 ZK 保持长连接(Session)
4. Session 超时 → 临时节点自动删除
服务发现:
1. 获取 /services/{service-name} 下的子节点列表
2. 解析节点数据得到实例地址
3. 设置 Watcher 监听子节点变化
4. 有节点变化时重新拉取列表
Watcher 通知:
创建 → ChildWatch → 通知客户端增加了一个实例
删除 → ChildWatch → 通知客户端减少了一个实例
Session 断开 → 所有临时节点自动删除ZAB 协议(Zookeeper Atomic Broadcast)
写入流程:
1. Client 请求发送到任意节点
2. 如果不是 Leader,转发到 Leader
3. Leader 生成事务 Proposal
4. Leader 广播 Proposal 到所有 Follower
5. 超过半数 Follower ACK
6. Leader 提交事务并通知 Follower
Leader 选举:
1. 当前 Leader 宕机或失联
2. 进入选举阶段(Fast Leader Election)
3. 投给数据最新的节点,先发起投票的节点优先
4. 获得超过半数投票的节点成为新 Leader与 Consul 的差异
| 特性 | Zookeeper | Consul |
|---|---|---|
| 一致性 | ZAB(类 Paxos) | Raft |
| 健康检查 | Session 心跳(被动) | 多种主动探测(TCP/HTTP/gRPC) |
| 数据模型 | 树形 ZNode | 树形 + KV Store |
| 通知机制 | Watcher(一次性) | 长轮询(可持续) |
| 多数据中心 | 需额外开发 | 原生支持(WAN Gossip) |
| DNS 集成 | 不支持 | 支持(SRV 记录) |
| 管理界面 | 第三方 | 内置 Web UI |
配置示例
# application.yml — Zookeeper Client(Spring Cloud Zookeeper)
spring:
cloud:
zookeeper:
connect-string: localhost:2181
discovery:
root: /services
register: true
instance-host: ${spring.cloud.client.ip-address}
instance-port: ${server.port}<!-- Dubbo 使用 Zookeeper 注册中心 -->
<dubbo:registry address="zookeeper://localhost:2181" />
<!-- 等价注解 -->
@DubboService
public class UserServiceImpl implements UserService {
// ...
}// 原生 Curator 框架操作
public class ZkServiceRegistry {
private final CuratorFramework client;
private static final String BASE_PATH = "/services";
// 注册服务(临时节点)
public void register(String serviceName, String address) throws Exception {
String path = BASE_PATH + "/" + serviceName + "/" + address;
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL) // 临时节点
.forPath(path, address.getBytes(StandardCharsets.UTF_8));
}
// 发现服务
public List<String> discover(String serviceName) throws Exception {
String path = BASE_PATH + "/" + serviceName;
return client.getChildren().forPath(path);
}
// 监听服务变化
public void watch(String serviceName, CuratorWatcher watcher) throws Exception {
String path = BASE_PATH + "/" + serviceName;
client.getChildren().usingWatcher(watcher).forPath(path);
}
}服务发现模式
客户端发现模式
Consumer Registry
│ │
│── 1. 拉取服务列表 ────────────→│
│←── 2. 返回实例列表 ────────────│
│ │
│── 3. 负载均衡(Ribbon)───────→│
│ 选择 提供者-B:8080 │
│ │
│── 4. 直接调用 ───────────────→│ 提供者-B:8080代表实现:Eureka + Ribbon / Spring Cloud LoadBalancer、Nacos + Dubbo
优点:
- 部署简单,无需额外组件
- 消费者可自定义负载均衡策略
缺点:
- 每个消费者都需要实现服务发现逻辑
- 不同语言需要适配不同客户端库
服务端发现模式
Consumer Load Balancer Provider
│ │ │
│── 1. 请求 → service:8080 ────→│ │
│ │── 2. 转发请求 ────────────→│
│ │ │
│ │ 服务发现 + 负载均衡 │
│ │ (用于发现 Provider 列表) │
│ │ ↕ │
│ │ Registry │代表实现:Kubernetes Service + kube-proxy、AWS ELB + ECS、Consul + Consul Template + HAProxy
优点:
- 消费者只需访问一个固定的负载均衡地址
- 语言无关,任何语言都可以通过 HTTP/DNS 调用
缺点:
- 多一层网络跳转,增加延迟
- 负载均衡器成为潜在瓶颈和单点
对比总结
| 维度 | 客户端发现 | 服务端发现 |
|---|---|---|
| 部署依赖 | 无额外组件 | 需要负载均衡器 |
| 语言耦合 | 高(需集成客户端库) | 低(纯网络调用) |
| 网络跳数 | 1 跳(直连) | 2 跳(经 LB) |
| 负载均衡 | 客户端侧控制 | LB 侧统一控制 |
| 典型场景 | 微服务框架内部(Spring Cloud/Dubbo) | 云原生环境(K8s/Istio) |
与 Kubernetes Service 的关系
Kubernetes 的服务发现机制
Kubernetes 原生提供了两种服务发现方式:
1. 环境变量方式:
Pod 启动时注入 Service IP:Port 的环境变量
局限性: 依赖 Pod 启动顺序,无法动态更新
2. DNS 方式(CoreDNS):
每个 Service 对应一个 DNS A/AAAA 记录
Pod 通过 service-name.namespace.svc.cluster.local 访问
kube-proxy 实现 VIP 到 Endpoint 的负载转发对比分析
| 维度 | Kubernetes Service | 传统注册中心(Eureka/Nacos/Consul) |
|---|---|---|
| 发现方式 | DNS + 环境变量 | API + 客户端库 |
| 负载均衡 | kube-proxy(iptables/IPVS) | 客户端侧(Ribbon)或 LB |
| 健康检查 | Readiness Probe(存活就绪探针) | 心跳 + 自定义健康检查 |
| 协议支持 | 四层(TCP/UDP)/七层(Ingress) | 七层(应用层) |
| 调用方式 | 域名访问 Service VIP | 直连 Pod IP |
| 更新延迟 | DNS TTL(秒级) | 即时推送 / 秒级轮询 |
| 语言耦合 | 无(纯 DNS + HTTP) | 需要集成 SDK |
| 适用场景 | 云原生(K8s),跨语言异构应用 | 微服务框架(同语言生态) |
共存与融合
在实际生产环境中,两者往往并存:
Kubernetes 集群
┌───── K8s Service (VIP) ─────┐
│ │
│ ┌─ Pod ─┐ ┌─ Pod ─┐ │
│ │App │ │App │ │
│ │+ SDK │ │+ SDK │ │
│ └───────┘ └───────┘ │
│ │ │ │
│ └────┬────┘ │
│ Eureka/Nacos │ ← 集群内部微服务互调用
└──────────────────────────────┘
│
Ingress / Gateway
│
外部流量入口- 同语言微服务(如 Java Spring Cloud):内部使用 Eureka/Nacos 进行服务发现,利用客户端负载均衡和灰度路由能力
- 跨语言或对外接口:通过 K8s Service DNS 暴露,降低耦合
- Istio/Service Mesh:进一步将服务发现下沉到 Sidecar Proxy,应用层完全透明
实践建议
1. 如果团队技术栈统一(如全 Java),优先使用 Nacos/Eureka + 客户端发现
2. 如果技术栈异构(Java + Go + Python),优先使用 K8s Service + DNS
3. 如果两者都有,可混合使用:内部 RPC 走注册中心,外部 HTTP 走 K8s Service
4. 迁移到 Service Mesh(如 Istio)时,服务发现逐渐下沉到 Sidecar,应用层移除 SDK