服务注册与发现

2023-02-17T14:11:02+08:00 | 16分钟阅读 | 更新于 2026-02-17T14:11:02+08:00

@

学习目标

学完本章,你应该能够:

  1. 解释为什么需要服务注册与发现,对比 IP 直连、DNS、注册中心、服务自省几种模型的优缺点。
  2. 讲清注册中心机制:注册数据、心跳、服务上下线、客户端发现时机。
  3. 掌握 CAP 理论,说清注册中心选型中 CP vs AP 的权衡。
  4. 对比 ZooKeeper / Eureka / Nacos / etcd 作为注册中心的优缺点。
  5. 用 Go + etcd 在 gRPC 中接入服务注册与发现,理解 Resolver 接口和 Lease 续约机制。
  6. 设计注册中心高可用方案与故障容错策略。

前置知识(如果下面任意一点生疏,先回看对应章):

  • 第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 -. 调用 .-> S2

IP 直连的应用场景

虽然原始,但实践中偶尔会用:

  • 联调:和同事合作完成任务,直接调同事电脑 IP,方便他本地 DEBUG;
  • DEBUG 特定实例:某些 BUG 只在特定实例上出现,直接把请求发到那个实例。

域名 + DNS 的优缺点

  • 优点:简单,不需要额外组件;
  • 缺点:
    • 不缓存 DNS:每次调用都多一次 DNS 查询;
    • 缓存 DNS:节点变更无法及时同步到客户端。

注册中心机制

DNS 的问题是不能及时收到节点变更通知。注册中心的核心思路:

  1. 服务端启动时主动在注册中心注册;
  2. 客户端首次调用前查询注册中心,缓存可用节点;
  3. 注册中心在节点变动时主动通知客户端。

服务自省(Dubbo 提出)

注册中心在大规模集群下会成为瓶颈。服务自省思路:

  • 保留注册中心,但只存最小化数据(如服务定位信息);
  • 其余元数据通过元数据服务暴露;
  • 元数据一般不变,客户端只需查询一次。

二、注册中心机制详解

2.1 服务启动过程

  1. 服务端启动时主动在注册中心注册自身信息;
  2. 服务端怎么知道注册中心在哪?一般通过域名 + 端口直接指定 IP + 端口
  3. 启动阶段连不上注册中心,服务端会直接退出(fail-fast)。

⚠️ 新手必踩的坑:注册中心连不上就 fail-fast 会不会误伤。会。如果注册中心短暂抖动,所有服务端一起退出,雪崩。生产上常配「启动期有限次重试 + 告警」,确认注册中心真挂了才退。但核心原则不变:宁可起不来,也别注册了脏数据(IP 写错)让客户端调不通。

2.2 注册什么数据

两类信息:

  • 定位信息:IP + 端口(最关键);
  • 其他信息:与微服务框架功能相关,如分组信息、权重、协议版本等(用于服务治理)。

2.3 心跳机制——服务端与注册中心

注册成功后,注册中心与服务端保持心跳,相当于租约,心跳就是续租。两种模式:

模式说明
服务端主动服务端每隔一段时间朝注册中心发心跳,注册中心可应答可不应答
注册中心主动注册中心朝所有注册的服务端发心跳

心跳失败时,注册中心判定服务端崩溃,通知客户端不要使用该节点。

2.4 心跳参数权衡

参数
心跳间隔压力大压力小
失败判定次数快发现崩溃,但难应对偶发失败慢发现崩溃,但能避免偶发失败

核心权衡

  • 间隔越短,压力越大;
  • 次数越少,越快发现崩溃,但难应对偶发性心跳失败;
  • 次数越多,越慢发现崩溃,但能避免偶发性失败。

2.5 高级心跳机制(少见)

  • 双向心跳:服务端和注册中心都主动发心跳;
  • 单向失败后切换:正常情况下服务端主动发,注册中心一段时间没收到后主动发起。

性价比不高,比较少采用。

2.6 服务优雅下线

服务端 A 不能在告诉注册中心"我下线了"之后就立刻退出——因为它可能还有正在处理的请求,网络上也还有客户端正在发请求。

优雅下线步骤

  1. 通知注册中心:本节点要下线;
  2. 服务端不再接受新请求(包括已读到一半的请求,读完直接拒绝);
  3. 等待正在处理的请求结束(包括定时任务);
  4. 所有请求处理完毕后退出;
  5. 设置超时时间,超时强制退出(防止长时间任务卡住)。

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.1localhost。服务端在自己机器上 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(租约)机制:

  1. 创建租约,设置 TTL(如 30 秒);
  2. 注册 endpoint 时带上 leaseID;
  3. 开启自动续约 KeepAlive,每 TTL/3 时间续约一次;
  4. 服务宕机后无法续约,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 到期, 自动删除 endpoint

4.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.RegisterServer.Serve,而 Serve 内部才真正 Listen 端口——那客户端可能在「端口还没开」时就拿到了地址,一连就失败、还触发 failover 白忙活。正确顺序:先 Listen 开端口、注册、再 Serve 接受连接。


