学习目标
学完本章,你应该能够:
- 解释为什么需要服务注册与发现,对比 IP 直连、DNS、注册中心、服务自省几种模型的优缺点。
- 讲清注册中心机制:注册数据、心跳、服务上下线、客户端发现时机。
- 掌握 CAP 理论,说清注册中心选型中 CP vs AP 的权衡。
- 对比 ZooKeeper / Eureka / Nacos / etcd 作为注册中心的优缺点。
- 用 Go + etcd 在 gRPC 中接入服务注册与发现,理解 Resolver 接口和 Lease 续约机制。
- 设计注册中心高可用方案与故障容错策略。
前置知识(如果下面任意一点生疏,先回看对应章):
- 第10章 单体拆分微服务:知道服务被拆成独立实例、端口动态变化。
- 第07章 Kafka / 基本网络:知道客户端/服务端、心跳、租约这些概念。
- 基本的 gRPC 使用(见第10章):知道
grpc.Dial、客户端是怎么连服务端的。 - 一点分布式常识:网络分区、主从、一致性是什么直觉。
本章你会动手做的事:
- 画一张「客户端怎么找到 interactive 服务实例」的时序图,标出注册、查询、通知三步。
- 用 etcd 客户端写一段服务端注册代码,带上 Lease 自动续约,观察 key 在 TTL 内一直存活。
- 把
kill -9掉服务端进程,等过 TTL 后观察 etcd 如何自动剔除这个节点。
一、核心概念讲解
1.1 为什么需要服务注册与发现
接入微服务架构后,第一个问题就是:我怎么知道哪些机器上部署了我需要的服务?
之前代码里写死 localhost:8090,开发环境就不合适了——服务实例会变(重启、扩容、宕机、IP 漂移),IP 写死根本维护不过来。
类比:微服务之前,你打电话给朋友是直接拨他手机号(IP 直连)。微服务之后,朋友今天用 A 号、明天换 B 号、出差又用 C 号——你根本记不住。于是你们约定:所有人的当前号码都报给一个「总机」(注册中心),你打之前先问总机「他现在用哪个号」,总机还会主动告诉你「他换号了」。这就是服务注册与发现。
1.2 几种服务发现模型对比
| 模型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| IP 直连 | 最简单 | IP 会变,不好维护 | 联调、DEBUG 特定实例 |
| 域名 + DNS | 简单,不需额外组件 | 不缓存则每次调用多一跳;缓存则不能及时更新 | 中小规模 |
| 注册中心 | 主动通知节点变化,实时性好 | 大规模集群下注册中心可能成为瓶颈 | 主流方案 |
| 服务自省 | 注册中心只存最小数据,元数据通过元数据服务暴露 | 实现复杂 | 超大规模集群(阿里 Dubbo 提出) |
| 借助网关 | 客户端只需知道网关 | 网关本身也需要被发现 | 网关作为统一入口 |
flowchart TD
C[客户端] --> G[总机/注册中心]
G -->|返回可用实例列表| C
S1[服务端实例1] -->|注册+心跳| G
S2[服务端实例2] -->|注册+心跳| G
S3[服务端实例3] -->|注册+心跳| G
C -. 调用 .-> S1
C -. 调用 .-> S2IP 直连的应用场景
虽然原始,但实践中偶尔会用:
- 联调:和同事合作完成任务,直接调同事电脑 IP,方便他本地 DEBUG;
- DEBUG 特定实例:某些 BUG 只在特定实例上出现,直接把请求发到那个实例。
域名 + DNS 的优缺点
- 优点:简单,不需要额外组件;
- 缺点:
- 不缓存 DNS:每次调用都多一次 DNS 查询;
- 缓存 DNS:节点变更无法及时同步到客户端。
注册中心机制
DNS 的问题是不能及时收到节点变更通知。注册中心的核心思路:
- 服务端启动时主动在注册中心注册;
- 客户端首次调用前查询注册中心,缓存可用节点;
- 注册中心在节点变动时主动通知客户端。
服务自省(Dubbo 提出)
注册中心在大规模集群下会成为瓶颈。服务自省思路:
- 保留注册中心,但只存最小化数据(如服务定位信息);
- 其余元数据通过元数据服务暴露;
- 元数据一般不变,客户端只需查询一次。
二、注册中心机制详解
2.1 服务启动过程
- 服务端启动时主动在注册中心注册自身信息;
- 服务端怎么知道注册中心在哪?一般通过域名 + 端口或直接指定 IP + 端口;
- 启动阶段连不上注册中心,服务端会直接退出(fail-fast)。
⚠️ 新手必踩的坑:注册中心连不上就 fail-fast 会不会误伤。会。如果注册中心短暂抖动,所有服务端一起退出,雪崩。生产上常配「启动期有限次重试 + 告警」,确认注册中心真挂了才退。但核心原则不变:宁可起不来,也别注册了脏数据(IP 写错)让客户端调不通。
2.2 注册什么数据
两类信息:
- 定位信息:IP + 端口(最关键);
- 其他信息:与微服务框架功能相关,如分组信息、权重、协议版本等(用于服务治理)。
2.3 心跳机制——服务端与注册中心
注册成功后,注册中心与服务端保持心跳,相当于租约,心跳就是续租。两种模式:
| 模式 | 说明 |
|---|---|
| 服务端主动 | 服务端每隔一段时间朝注册中心发心跳,注册中心可应答可不应答 |
| 注册中心主动 | 注册中心朝所有注册的服务端发心跳 |
心跳失败时,注册中心判定服务端崩溃,通知客户端不要使用该节点。
2.4 心跳参数权衡
| 参数 | 短 | 长 |
|---|---|---|
| 心跳间隔 | 压力大 | 压力小 |
| 失败判定次数 | 快发现崩溃,但难应对偶发失败 | 慢发现崩溃,但能避免偶发失败 |
核心权衡:
- 间隔越短,压力越大;
- 次数越少,越快发现崩溃,但难应对偶发性心跳失败;
- 次数越多,越慢发现崩溃,但能避免偶发性失败。
2.5 高级心跳机制(少见)
- 双向心跳:服务端和注册中心都主动发心跳;
- 单向失败后切换:正常情况下服务端主动发,注册中心一段时间没收到后主动发起。
性价比不高,比较少采用。
2.6 服务优雅下线
服务端 A 不能在告诉注册中心"我下线了"之后就立刻退出——因为它可能还有正在处理的请求,网络上也还有客户端正在发请求。
优雅下线步骤:
- 通知注册中心:本节点要下线;
- 服务端不再接受新请求(包括已读到一半的请求,读完直接拒绝);
- 等待正在处理的请求结束(包括定时任务);
- 所有请求处理完毕后退出;
- 设置超时时间,超时强制退出(防止长时间任务卡住)。
2.7 客户端的服务发现时机
| 时机 | 行为 |
|---|---|
| 启动时发现 | 启动时拿到所有要用的微服务,初始化时执行服务发现。发现失败则客户端不启动 |
| 首次调用时发现 | 启动时不发现,第一次调用某服务时才执行发现 |
客户端也要和注册中心保持心跳,确保注册中心在节点变动时能通知到客户端。
sequenceDiagram
participant S as 服务端
participant R as 注册中心
participant C as 客户端
S->>R: 启动注册(IP:Port)
R-->>S: 心跳续租(持续)
C->>R: 查询 interactive 实例列表
R-->>C: 返回[实例1,实例2]
S->>R: 下线/心跳断开
R-->>C: 主动通知:实例2 没了
C->>S: 只调实例1(failover)三、CAP 理论与注册中心选型
3.1 CAP 理论
分布式系统不可能同时满足三个特性,最多同时满足两项:
- 一致性 Consistency:数据在多个副本之间保持一致。任一节点读到的数据都一样。
- 可用性 Availability:系统一直可用,每个请求都能在有限时间内返回结果。
- 分区容错性 Partition Tolerance:出现网络分区(网络故障导致通信中断)时仍能正常工作。
3.2 为什么大多数时候选 P
在分布式环境下,网络分区是客观存在的——你无法阻止网络故障。所以 P 大多数时候必不可少。
网络分区发生时只能二选一:
- 放弃 A 和 B 之间同步数据 → 舍弃一致性;
- 放弃 A 和 B 对外服务 → 舍弃可用性。
例外:etcd 严格来说是 AC 模型,没有选 P——网络故障导致节点间不能通信时,etcd 集群不可用。
3.3 注册中心应该选 CP 还是 AP
面试标准答案:选 AP。
理由:在服务发现场景下,“拿到错误的数据也好过拿不到数据”——即便注册信息略有偏差,客户端仍可以尝试调用,比注册中心直接不可用要好。
实践看法:
- 小规模集群:随便选,负载小、流量小,注册中心很难崩溃;
- 大规模集群:优先 AP。
3.4 主流注册中心对比
ZooKeeper(CP 模型)
- 架构:主从结构,树形数据存储(如
/app1/p_1表示服务 app1 的一个节点); - 机制:客户端 Watcher 监听节点变化,主动拉取最新数据;
- 优点:API 简单,成熟度高,多语言客户端支持好,Push 模型实时通知;
- 缺点:
- 无法支撑超大规模集群(TCP 连接数限制);
- 主节点是写瓶颈;
- 主从模式可能脑裂(网络恢复后两个主节点);
- 主节点选举期间集群不可用;
- 本质:CP 模型,中小规模没问题。
Eureka(AP 模型)
- 架构:对等集群,节点地位平等,相互同步数据;
- 优点:可用性高,支持横向扩展,无单点写瓶颈;
- 缺点:服务发现慢(数据要扩散到全集群),多语言支持差;
- 本质:AP 模型。
Nacos(同时支持 CP 和 AP)
- 优点:
- 高可用(AP 模式 + 熔断降级);
- 中文社区,无语言壁垒;
- 功能丰富(namespace、Group 区分环境);
- 缺点:
- 部署运维难度高;
- CP/AP 集成在一起,源码复杂;
- 多语言支持较差;
- 本质:可切换。
etcd(AC 模型,实践接近 CP)
- 优点:
- Go 编写,部署简单,HTTP 接口;
- Raft 算法保证强一致性,可靠;
- 大规模集群表现优秀;
- 缺点:网络稳定性差时不合适(网络分区会不可用);
- 本质:Go 生态首选。
3.5 选型结论
小规模集群随便选,熟悉哪个用哪个;大规模集群优先考虑 etcd。
理论上的选型标准:
- 通用标准:成熟度高、社区完善;
- 扩展性:能跟着业务增长扩展;
- 成本:部署集群所需节点数;
- 集群模式:对等集群优于主从集群;
- CAP:优先 AP。
现实:写文档罗列优缺点,给个建议,最后老板拍板。这些成熟中间件选哪个都不会出大问题,也都有可能出小问题。
graph TD
CAP[分布式系统三选二] --> C[一致性 C]
CAP --> A[可用性 A]
CAP --> P[分区容错 P]
C -. 与 .- A
A -. 与 .- P
C -. 与 .- P
note[网络分区客观存在
通常必选 P
剩下 C 与 A 二选一]四、在 gRPC 中接入注册中心
4.1 gRPC 默认的服务发现方案
gRPC 默认使用 域名解析策略:
- 传入
user_svc.mycompany.com:8090,gRPC 会定期访问域名服务器更新本地缓存的可用 IP; - 如果直接传 IP 地址,连域名解析都省了;
- 在 K8s 中推荐使用这种形态(用 Service 名通信)。
源码位置:dnsResolver。
4.2 Resolver 接口——gRPC 的核心抽象
在 gRPC 中使用注册中心,本质就是提供一个用注册中心实现的 Resolver。
// gRPC 的 Resolver 接口(简化版)
type Resolver interface {
// ResolveNow 立即解析
ResolveNow(ResolveNowOptions)
// Close 关闭
Close()
}
// Builder 构建 Resolver
type Builder interface {
// Build 创建 Resolver,target 是服务地址,clientConn 用于通知解析结果
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
// Scheme 返回支持的协议前缀,如 "etcd"
Scheme() string
}
关键:gRPC 根本不知道你用的是不是注册中心,它只提供客户端这边的 Resolver 接口。
4.3 etcd 服务端注册示例
// webook/internal/interactive/ioc/registry.go
package ioc
import (
"context"
"log"
"time"
"go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/naming/endpoints/resolver"
)
func InitRegistry(etcdClient *clientv3.Client) {
// 步骤 1:创建 endpoint.Manager
em, err := endpoints.NewManager(etcdClient, "service/interactive/")
if err != nil {
panic(err)
}
// 步骤 2:创建租约(关键!用于宕机自动剔除)
leaseResp, err := etcdClient.Grant(context.Background(), 30)
if err != nil {
panic(err)
}
leaseID := leaseResp.ID
// 步骤 3:开启自动续约
kaCtx, _ := context.WithCancel(context.Background())
ch, err := etcdClient.KeepAlive(kaCtx, leaseID)
if err != nil {
panic(err)
}
// 处理续约结果,简单打日志即可
go func() {
for resp := range ch {
log.Println("续约成功", resp.ID)
}
log.Println("续约 goroutine 退出")
}()
// 步骤 4:获取本机 IP(不能用 127.0.0.1,必须注册客户端能连的 IP)
ip := getOutboundIP()
addr := fmt.Sprintf("%s:8091", ip.String())
// 步骤 5:注册 endpoint,带上租约 ID
err = em.AddEndpoint(context.Background(),
"service/interactive/"+addr,
endpoints.Endpoint{
Addr: addr,
// 这里可以放元数据:分组、权重、协议版本等
Metadata: map[string]interface{}{
"version": "v1",
},
},
clientv3.WithLease(leaseID),
)
if err != nil {
panic(err)
}
}
// getOutboundIP 通过对外发 UDP 报文确定本机 IP
// UDP 不会真正建立连接,只是确定路由
func getOutboundIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
panic(err)
}
defer conn.Close()
return conn.LocalAddr().(*net.UDPAddr).IP
}
⚠️ 新手必踩的坑:注册了
127.0.0.1或localhost。服务端在自己机器上net.Listen("tcp", "127.0.0.1:8091"),顺手把127.0.0.1注册到 etcd。结果别的机器上的客户端拿到127.0.0.1:8091,连的是自己本机,根本连不上目标服务。必须用「对外通信 IP」——示例里getOutboundIP靠 UDP 报文探测出口 IP,才是客户端真能连的地址。
4.4 更新元数据
// etcd 没有提供 Edit 操作,只能用 Add 覆盖(key 已存在时就是更新)
em.AddEndpoint(ctx, key, endpoints.Endpoint{
Addr: addr,
Metadata: newMetadata,
}, clientv3.WithLease(leaseID))
4.5 删除 Endpoint(服务退出时)
// 服务退出时先删除自己注册的 Endpoint
em.DeleteEndpoint(ctx, "service/interactive/"+addr)
4.6 宕机了没删除怎么办——Lease 续约机制
问题:如果服务器直接宕机(kill -9),来不及调用 DeleteEndpoint,etcd 上还会残留这个节点。
解决:使用 Lease(租约)机制:
- 创建租约,设置 TTL(如 30 秒);
- 注册 endpoint 时带上 leaseID;
- 开启自动续约
KeepAlive,每TTL/3时间续约一次; - 服务宕机后无法续约,TTL 到期后 etcd 自动剔除该节点。
etcd 默认续约间隔:每 TTL/3 续约一次。
缺点:etcd 自带实现没有暴露很多控制选项,难以在面试中"刷亮点",但实践中没问题。
sequenceDiagram
participant S as 服务端
participant E as etcd
S->>E: Grant 租约 TTL=30s
S->>E: AddEndpoint + leaseID
loop 每 10s 续约
S->>E: KeepAlive
E-->>S: 续约成功, TTL 重置
end
Note over S: 突然 kill -9
Note over E: 30s 内无续约
E->>E: TTL 到期, 自动删除 endpoint4.7 客户端服务发现
// webook/internal/web/ioc/grpc.go
package ioc
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
etcdresolver "go.etcd.io/etcd/client/v3/naming/resolver"
)
func InitInteractiveClient(etcdClient *clientv3.Client) interactivev1.InteractiveServiceClient {
// 步骤 1:用 etcd 客户端构造一个 gRPC Resolver
r, err := etcdresolver.NewBuilder(etcdClient)
if err != nil {
panic(err)
}
// 步骤 2:创建 gRPC 连接,目标使用 etcd:// 协议前缀
// 测试环境用 insecure 跳过 TLS
conn, err := grpc.Dial(
"etcd:///service/interactive/",
grpc.WithResolvers(r),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
// 步骤 3:用连接初始化客户端
// 后续 SDK 会自动处理节点列表的同步更新
return interactivev1.NewInteractiveServiceClient(conn)
}
客户端这边很简单,不需要关心同步数据之类的事情,SDK 都处理了。
4.8 改造 interactive 的服务启动
核心修改:扩展 ginx.Server 的功能,让它负责和注册中心打交道。
// webook/pkg/ginx/server.go
package ginx
type Server struct {
*grpc.Server
addr string
registry *Registry
}
func (s *Server) Serve(lis net.Listener) error {
// 关键:确保端口打开之后再注册
// 否则客户端拿到注册信息却连不上
err := s.Server.Serve(lis)
return err
}
// RegisterAndServe 注册并服务
func (s *Server) RegisterAndServe(lis net.Listener) error {
// 步骤 1:先注册(端口已经 Listen 成功)
if s.registry != nil {
if err := s.registry.Register(s.addr); err != nil {
return err
}
defer s.registry.Deregister()
}
// 步骤 2:启动 gRPC Server
return s.Server.Serve(lis)
}
注意:注册必须在端口 Listen 成功之后,否则客户端拿到地址却连不上。
⚠️ 新手必踩的坑:先注册、后
Serve。如果RegisterAndServe里先registry.Register再Server.Serve,而Serve内部才真正Listen端口——那客户端可能在「端口还没开」时就拿到了地址,一连就失败、还触发 failover 白忙活。正确顺序:先Listen开端口、注册、再Serve接受连接。
五、K8s 中的服务注册与发现
5.1 几种方式都能用,但要注意两个问题
- 注册的 IP 必须是外部通信 IP(K8s Pod 之间通信的 IP);
- IP 漂移:K8s Pod 崩溃后重新启动,IP 可能变化;网络配置变化也可能导致 IP 漂移。
5.2 推荐:直接用 DNS
在 K8s 里虽然也能用注册中心,但推荐直接用 DNS——用 Service 名字通信,K8s 自动维护 Service → Pod 的映射。
六、选用注册中心时要关心什么
接入一个注册中心时,关于 SDK(如 etcd 客户端依赖)要清楚:
- 有没有自动续约机制?没有就要手动续约;
- 续约间隔多长?
- 续约失败,SDK 有没有处理机制?
- 注册中心和客户端是什么模型?客户端多久能知道服务端变化?
知道这些,遇到服务注册与发现相关问题就更容易定位。
七、其他 Go 微服务框架的服务发现
所有依托于 gRPC 的微服务框架(go-zero、Kratos、ego 等)接入服务注册与发现都大同小异:
- 必然实现 gRPC 的 Resolver 接口;
- 必然有一个 register 过程(服务端启动时注册);
- 必然有续约过程(自己写或依赖客户端 SDK);
- 如果要支持多种注册中心,会有一个
Registry/Discovery接口层做抽象。
学习不同框架时,沿着这个思路找源码就容易理解。个人感觉 Kratos 的源码更直观。
八、注册中心高可用与故障容错
8.1 高可用关键点——三个节点 + 三条边
客户端 ←→ 注册中心 ←→ 服务端
要思考的问题:
- 服务端崩溃怎么办?
- 注册中心崩溃怎么办?
- 客户端崩溃?(不需要处理,客户端都崩了就没法调用)
- 注册中心和服务端无法通信怎么办?
- 注册中心和客户端无法通信怎么办?(按"注册中心崩溃"一致行为容错)
- 客户端和服务端无法通信怎么办?(failover 策略)
8.2 服务端崩溃
注册中心发现服务端崩溃有秒级延迟。客户端要能正常处理:
- 发现连不上服务端,换一个节点重试(failover)。
8.3 注册中心高可用方案
| 方案 | 说明 |
|---|---|
| 集群部署 | 多活方案,避免单点 |
| 双注册中心 | 同时注册两个,一个崩了用另一个 |
| 按业务拆分 | 核心业务和非核心业务用不同注册中心 |
8.4 注册中心崩溃——客户端怎么办
后果:客户端无法收到服务端变动信息(上线/下线)。
容错策略:
- 使用本地缓存的可用节点信息(缺陷:可能不准,有节点已下线,有节点新加进来);
- 调用时发现无法连通,把该节点移出可用列表;
- 如果是新服务(没有任何缓存),返回特定错误,让调用者决定。
8.5 注册中心崩溃——服务端怎么办
服务端无法更新注册信息:
- 新节点:注册失败时直接退出服务;
- 老节点:继续提供服务(客户端可能还在用本地缓存的注册数据)。
8.6 注册中心和服务端无法通信
注册中心会判定服务端崩溃,但服务端实际还活着。
- 客户端在收到错误判定前,依旧能调用服务端;
- 客户端、服务端、注册中心都不需要做什么;
- 但服务端发现自己一段时间无法和注册中心保持心跳后,要告警,并考虑是否退出。
flowchart TD
Q{哪个组件挂了?}
Q -->|服务端崩| A[客户端 failover 换节点]
Q -->|注册中心崩| B[客户端用本地缓存
连不上就剔除+报错]
Q -->|注册中心↔服务端断| C[服务端告警+考虑退出
客户端暂不受影响]
Q -->|客户端崩| D[无需处理]九、工程实践要点
- 服务端启动失败要 fail-fast:连不上注册中心直接退出,避免脏数据。
- 注册必须在端口 Listen 成功之后:否则客户端拿到地址连不上。
- IP 不能用 127.0.0.1:必须注册客户端能连的局域网 IP,通过 UDP 报文确定。
- 续约结果要处理:简单打日志就够,但要有监控。
- 优雅下线要等请求处理完:但也要有超时控制。
- 客户端要本地缓存注册数据:注册中心崩溃时能继续调用。
- failover 必备:客户端发现某节点连不上要换节点重试。
- K8s 优先用 DNS:避免 IP 漂移问题。
- 核心业务单独注册中心:避免非核心业务故障影响核心。
- 测试环境模拟注册中心崩溃:验证客户端容错措施是否到位。
十、面试要点
基础理论题
- 服务注册与发现有哪些组件?
- 什么是注册中心?为什么要用?直接用 IP 或域名行不行?
- 注册时究竟注册了什么数据?
- 注册中心和服务端怎么保持心跳?心跳间隔怎么设置?
- 怎么避免偶发性心跳失败?
- 服务端下线需要注意什么?具体步骤?
- 客户端和注册中心需要保持心跳吗?
CAP 与选型题
- 什么是 CAP 原理?
- 为什么大多数时候选 P?
- 注册中心 CAP 选哪个?(先答标准答案 AP,再说自己的理解)
- 你们公司用什么注册中心?为什么用?
- 怎么保证注册中心高可用?
- 注册中心崩溃后怎么办?
- 服务端连不上注册中心会怎样?客户端会怎样?
- 客户端连不上注册中心怎么办?
实践题
- 怎么在 gRPC 中接入 etcd?
- Resolver 是什么?为什么 gRPC 要这个抽象?
- Lease 是什么?为什么需要?
- 续约间隔多长?默认值是多少?
- 怎么获取本机 IP?为什么不能从配置文件读?
- K8s 中推荐用哪种方式?为什么?
自测题与动手练习
自测题(合上书能答出来,才算懂):
- 微服务项目里直接把服务端 IP 写死在客户端,会遇到什么问题?注册中心相比 DNS 的核心优势是什么?
- 注册中心机制里「注册、心跳、节点变更通知、客户端发现」四步分别解决什么?
- CAP 三选二,为什么注册中心通常选 AP 而不是 CP?etcd 为什么被说成「AC 模型」?
- etcd 服务端注册为什么必须带 Lease?如果服务端
kill -9没调DeleteEndpoint,etcd 怎么自动剔除它? - 注册中心整个崩了,客户端和服务端各自应该怎么容错?新服务(没有任何缓存)调用会怎样?
动手练习(建议真做一遍):
- 跑通注册 + 发现:按 4.3 起一个 etcd,服务端用
InitRegistry注册service/interactive/,客户端用 4.7 的etcd:///连上并成功调一次Get。 - 观察 Lease 剔除:服务端注册成功后
kill -9掉它,盯着 etcd(或客户端日志),确认过 TTL(默认 30s)后这个 endpoint 自动消失。 - 模拟注册中心崩溃:把 etcd 进程停掉,观察客户端是否能用本地缓存继续调用老节点、连不上时能否 failover;再恢复 etcd,看节点列表是否重新同步。
十一、本章小结
- 服务注册与发现是微服务架构的基础设施,解决"客户端如何找到动态变化的服务端实例"问题。
- 几种模型:IP 直连(联调用)、DNS(K8s 推荐)、注册中心(主流)、服务自省(超大规模)。
- 注册中心机制:服务端注册 → 心跳续租 → 节点变更通知客户端 → 客户端 failover。
- CAP 理论:分布式系统三选二。注册中心选型面试标准答案是 AP,实践中小规模随便选、大规模选 etcd。
- 主流注册中心对比:
- ZooKeeper:CP 模型,主从结构,脑裂风险,老牌;
- Eureka:AP 模型,对等集群,服务发现慢;
- Nacos:CP/AP 可切换,中文社区好,功能丰富;
- etcd:AC 模型,Raft 强一致,Go 生态首选。
- gRPC 接入注册中心:实现 Resolver 接口 + 服务端注册 + Lease 续约。
- 高可用:注册中心集群 + 双注册中心 + 按业务拆分 + 客户端本地缓存 + failover。
- 故障容错:注册中心崩了用本地缓存,服务端崩了 failover,通信断了告警+考虑退出。
至此,从单体拆分微服务、不停机数据迁移、服务注册与发现,你已经走完了微服务入门的三大核心环节。接下来要进入更深入的服务治理、可观测性、网关等主题。