深入解析gRPC服务发现机制

深入理解 gRPC 服务发现:从原理到实现

前言

在微服务架构中,服务实例的地址是动态变化的。服务可能随时扩容、缩容、重启或迁移,如果客户端硬编码服务地址,将面临以下问题:

本文基于 Microg 框架讲解,项目地址:https://github.com/atliliw/microg

如果觉得有帮助,欢迎 Star ⭐

  • 服务扩容时,需要修改客户端配置
  • 服务宕机时,客户端无法自动切换
  • 无法实现负载均衡

服务发现 是解决这些问题的关键机制。本文将深入讲解 gRPC 服务发现的完整流程,包括服务注册、服务发现、Resolver 解析等核心机制。

一、服务发现的三个角色

服务发现涉及三个核心角色:

角色 职责 说明
Provider 服务提供者 启动时注册到 Registry,停止时注销
Registry 注册中心 存储服务信息,提供查询和监听能力
Consumer 服务消费者 通过服务名发现实例,发起调用
复制代码
    Provider              Registry              Consumer
   (服务提供者)           (注册中心)            (服务消费者)

   ┌─────────┐           ┌─────────┐           ┌─────────┐
   │ user-srv│           │  Consul │           │  client │
   │ :9000   │           │         │           │         │
   └─────────┘           └─────────┘           └─────────┘

   职责:                 职责:                 职责:
   - 启动服务             - 存储服务信息         - 发现服务
   - 注册到 Registry      - 提供查询/监听       - 调用服务
   - 停止时注销           - 健康检查            - 负载均衡

二、服务注册流程

当 Provider 启动时,需要将自己注册到 Registry。完整流程如下:

复制代码
┌─────────────────────┐
│  1. Provider 启动    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  2. 创建 Registry    │
│  consul.New(client) │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  3. 创建 gRPC Server │
│  rpcserver.New()    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  4. 创建 App         │
│  app.New(           │
│    WithName(),      │
│    WithRPCServer(), │
│    WithRegistrar(), │
│  )                  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────────────────────────────┐
│  5. 构建服务实例 ServiceInstance             │
│     {                                       │
│       ID:        "uuid-xxx"                 │
│       Name:      "user-srv"                 │
│       Endpoints: ["grpc://192.168.1.10:9000"]│
│     }                                       │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  6. Registrar.Register(ctx, instance)       │
│                                             │
│     调用 Consul API 注册服务                 │
│     配置健康检查 (TCP/HTTP)                  │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  7. Consul 存储                             │
│                                             │
│     Services:                               │
│       user-srv:                             │
│         - ID: xxx, Address: 192.168.1.10:9000│
│         - Check: TCP health check (10s)    │
└─────────────────────────────────────────────┘

代码示例

go 复制代码
// Provider 启动代码
func main() {
    // 1. 创建 Consul 客户端
    consulClient, _ := api.NewClient(api.DefaultConfig())
    
    // 2. 创建 Registry
    registry := consul.New(consulClient,
        consul.WithHealthCheck(true),
        consul.WithHeartbeat(true),
    )
    
    // 3. 创建 gRPC Server
    rpcServer := rpcserver.NewServer(
        rpcserver.WithAddress(":9000"),
    )
    
    // 4. 创建 App 并启动
    app.New(
        app.WithName("user-srv"),
        app.WithRPCServer(rpcServer),
        app.WithRegistrar(registry),
    ).Run()
}

三、服务发现流程

当 Consumer 需要调用服务时,通过服务名从 Registry 发现服务实例。完整流程如下:

