194 lines
8.0 KiB
Markdown
194 lines
8.0 KiB
Markdown
---
|
||
tags: [arch/microservice, service-discovery, eureka, nacos, consul, cap-theory, health-check]
|
||
create time: 2026-08-08 18:00
|
||
update time: 2026-08-08 18:00
|
||
---
|
||
|
||
# 服务注册与发现
|
||
|
||
## 概述
|
||
|
||
服务注册与发现是微服务架构的基石。当服务实例动态扩缩容时,调用方需要知道"当前谁在提供服务"以及"它们在哪"。本文对比 Eureka、Nacos、Consul 三大主流方案的核心设计差异,分析 CAP 理论在服务注册中心中的取舍,以及健康检查的各种实现方式。
|
||
|
||
## 核心原理
|
||
|
||
### 服务注册/注销/续约流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Client as 客户端
|
||
participant Server as 服务提供者
|
||
participant Registry as 注册中心
|
||
participant Discover as 消费方
|
||
Client->>Server: 启动服务
|
||
Server->>Registry: 1. 注册服务 (POST /register)
|
||
Registry-->>Server: 注册成功
|
||
Server->>Registry: 2. 心跳续约 (POST /heartbeat)
|
||
Registry->>Registry: 更新 lastDirtyTimestamp
|
||
loop 每 N 秒
|
||
Server->>Registry: 心跳续约
|
||
Registry-->>Server: ACK
|
||
end
|
||
Discover->>Registry: 3. 查询服务列表 (GET /instances)
|
||
Registry-->>Discover: 返回健康实例列表
|
||
Discover->>Server: 发起 RPC 调用
|
||
Note over Server: 服务关闭
|
||
Server->>Registry: 4. 主动注销 (DELETE /deregister)
|
||
Registry-->>Server: 注销成功
|
||
Registry->>Registry: 从服务列表中移除
|
||
```
|
||
|
||
**三步走**:注册 → 续约(或心跳)→ 注销。绝大多数故障场景下,调用方感知到服务宕机会延迟一个续约超时周期(通常 30~90 秒)。
|
||
|
||
### Eureka — AP 取向的自我修复
|
||
|
||
Eureka 是 Netflix 开源的方案,设计上严格遵循 **AP(可用性 + 分区容错性)**:
|
||
|
||
- **双副本架构**:Peer Replication 机制将数据异步同步给所有节点,不同步视为正常而非故障。
|
||
- **自我保护机制**:当 15 分钟内超过 85% 实例未发送心跳,Eureka 锁定保护窗口,不再剔除任何实例。这意味着被误判为宕机的实例仍会出现在服务列表中——宁可多调几次再失败,也不主动剔除。
|
||
- **客户端缓存**:消费者缓存服务列表(默认每 30 秒刷新),即使注册中心完全不可用,消费方依然能根据本地缓存继续通信。
|
||
|
||
```java
|
||
// Eureka 自我保护机制触发条件
|
||
public boolean shouldEnableRenewments() {
|
||
double threshold = registry.getRenewalThreshold(); // 85%
|
||
if (serverConfig.isSelfPreservationEnabled()
|
||
&& registry.getCachedInstanceStatus().equals(HEAVY_OVERLOAD)) {
|
||
return true; // 进入自我保护,允许续约
|
||
}
|
||
return super.shouldEnableRenewments();
|
||
}
|
||
```
|
||
|
||
> [!WARNING]
|
||
> 自我保护机制在生产环境是个双刃剑:它保证了高可用,但可能导致流量打向已宕机的实例。建议监控 `eureka.server.selfPreservationThreshold` 指标。
|
||
|
||
### Nacos — CP/AP 可切换的统一平台
|
||
|
||
Nacos 解决了 Eureka 和 ZooKeeper 只能二选一的问题:
|
||
|
||
| 模式 | 适用场景 | 一致性协议 | 典型部署形态 |
|
||
|------|---------|-----------|------------|
|
||
| AP 模式 | 服务注册发现 | Distro 协议(自研,类似 Eureka 的双写) | 集群部署 |
|
||
| CP 模式 | 配置管理 | Raft 协议 | 奇数节点集群 |
|
||
|
||
**统一命名空间管理**:Nacos 通过 `namespace` 实现环境隔离(dev/test/prod),每个命名空间有独立的服务和配置视图。底层基于 PostgreSQL 存储元数据,天然支持持久化。
|
||
|
||
```go
|
||
// Nacos Go SDK 服务订阅示例
|
||
client, _ := nacos.NewClient(nacos.Config{
|
||
ServerAddr: "127.0.0.1:8848",
|
||
NamespaceId: "prod-ns",
|
||
})
|
||
|
||
info, _ := client.Subscribe(&nacos.Subscriber{
|
||
ServiceName: "user-service",
|
||
Cluster: "DEFAULT",
|
||
OnEvent: func(event *nacos.Event) {
|
||
for _, inst := range event.Hosts {
|
||
fmt.Println(inst.InstanceIp, inst.InstancePort)
|
||
}
|
||
},
|
||
})
|
||
```
|
||
|
||
> [!TIP]
|
||
> 面试常考点:为什么 Nacos 能在同一套代码中既做 AP 又做 CP?答:两种模式使用不同的数据存储路径和服务类型——`nacos.v1.auth.user`(CP,Raft)与 `nacos.v1.instance`(AP,Distro)。
|
||
|
||
### Consul — Serf Gossip + 丰富健康检查
|
||
|
||
HashiCorp 出品的 Consul 有三个独特优势:
|
||
|
||
1. **Serf gossip 协议**:基于 WAN Gossip 的成员管理,相比 TCP 直连更容错。节点通过 pull-based push-based 混合机制传播成员变更。
|
||
2. **丰富的健康检查**:支持 TCP、HTTP、HTTP Body、docker、脚本五种检查方式。还支持 TTL(手动报告状态)模式。
|
||
3. **KV 存储能力**:内嵌 Raft 一致性 KV 存储,可用于配置管理、分布式锁等辅助场景。
|
||
|
||
```go
|
||
// Consul HTTP 健康检查配置
|
||
func registerCheck() *api.AgentServiceCheck {
|
||
return &api.AgentServiceCheck{
|
||
HTTP: "http://localhost:8080/health",
|
||
Timeout: "5s",
|
||
Interval: "10s",
|
||
DeregisterCriticalServiceAfter: "90s",
|
||
}
|
||
}
|
||
```
|
||
|
||
### CAP 理论在服务注册中心的体现
|
||
|
||
| 注册中心 | CAP 选择 | 网络分区时的行为 | 数据一致性保证 |
|
||
|---------|---------|-----------------|--------------|
|
||
| Eureka | AP | 各节点独立响应,可能返回过期数据 | 最终一致 |
|
||
| Nacos (AP) | AP | Distro 协议异步复制,容忍短暂不一致 | 最终一致 |
|
||
| Nacos (CP) | CP | 选出 Leader 后响应,部分节点不可用时降级 | 强一致 |
|
||
| Consul | CP(TCP)/ AP(DNS) | Raft Leader 不可达时无法写入 | 读可能 stale |
|
||
|
||
> [!NOTE]
|
||
> PACELC 补充:CAP 只讨论网络分区场景。PACELC 指出:无分区时,延迟(Latency)与一致性(Consistency)仍需权衡。Nacos AP 模式、Eureka 都是选择了低延迟而非强一致。
|
||
|
||
### 健康检查方式对比
|
||
|
||
| 方式 | 原理 | 优点 | 缺点 |
|
||
|------|-----|------|------|
|
||
| TCP | 尝试建立 TCP 连接 | 轻量、通用 | 无法感知应用层故障 |
|
||
| HTTP/HTTPS | 请求指定 endpoint 检查状态码 | 灵活,可携带业务逻辑 | 增加服务器负载 |
|
||
| gRPC | gRPC Health Checking Protocol | 适合 gRPC 服务 | 需额外 proto 定义 |
|
||
| Script | 执行自定义脚本 | 最灵活,可做深度检查 | 维护成本高 |
|
||
| Edge探测 | 模拟真实用户请求 | 最贴近用户体验 | 复杂度高,一般用于 SLA 监控 |
|
||
|
||
## 代码示例
|
||
|
||
以下是一个简化版的 Go 服务注册中心心跳续约逻辑:
|
||
|
||
```go
|
||
type Heartbeat struct {
|
||
ServiceID string
|
||
InstanceID string
|
||
LastPingAt time.Time
|
||
}
|
||
|
||
type Registry struct {
|
||
instances map[string]*Instance // serviceID -> Instance
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// Register 注册一个新实例
|
||
func (r *Registry) Register(svc, instanceID, addr string) error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
if _, ok := r.instances[svc]; !ok {
|
||
r.instances[svc] = &Instance{Addr: addr}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Renew 续约:更新最后心跳时间
|
||
func (r *Registry) Renew(svc, instanceID string) bool {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
inst, ok := r.instances[svc]
|
||
if !ok {
|
||
return false
|
||
}
|
||
inst.LastHeartbeat = time.Now()
|
||
return true
|
||
}
|
||
```
|
||
|
||
## 实践场景
|
||
|
||
**秋招高频问题:**
|
||
|
||
- "Eureka 的自我保护机制什么时候该关?" — 答案:金融类对一致性要求高的场景可以考虑关闭,普通互联网业务不建议关。关了之后可能出现"雪崩式下线"——一批实例同时被认为是故障而全部剔除。
|
||
- "Nacos 和 Eureka 有什么本质区别?" — Eureka 只管服务发现,Nacos 是一体化平台(服务发现 + 配置中心)。Nacos 还能按 namespace/group/key 精确分组配置。
|
||
- "Consul 的 DNS 接口为什么是 AP 模式?" — DNS 本身是无状态的,Consul 的 DNS API 直接指向随机节点读取本地内存缓存,不经过 Raft 日志,因此速度极快但可能读到旧数据。
|
||
|
||
**实战建议:** 小团队用一个 Nacos 集群就够用;大规模多机房场景考虑 Consul 的多数据中心能力。
|
||
|
||
## 关联笔记
|
||
|
||
- [[负载均衡算法]]
|
||
- [[限流熔断降级]]
|
||
- [[配置中心设计]]
|