五、K8s 中的服务注册与发现

5.1 几种方式都能用,但要注意两个问题

  1. 注册的 IP 必须是外部通信 IP(K8s Pod 之间通信的 IP);
  2. 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[无需处理]

九、工程实践要点

  1. 服务端启动失败要 fail-fast:连不上注册中心直接退出,避免脏数据。
  2. 注册必须在端口 Listen 成功之后:否则客户端拿到地址连不上。
  3. IP 不能用 127.0.0.1:必须注册客户端能连的局域网 IP,通过 UDP 报文确定。
  4. 续约结果要处理:简单打日志就够,但要有监控。
  5. 优雅下线要等请求处理完:但也要有超时控制。
  6. 客户端要本地缓存注册数据:注册中心崩溃时能继续调用。
  7. failover 必备:客户端发现某节点连不上要换节点重试。
  8. K8s 优先用 DNS:避免 IP 漂移问题。
  9. 核心业务单独注册中心:避免非核心业务故障影响核心。
  10. 测试环境模拟注册中心崩溃:验证客户端容错措施是否到位。

十、面试要点

基础理论题

  • 服务注册与发现有哪些组件?
  • 什么是注册中心?为什么要用?直接用 IP 或域名行不行?
  • 注册时究竟注册了什么数据?
  • 注册中心和服务端怎么保持心跳?心跳间隔怎么设置?
  • 怎么避免偶发性心跳失败?
  • 服务端下线需要注意什么?具体步骤?
  • 客户端和注册中心需要保持心跳吗?

CAP 与选型题

  • 什么是 CAP 原理?
  • 为什么大多数时候选 P?
  • 注册中心 CAP 选哪个?(先答标准答案 AP,再说自己的理解)
  • 你们公司用什么注册中心?为什么用?
  • 怎么保证注册中心高可用?
  • 注册中心崩溃后怎么办?
  • 服务端连不上注册中心会怎样?客户端会怎样?
  • 客户端连不上注册中心怎么办?

实践题

  • 怎么在 gRPC 中接入 etcd?
  • Resolver 是什么?为什么 gRPC 要这个抽象?
  • Lease 是什么?为什么需要?
  • 续约间隔多长?默认值是多少?
  • 怎么获取本机 IP?为什么不能从配置文件读?
  • K8s 中推荐用哪种方式?为什么?

自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. 微服务项目里直接把服务端 IP 写死在客户端,会遇到什么问题?注册中心相比 DNS 的核心优势是什么?
  2. 注册中心机制里「注册、心跳、节点变更通知、客户端发现」四步分别解决什么?
  3. CAP 三选二,为什么注册中心通常选 AP 而不是 CP?etcd 为什么被说成「AC 模型」?
  4. etcd 服务端注册为什么必须带 Lease?如果服务端 kill -9 没调 DeleteEndpoint,etcd 怎么自动剔除它?
  5. 注册中心整个崩了,客户端和服务端各自应该怎么容错?新服务(没有任何缓存)调用会怎样?

动手练习(建议真做一遍)

  1. 跑通注册 + 发现:按 4.3 起一个 etcd,服务端用 InitRegistry 注册 service/interactive/,客户端用 4.7 的 etcd:/// 连上并成功调一次 Get
  2. 观察 Lease 剔除:服务端注册成功后 kill -9 掉它,盯着 etcd(或客户端日志),确认过 TTL(默认 30s)后这个 endpoint 自动消失。
  3. 模拟注册中心崩溃:把 etcd 进程停掉,观察客户端是否能用本地缓存继续调用老节点、连不上时能否 failover;再恢复 etcd,看节点列表是否重新同步。

十一、本章小结

  1. 服务注册与发现是微服务架构的基础设施,解决"客户端如何找到动态变化的服务端实例"问题。
  2. 几种模型:IP 直连(联调用)、DNS(K8s 推荐)、注册中心(主流)、服务自省(超大规模)。
  3. 注册中心机制:服务端注册 → 心跳续租 → 节点变更通知客户端 → 客户端 failover。
  4. CAP 理论:分布式系统三选二。注册中心选型面试标准答案是 AP,实践中小规模随便选、大规模选 etcd。
  5. 主流注册中心对比:
    • ZooKeeper:CP 模型,主从结构,脑裂风险,老牌;
    • Eureka:AP 模型,对等集群,服务发现慢;
    • Nacos:CP/AP 可切换,中文社区好,功能丰富;
    • etcd:AC 模型,Raft 强一致,Go 生态首选。
  6. gRPC 接入注册中心:实现 Resolver 接口 + 服务端注册 + Lease 续约。
  7. 高可用:注册中心集群 + 双注册中心 + 按业务拆分 + 客户端本地缓存 + failover。
  8. 故障容错:注册中心崩了用本地缓存,服务端崩了 failover,通信断了告警+考虑退出。

至此,从单体拆分微服务、不停机数据迁移、服务注册与发现,你已经走完了微服务入门的三大核心环节。接下来要进入更深入的服务治理、可观测性、网关等主题。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!