Files
cs-note/hhs/MS/02-服务治理/04-服务发现.md
T
2026-05-24 11:42:38 +08:00

238 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [microservice, service-discovery, consul, nacos, etcd, kubernetes]
create time: 2026-05-05 00:00
---
# 服务发现
## 概述
服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:**我怎么找到你?**
> [!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["客户端内置负载均衡逻辑"]
B -.-> noteA
end
subgraph Server["服务端发现模式"]
C[Service C] -->|请求| P[LoadBalancer Proxy]
P -->|查询注册中心| R2[(Registry)]
R2 -->|返回实例| P
P -->|转发| D[Service D]
noteC["客户端无感知,透明代理"]
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"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
// 1. 创建服务端配置(连接哪个 Nacos)
sc := constant.ServerConfig{
IpAddr: "127.0.0.1",
Port: 8848,
}
```
> 上面仅展示了 serverConfig,实际创建时还需传入 `ClientConfig`(命名空间、超时等)。为简洁起见此处省略。
```go
// 2. 创建 NamingClient
client, _ := clients.NewNamingClient(
map[string]any{"serverConfigs": []constant.ServerConfig{sc}},
)
// 3. 注册服务实例(默认临时实例,走心跳保活)
_ = client.RegisterInstance(vo.RegisterInstanceParam{
Ip: "10.0.0.1",
Port: 8080,
ServiceName: "order-service",
ClusterName: "DEFAULT",
})
// 4. 拉取某服务的可用实例列表(同步快照,含健康过滤)
instances, _, _ := client.SelectInstances(vo.SelectInstanceParam{
ServiceName: "order-service",
HealthyOnly: true, // 只返回健康实例
})
for _, inst := range instances {
// inst.Ip: inst.Port — 拿到后可直接发起 HTTP / gRPC 调用
}
```
### Consul
HashiCorp 出品,基于 Raft 共识协议(CP),天然支持健康检查和 DNS 接口。
**核心特点**:
- 内建 KV 存储,可作为轻量配置中心
- 支持 multi-datacenter 部署
- DNS 接口:`order-service.service.consul` 可直接解析 IP
**与 Nacos 选型对比**:
| 维度 | Consul | Nacos |
|------|--------|-------|
| 一致性模型 | CP(Raft) | AP + CP 可切换 |
| 部署复杂度 | 需独立部署 Agent(Sidecar 模式) | Go SDK 内置,无额外代理 |
| 生态集成 | Terraform / Vault 深度绑定 | Spring Cloud / Dubbo 原生支持 |
| 适用场景 | 多云 / 混合云、基础设施层 | Java 技术栈、业务服务层 |
> [!tip] Consul Sidecar 模式
> Consul Agent 通常以 DaemonSet 方式跑在每台节点上,应用通过 localhost 向本地 Agent 注册。这种 **Sidecar 模式**天然适配服务端发现架构——应用把 Consul Agent 当作"本地注册中心"即可。
### 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 优雅终止
> 以 sidecar 方式集成 Consul Agent 时,Pod 配置如下:
> ```yaml
> spec:
> terminationGracePeriodSeconds: 30 # 最多等 30 秒再 SIGKILL
> initContainers:
> - name: consul-init # 先启动 Sidecar
> image: consul:latest
> command: ["consul", "agent", "-bind", "-config-file=/etc/consul.d/server.json"]
> containers:
> - name: app
> lifecycle:
> preStop:
> exec:
> command: ["sh", "-c", "sleep 15"] # 先停流量(让 LB / Consul 摘流)
> postStart:
> exec:
> command: ["sh", "-c", "sleep 5"] # 等 Sidecar 就绪后再开流量
> ```
> 关键点:**preStop sleep** 留给 LB / Consul 摘流时间,**postStart sleep** 确保 Sidecar 已准备好再做健康检查。
## 关联笔记
- [[02-服务治理/01-API网关]] — API Gateway 是服务发现的用户之一
- [[02-服务治理/06-容错模式]] — 健康检查是容错体系的基础
- [[05-部署运维/02-Kubernetes]] — K8s Service 的服务发现原理