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
+/
+随机值
- 真正进入 etcd 的 key 为以
- 实现 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.DialContext
的target
string 值通过parseTarget
解析为resolver.Target
,新版本为URL
的包装体 -
寻找 resolver
- gRPC 会优先从
resolver.Target
中的获取scheme
名称,该值即为开发者在实现resolver.Builder
时Scheme() string
办法返回值一样。 - gRPC 去 DialOption 中的 resolver 列表寻找名称雷同 resolver
- 通过
newCCResolverWrapper
办法调用resolver.Buidler.Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
办法实现解析 - 告知负载均衡器后续解决
- gRPC 会优先从
合成
- gRPC 怎么晓得该获取那个 resolver.Builder?
咱们在通过grpc.DialContext
传递的target
肯定要合乎 gRPC 标准
其实就是合乎URL
格局,如http://foo.com
,http
即为 scheme,咱们这里target
格局为custom://xxx
,xxx
为 server 端的实例名称(即grpc-server
)
这样咱们就通知了 gRPC 该抉择scheme
为custom
的 resolver.Builder。 - 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)
-
解析
target
为resolver.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
外,balancer
、compressor
等都利用了这个伎俩提供了扩大入口,后续咱们再来探讨。
总结
gRPC 的 grpc.Dial
或者 gprc.DialContext
的 target
并非肯定是 server 的 endpoint,也有可能是满足开发者需要的
某类合乎 URL
命名格调的值,如本 demo 中是 server 服务向 etcd 数据库存储 server 启动的每个实例的 endpoint 的 key
的 prefix
,
如果用户没有提供 resolver.Builder
,gRPC 会依据默认 scheme
(resolver.GetDefaultScheme()
)去查找。
源码
https://github.com/anqiansong…