复制代码
┌─────────────────────┐
│  1. Consumer 启动    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────────────────────────────┐
│  2. 创建 gRPC 连接                           │
│                                             │
│  conn, _ := grpc.Dial(                      │
│      "discovery:///user-srv",  // 服务名    │
│      grpc.WithResolvers(builder),           │
│  )                                          │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  3. gRPC 解析 endpoint                       │
│                                             │
│     "discovery:///user-srv"                 │
│           │                                 │
│           ├── scheme = "discovery"          │
│           └── serviceName = "user-srv"      │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  4. Builder.Build() 创建 Resolver           │
│                                             │
│     a. 提取服务名:user-srv                 │
│     b. 调用 Discovery.Watch() 创建 Watcher  │
│     c. 创建 Resolver 实例                   │
│     d. 启动后台监听 goroutine               │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  5. Discovery.Watch() 创建监听器             │
│                                             │
│     a. 创建 serviceSet 管理实例和 watcher   │
│     b. 创建 watcher 实例                    │
│     c. 启动后台轮询 goroutine               │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  6. 后台轮询 Consul(长轮询机制)            │
│                                             │
│     for {                                   │
│         // 携带 WaitIndex 查询              │
│         instances = consul.Service(name, idx)│
│                                             │
│         if 服务变化 {                        │
│             serviceSet.broadcast(instances) │
│         }                                   │
│     }                                       │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  7. Resolver.watch() 监听变化                │
│                                             │
│     for {                                   │
│         instances := watcher.Next()  // 阻塞│
│         resolver.update(instances)          │
│     }                                       │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  8. Resolver.update() 更新连接地址           │
│                                             │
│     // 转换为 gRPC 地址格式                  │
│     addrs := []resolver.Address{            │
│         {Addr: "192.168.1.10:9000"},        │
│         {Addr: "192.168.1.11:9000"},        │
│     }                                       │
│                                             │
│     // 通知 gRPC 更新                        │
│     cc.UpdateState(resolver.State{          │
│         Addresses: addrs,                   │
│     })                                      │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  9. gRPC 负载均衡                            │
│                                             │
│     根据策略选择地址发起请求:                │
│     - round_robin: 轮询                     │
│     - pick_first: 选第一个                  │
│     - p2c: Power of Two Choices             │
└─────────────────────────────────────────────┘

代码示例

go 复制代码
// Consumer 连接代码
func main() {
    // 1. 创建 Registry
    registry := consul.New(consulClient)
    
    // 2. 创建 gRPC 连接(使用服务发现)
    conn, _ := rpcserver.DialInsecure(ctx,
        rpcserver.WithEndpoint("discovery:///user-srv"),
        rpcserver.WithDiscovery(registry),
    )
    
    // 3. 创建客户端
    client := pb.NewUserClient(conn)
    
    // 4. 调用服务(自动负载均衡)
    resp, _ := client.GetUser(ctx, &pb.Request{Id: 1})
}

四、核心组件详解

1. Registrar(注册器)

职责:将服务实例注册到 Registry,停止时注销。

go 复制代码
type Registrar interface {
    Register(ctx context.Context, service *ServiceInstance) error
    Deregister(ctx context.Context, service *ServiceInstance) error
}

使用者:Provider

2. Discovery(发现器)

职责:从 Registry 获取服务实例列表,创建监听器。

go 复制代码
type Discovery interface {
    GetService(ctx context.Context, name string) ([]*ServiceInstance, error)
    Watch(ctx context.Context, name string) (Watcher, error)
}

使用者:Consumer

3. Watcher(监听器)

职责:阻塞等待服务变化,返回最新的实例列表。

go 复制代码
type Watcher interface {
    Next() ([]*ServiceInstance, error)  // 阻塞等待
    Stop() error                         // 停止监听
}

使用者:Discovery(内部)

4. Builder(解析器工厂)

职责:gRPC 根据 scheme 匹配 Builder,调用 Build 创建 Resolver。

go 复制代码
type Builder interface {
    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
    Scheme() string  // 返回 "discovery"
}

使用者:gRPC(内部)

5. Resolver(解析器)

职责:解析服务名为地址列表,通知 gRPC 更新连接。

go 复制代码
type Resolver interface {
    Close()
    ResolveNow(ResolveNowOptions)
}

关键方法

  • watch():后台监听服务变化
  • update():将 ServiceInstance 转换为 resolver.Address
  • cc.UpdateState():通知 gRPC 更新连接

使用者:gRPC(内部)

五、Consul 长轮询机制

服务发现的核心是高效感知服务变化。Consul 提供了长轮询机制:

复制代码
┌─────────────────────────────────────────────────────┐
│              Consul 长轮询机制                       │
└─────────────────────────────────────────────────────┘

客户端请求:
    GET /v1/catalog/service/user-srv?index=100&wait=120s
                                           │        │
                                           │        └── 最长等待时间
                                           └── 上次的索引

Consul 行为:
    1. 如果服务没变化(index 仍为 100)
       → 阻塞等待,最多 120 秒
       
    2. 如果服务变化了(index 变为 101)
       → 立即返回新数据
       
    3. 如果 120 秒内无变化
       → 返回空结果,客户端重新请求

优点

  • 实时感知变化(有变化立即返回)
  • 减少无效查询(无变化时阻塞等待)
  • 降低 Consul 压力

六、组件调用关系图

复制代码
Provider 端                              Consumer 端

┌──────────────────┐                    ┌──────────────────┐
│     main.go      │                    │     main.go      │
│                  │                    │                  │
│  app.New().Run() │                    │  DialInsecure()  │
└────────┬─────────┘                    └────────┬─────────┘
         │                                       │
         ▼                                       ▼
