乐趣区

关于grpc:gRPC-源码阅读及实践之-resolver

Resolver

gRPC 插件式编程之 Resolver


随着微服务越来越流行,服务间的通信也是绕不开的话题,gRPC 在泛滥 RPC 框架中算得上佼佼者,不仅其有一个好爸爸,grpc 在扩大方面也给开发者留
有足够的空间,明天咱们将走进 grpc 扩大之 Resolver,gRPC Resolver 提供了用户自行解析主机的扩大能力,咱们在应用 gRPC 时,大家有没有想过,
为什么 gRPC 为什么反对以下几种格局的 target:

  • 直连,链接 target 为指标服务的 endpoint
  • dns 服务发现
  • unix

其中在进入连贯之前,gRPC 会依据用户是否提供了 Resolver 来进行指标服务的 endpoint 解析,明天咱们来尝试写一个最简略的 etcd 做服务发现的例子

阐明

源码浏览的 gRPC 版本为 3.5.1

环境

  • etcd 装置
  • go

思路

  • 咱们将为 server 服务,假如名称为 grpc-server 启动多个实例
  • grpc-server 为 key 向 etcd put 每个实例的 endpoint

    • 真正进入 etcd 的 key 为以 grpc-server + / + 随机值
  • 实现 resolver.Builder,获取 target
  • 从 etcd 读取以 grpc-server 为 prefix 的 endpoints
  • 告诉负载均衡器从新 pick 实例

实现

实现 resolver.Builder

type customBuilder struct {scheme string}

func NewCustomBuilder(scheme string) resolver.Builder {
  return &customBuilder{scheme: scheme,}
}

func (b *customBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {var address []resolver.Address
  key := target.URL.Host
  hosts := pool.GetOr(key, nil)
  fmt.Println(hosts)
  for _, host := range hosts {
    address = append(address, resolver.Address{Addr: host,})
  }
  cc.UpdateState(resolver.State{Addresses: address})
  return &nopResolver{}, nil}

func (b *customBuilder) Scheme() string {return Scheme}

利用

在 client 发动调用时通过 grpc.WithResolvers DialOption 告知 gRPC

r := builder.NewCustomBuilder(builder.Scheme)
    conn, err := grpc.Dial(builder.Format("grpc-server"), grpc.WithInsecure(), grpc.WithResolvers(r))

grpc.Dial 的第一个参数为 target,因而 target 并非肯定是指标服务的 endpoint(仅直连模式才传指标服务的真正 endpoint),也可能是
指向某一个注册核心的遵循 URL 地址标准的一个值,便于开发者自定义 resolver.Builder 依据 target 拿到相应信息去做指标服务真正的 endpoints 解析,
如上文的 resolver.Builder 实现办法

Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error)

时,则依据解析后的 resolver.Target 拿到 etcd 的 key,再去获取指标服务的 endpoints,至于解析完后怎么告诉负载均衡器的咱们后续再讲。

示例后果

  • 启动 etcd

    $ etcd
  • 启动两个 server

    go run server.go -addr localhost:8888
    go run server.go -addr localhost:8889
  • 启动 client

    $ go run client.go
    endpoints: [localhost:8888 localhost:8889]
    output: hi

原理

咱们从 client 通过 grpc.WithResolvers 告知 gRPC resolver.Builder 后,他是怎么调用咱们给的 resolver 的?
顺着 grpc.DialContext 源码看上来就晓得,gRPC 会调用一个 ClientConn.parseTargetAndFindResolver 的办法,该
办法做了两个工作:

  • grpc.DialContexttarget string 值通过 parseTarget 解析为 resolver.Target,新版本为 URL 的包装体
  • 寻找 resolver

    • gRPC 会优先从 resolver.Target 中的获取 scheme 名称,该值即为开发者在实现 resolver.BuilderScheme() string 办法返回值一样。
    • gRPC 去 DialOption 中的 resolver 列表寻找名称雷同 resolver
    • 通过 newCCResolverWrapper 办法调用 resolver.Buidler.Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)办法实现解析
    • 告知负载均衡器后续解决

合成

  1. gRPC 怎么晓得该获取那个 resolver.Builder?
    咱们在通过 grpc.DialContext 传递的 target 肯定要合乎 gRPC 标准
    其实就是合乎 URL 格局,如 http://foo.comhttp 即为 scheme,咱们这里 target 格局为 custom://xxxxxx 为 server 端的实例名称(即 grpc-server
    这样咱们就通知了 gRPC 该抉择 schemecustom 的 resolver.Builder。
  2. gRPC 在哪里找到 resolver.Builder 的?
    gRPC 的 resolver.Builder 并不会无中生有,而是咱们在实例化 resolver.Buidler 的实现类时进行注册了,其实就是写到 gRPC 外部的一个全局 map 变量中了,
    gRPC 在寻找是也是通 scheme 为 key 去这个 map 里找。
  • 注册 resolver.Builder

    func init() {resolver.Register(&customBuilder{})
    }
    func Register(b Builder) {m[b.Scheme()] = b
    }
var (
    // m is a map from scheme to resolver builder.
    m = make(map[string]Builder)
    // defaultScheme is the default scheme to use.
    defaultScheme = "passthrough"
)
  • 获取 resolver.Builder
for _, rb := range cc.dopts.resolvers {if scheme == rb.Scheme() {return rb}
}
return resolver.Get(scheme)
  1. 解析 targetresolver.Target

    func parseTarget(target string) (resolver.Target, error) {u, err := url.Parse(target)
      if err != nil {return resolver.Target{}, err
      }
      
      endpoint := u.Path
      if endpoint == "" {endpoint = u.Opaque}
      endpoint = strings.TrimPrefix(endpoint, "/")
      return resolver.Target{
       Scheme:    u.Scheme,
       Authority: u.Host,
       Endpoint:  endpoint,
       URL:       *u,
      }, nil
    }

源码地位

clientconn.go:1622

其余

在 gRPC 中,像这种 register & get 的模式成为插件式编程,通过这种伎俩给开发者提供了扩大入口,除 resolver 外,
balancercompressor 等都利用了这个伎俩提供了扩大入口,后续咱们再来探讨。

总结

gRPC 的 grpc.Dial 或者 gprc.DialContexttarget 并非肯定是 server 的 endpoint,也有可能是满足开发者需要的
某类合乎 URL 命名格调的值,如本 demo 中是 server 服务向 etcd 数据库存储 server 启动的每个实例的 endpoint 的 keyprefix
如果用户没有提供 resolver.Builder,gRPC 会依据默认 schemeresolver.GetDefaultScheme())去查找。

源码

https://github.com/anqiansong…

退出移动版