206 lines
6.1 KiB
Markdown
206 lines
6.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: [microservice, service-discovery, consul, nacos, etcd, kubernetes]
|
|||
|
|
create time: 2026-05-05
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 服务发现
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:**我怎么找到你?**
|
|||
|
|
|
|||
|
|
> [!question] 引出问题
|
|||
|
|
> 假设你的订单服务有 3 个实例,运行在 `10.0.1.1:8080`、`10.0.1.2:8080`、`10.0.1.3:8080`。当其中一个实例扩容下线时,调用方如何得知最新的地址列表?手动改配置滚动重启?还是……有更好的办法?
|
|||
|
|
|
|||
|
|
## 两种发现模式
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
subgraph Client["客户端发现模式"]
|
|||
|
|
A[Service A] -->|1.查询注册中心| R1[(Registry)]
|
|||
|
|
R1 -->|2.返回实例列表| A
|
|||
|
|
A -->|3.自选实例直连| B[Service B]
|
|||
|
|
noteA[客户端内置负载均衡逻辑]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph Server["服务端发现模式"]
|
|||
|
|
C[Service C] -->|请求| P[LoadBalancer Proxy]
|
|||
|
|
P -->|查询注册中心| R2[(Registry)]
|
|||
|
|
R2 -->|返回实例| P
|
|||
|
|
P -->|转发| D[Service D]
|
|||
|
|
noteC[客户端无感知,透明代理]
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**对比**:
|
|||
|
|
|
|||
|
|
| 维度 | 客户端发现 | 服务端发现 |
|
|||
|
|
|------|-----------|-----------|
|
|||
|
|
| 典型实现 | Eureka、Consul、Nacos | Nginx、Envoy、K8s Service |
|
|||
|
|
| 耦合度 | 客户端嵌入注册逻辑 | 透明代理,客户端无感知 |
|
|||
|
|
| 性能 | 直连调用,延迟最低 | 多一跳代理开销 |
|
|||
|
|
| 运维复杂度 | 每门语言需独立 SDK | 统一部署代理,技术无关 |
|
|||
|
|
| 灵活性 | 客户端可自定义路由策略 | 受限于代理能力 |
|
|||
|
|
|
|||
|
|
### 客户端发现的优劣势分析
|
|||
|
|
|
|||
|
|
**优势**:
|
|||
|
|
- 直连调用,少了一跳网络开销
|
|||
|
|
- 客户端可以根据本地缓存做智能选择(如就近节点优先)
|
|||
|
|
- 框架成熟,Eureka/Consul 社区案例丰富
|
|||
|
|
|
|||
|
|
**劣势**:
|
|||
|
|
- 每个服务实现中都需要集成 SDK,跨语言迁移成本高
|
|||
|
|
- 客户端需要处理实例列表的缓存与刷新逻辑
|
|||
|
|
- 新增服务时需要修改调用方的代码或配置
|
|||
|
|
|
|||
|
|
### 服务端发现的优劣势分析
|
|||
|
|
|
|||
|
|
**优势**:
|
|||
|
|
- 完全解耦——调用方只需知道 Proxy 地址
|
|||
|
|
- 统一治理入口,可以在代理层加限流、熔断等逻辑
|
|||
|
|
- 适合多语言团队,无需各自维护 SDK
|
|||
|
|
|
|||
|
|
**劣势**:
|
|||
|
|
- 额外一跳增加延迟(通常 <1ms,高吞吐场景累积明显)
|
|||
|
|
- Proxy 本身成为单点瓶颈,必须做横向扩展
|
|||
|
|
- 调测复杂度上升——出了问题要看两层日志
|
|||
|
|
|
|||
|
|
## 主流注册中心选型
|
|||
|
|
|
|||
|
|
### Nacos(推荐国内团队首选)
|
|||
|
|
|
|||
|
|
阿里开源,支持 AP(临时实例)+ CP(持久实例)双模型,同时提供配置管理功能。
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Nacos 服务注册 & 拉取
|
|||
|
|
import (
|
|||
|
|
"github.com/nacos-group/nacos-sdk-go/v2/clients"
|
|||
|
|
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 创建注册客户端
|
|||
|
|
sc := constant.ServerConfig{
|
|||
|
|
IpAddr: "127.0.0.1",
|
|||
|
|
Port: 8848,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注册服务实例
|
|||
|
|
client, _ := clients.NewNamingClient(
|
|||
|
|
value_map.NewValueMap(map[string]any{"serverConfig": sc}),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
client.RegisterInstance(naming_param.RegisterInstanceParam{
|
|||
|
|
Ip: "10.0.0.1",
|
|||
|
|
Port: 8080,
|
|||
|
|
ServiceName: "order-service",
|
|||
|
|
ClusterName: "DEFAULT",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 拉取某服务的可用实例列表
|
|||
|
|
instances, _ := client.SelectInstances(naming_param.SelectInstanceParam{
|
|||
|
|
ServiceName: "order-service",
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Consul
|
|||
|
|
|
|||
|
|
HashiCorp 出品,基于 Raft 共识协议(CP),天然支持健康检查和 DNS 接口。
|
|||
|
|
|
|||
|
|
**核心特点**:
|
|||
|
|
- 内建 KV 存储,可作为轻量配置中心
|
|||
|
|
- 支持 multi-datacenter 部署
|
|||
|
|
- DNS 接口:`order-service.service.consul` 可直接解析 IP
|
|||
|
|
|
|||
|
|
### etcd + K8s Service
|
|||
|
|
|
|||
|
|
纯容器环境下最自然的选择。etcd 存数据,K8s 提供完整的发现和负载均衡体系。
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# K8s Service — 服务端发现模式的代表
|
|||
|
|
apiVersion: v1
|
|||
|
|
kind: Service
|
|||
|
|
metadata:
|
|||
|
|
name: order-service
|
|||
|
|
spec:
|
|||
|
|
selector:
|
|||
|
|
app: order
|
|||
|
|
ports:
|
|||
|
|
- port: 80
|
|||
|
|
targetPort: 8080
|
|||
|
|
type: ClusterIP
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
调用方只需 `http://order-service:80`,K8s 通过 iptables/IPVS 自动实现负载均衡。
|
|||
|
|
|
|||
|
|
### Eureka
|
|||
|
|
|
|||
|
|
Netflix 开源,AP 模型。**注意**:Eureka 2.x 已开源关闭,不建议新项目使用。已有系统在迁移到 Nacos 或 Consul 之前可以继续用。
|
|||
|
|
|
|||
|
|
## 关键机制
|
|||
|
|
|
|||
|
|
### 服务注册流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant S as 服务实例
|
|||
|
|
participant R as 注册中心
|
|||
|
|
participant H as 健康检查器
|
|||
|
|
|
|||
|
|
S->>R: 1. Register(instance)
|
|||
|
|
R-->>S: 2. ACK
|
|||
|
|
Note over S,R: 实例写入内存 / etcd
|
|||
|
|
|
|||
|
|
loop 定时心跳
|
|||
|
|
S->>R: 3. Heartbeat (每 5s)
|
|||
|
|
R-->>S: 4. ACK
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
R->>H: 5. 超过心跳间隔未收到 → 标记不健康
|
|||
|
|
H->>R: 6. 摘除实例
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 临时实例 vs 持久实例
|
|||
|
|
|
|||
|
|
| 类型 | 宕机检测方式 | 数据存储 | 适用场景 |
|
|||
|
|
|------|-------------|---------|---------|
|
|||
|
|
| **临时实例 (ephemeral)** | 心跳超时自动摘除 | 内存 | Web 服务、应用实例 |
|
|||
|
|
| **持久实例 (persistent)** | 主动注销才摘除 | etcd | 数据库连接、外部依赖 |
|
|||
|
|
|
|||
|
|
> [!warning] 常见陷阱
|
|||
|
|
> 把数据库当成临时实例注册 —— 数据库挂了不需要被 "检测到",它自己控制启停。应该用持久实例。
|
|||
|
|
|
|||
|
|
### 优雅上下线
|
|||
|
|
|
|||
|
|
服务下线时不能直接断网,否则正在处理的请求会失败。
|
|||
|
|
|
|||
|
|
**上线流程**:
|
|||
|
|
1. 进程启动 → 加载配置、初始化连接池
|
|||
|
|
2. `/ready` 探针返回 200 → 注册中心注册 + K8s 加入负载均衡池
|
|||
|
|
3. 开始接收流量
|
|||
|
|
|
|||
|
|
**下线流程**:
|
|||
|
|
1. 注册中心注销实例 → 停止接收新请求
|
|||
|
|
2. 等待正在处理的请求完成(最大等待 N 秒)
|
|||
|
|
3. 关闭连接池、保存状态
|
|||
|
|
4. 进程退出
|
|||
|
|
|
|||
|
|
> [!tip] K8s Pod 优雅终止
|
|||
|
|
> ```yaml
|
|||
|
|
> spec:
|
|||
|
|
> terminationGracePeriodSeconds: 30 # 最多等 30 秒再 SIGKILL
|
|||
|
|
> ```
|
|||
|
|
> 配合 `preStop` Hook 可以提前摘除流量:
|
|||
|
|
> ```yaml
|
|||
|
|
> lifecycle:
|
|||
|
|
> preStop:
|
|||
|
|
> exec:
|
|||
|
|
> command: ["sh", "-c", "sleep 15"] # 给负载均衡摘流的时间
|
|||
|
|
> ```
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[02-服务治理/API网关/README]] — API Gateway 是服务发现的用户之一
|
|||
|
|
- [[02-服务治理/容错模式/README]] — 健康检查是容错体系的基础
|
|||
|
|
- [[05-部署运维/Kubernetes/README]] — K8s Service 的服务发现原理
|