┌──────────────────┐                    ┌──────────────────┐
│      App         │                    │    Builder       │
│                  │                    │                  │
│  - buildInstance │                    │  Build()         │
│  - Register      │                    └────────┬─────────┘
└────────┬─────────┘                             │
         │                                       ▼
         │                             ┌──────────────────┐
         │                             │    Discovery     │
         │                             │                  │
         │                             │  Watch()         │
         │                             └────────┬─────────┘
         │                                      │
         ▼                                      ▼
┌──────────────────┐                    ┌──────────────────┐
│    Registrar     │                    │    Watcher       │
│                  │                    │                  │
│  Register()      │◄───────────────────│  Next()          │
└────────┬─────────┘                    └────────┬─────────┘
         │                                       │
         │                                       │
         ▼                                       ▼
┌────────────────────────────────────────────────────────────┐
│                      Consul Registry                       │
│                                                            │
│   Service: user-srv                                        │
│     - ID: xxx, Address: 192.168.1.10:9000, Check: TCP    │
│     - ID: yyy, Address: 192.168.1.11:9000, Check: TCP    │
│                                                            │
│   长轮询:WaitIndex 查询 → 无变化阻塞 → 有变化立即返回      │
└────────────────────────────────────────────────────────────┘

七、数据流向

服务注册数据流

复制代码
  Provider                    Microg                     Consul
     │                           │                          │
     │ app.Run()                 │                          │
     ├──────────────────────────►│                          │
     │                           │                          │
     │                           │ ServiceInstance          │
     │                           │ {ID, Name, Endpoints}    │
     │                           ├─────────────────────────►│
     │                           │                          │
     │                           │                          │ 存储
     │◄──────────────────────────┤ 注册成功                 │

服务发现数据流

复制代码
  Consumer                   Resolver                   Consul
     │                           │                          │
     │ Dial("discovery:///...")  │                          │
     ├──────────────────────────►│                          │
     │                           │                          │
     │                           │ Watch("user-srv")        │
     │                           ├─────────────────────────►│
     │                           │                          │
     │                           │     长轮询等待变化        │
     │                           │◄─────────────────────────┤
     │                           │                          │
     │                           │ instances: [...]         │
     │                           │                          │
     │◄──────────────────────────┤ UpdateState(addrs)       │
     │                           │                          │
     │ gRPC 连接到新地址          │                          │

八、总结

核心流程

复制代码
Provider 用 Registrar 注册服务到 Consul

Consumer 用 Discovery 从 Consul 发现服务

Discovery 创建 Watcher 监听服务变化

gRPC 调用 Builder 创建 Resolver

Resolver 通过 Watcher 获取实例,通知 ClientConn 更新连接

组件职责一览

组件 职责 使用者
Registrar 注册/注销服务 Provider
Discovery 获取服务列表,创建监听器 Consumer
Watcher 监听服务变化 Consumer(内部)
Builder 创建 Resolver gRPC(内部)
Resolver 解析服务地址,通知更新 gRPC(内部)

设计亮点

  1. 角色清晰:Provider/Consumer/Registry 职责分明
  2. 实时感知:Watch 机制 + 长轮询,实时感知服务变化
  3. 高效节能:长轮询减少无效查询
  4. 标准接口:遵循 gRPC resolver 标准,易于扩展

本文完整讲解了 gRPC 服务发现的原理与实现。理解这些机制,有助于更好地构建微服务应用。

项目地址https://github.com/atliliw/microg

如果觉得有帮助,欢迎点赞收藏,给个 Star ⭐!

相关推荐
ALex_zry2 小时前
微服务架构下的服务发现与注册:gRPC服务治理实战
微服务·架构·服务发现
AI精钢4 小时前
什么是面向 Agent 的 LLM?从 Qwen3.6-Plus 看大模型的新分水岭
网络·数据库·人工智能·云原生·aigc
AI精钢5 小时前
从 Prompt Engineering 到 Fine-Tuning:LLM 应用落地的理性决策框架
大数据·人工智能·云原生·prompt·aigc
Java面试题总结6 小时前
测试文章 #95 — 平台发布验证(51CTO/OSCHINA/Juejin)
consul
weixin_397578026 小时前
web前端怎么调用后端接口
微服务
笑笑先生8 小时前
Proxy 与 Namespace:终结环境与鉴权的噩梦
后端·微服务·架构
Chasing__Dreams8 小时前
Linux--操作系统--7--IPC、RPC
linux·运维·rpc
irpywp8 小时前
EmDash:重构内容基建的Serverless范式
云原生·重构·serverless
8Qi88 小时前
微服务通信:同步 vs 异步与MQ选型指南
java·分布式·微服务·云原生·中间件·架构·rabbitmq