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.Addresskey := target.URL.Hosthosts := 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.Pathif 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/golang-notes/tree/main/example/grpc/resolver/builder/builder.go

gRPC源码阅读及实践之 Resolver相关推荐

  1. 【NLP】Transformers 源码阅读和实践

    本文主要针对HuggingFace开源的 transformers,以BERT为例介绍其源码并进行一些实践.主要以pytorch为例 (tf 2.0 代码风格几乎和pytorch一致),介绍BERT使 ...

  2. 基于lis3dh的简易倾角仪c源码_开源网关apisix源码阅读和最佳实践

    大家应该都接手过这种项目,前人找一个开源软件改一改,发上线. 我这里便曾经遇到过类似的问题. 随着需求的增加,各种维护人员东改改西改改,原来的开源项目被改的面目全非,再也无法和上游合并. 甚至TLS协 ...

  3. DM 源码阅读系列文章(二)整体架构介绍

    2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第二篇,第一篇文章 简单介绍了 DM 源码阅读的目的和规划,以及 DM 的源码结 ...

  4. strings.Builder 源码阅读与分析

    strings.Builder源码阅读与分析 背景之字符串拼接 在 Go 语言中,对于字符串的拼接处理有很多种方法,那么那种方法才是效率最高的呢? str := []string{"aa&q ...

  5. Spark源码阅读(五) --- Spark的支持的join方式以及join策略

    版本变动 2021-08-30 增加了对Broadcast Hash Join小表大小的评估内容 增加了对Sort Merge Join优于Shuffle Hash Join调用的解释 目录 Spar ...

  6. React 表单源码阅读笔记

    1 概念 1.1 什么是表单 实际上广义上的表单并不是特别好界定,维基上讲表单是一系列带有空格的文档,用于输写或选择.更具体的,在网页中表单主要负责数据采集的功能,我们下文中所提到的表单都指后者.如下 ...

  7. 大神手把手教源码阅读的方法、误区以及三种境界

    丁威 中间件兴趣圈 读完需要 1 分钟 速读仅需 1 分钟 在技术职场中普遍存在如下几种现象: 对待工作中所使用的技术不需要阅读源码,只需在开发过程中能够熟练运用就行 看源码太费时间,而且容易忘记,如 ...

  8. 【vn.py学习笔记(八)】vn.py utility、BarGenerator、ArrayManager源码阅读

    [vn.py学习笔记(八)]vn.py utility.BarGenerator.ArrayManager源码阅读 写在前面 1 工具函数 2 BarGenerator 2.1 update_tick ...

  9. 【vn.py学习笔记(五)】vn.py Base、Log、Oms、Email Engine源码阅读

    [vn.py学习笔记(五)]vn.py Base.Log.Oms.Email Engine源码阅读 写在前面 1 BaseEngine 2 LogEngine 3 OmsEngine 3.1 构造函数 ...

  10. 《源码阅读》专栏系列开篇 - 当Java工程师的这几年

    <源码阅读>专栏系列开篇 - 当Java工程师的这几年 2020年了,受疫情影响,开始在家坐起了"月子".利用这段时间梳理下自己的职业生涯规划. 职业生涯回顾 2013 ...

最新文章

  1. NLP任务中的文本预处理步骤、工具和示例
  2. 读“深度学习在图像处理领域中的应用综述”有感
  3. R异常数据检测及处理方法
  4. Cadvisor源码分析监控项
  5. java自定义日志级别_自定义log4j日志级别
  6. 移动应用发展远超Web 谷歌苹果将主导世界?联网的发展才刚刚开始
  7. Qt 项目视图的便捷类
  8. php7 setcookie无效_PHP setcookie() 函数 | 菜鸟教程
  9. ubuntu16.04 配置nginx支持redis
  10. 使用DynamoDB映射器将DynamoDB项目映射到对象
  11. Codeforces Round #325 (Div. 2) B. Laurenty and Shop 前缀和
  12. python安装sqap_python文件I/O
  13. 现代Web开发需要学习的15大技术
  14. 08版新精粹45集实战技巧视频教程免费下载wordExcel
  15. 如何利用github打造博客专属域名
  16. 我所认知的世界,不是Fragmention,而是Think
  17. java getsystemtime,草泥马之家-使用javaAgent解除Burp的时间限制
  18. pinctrl学习笔记---描述/获得引脚
  19. Counting swaps
  20. 2018阿里巴巴全球诸神之战创客大赛总决赛即将举行

热门文章

  1. python编程自然数表达式_实现四则运算 (python实现)by 周乃君 张宏根
  2. emv交易流程介绍,简易波动指标EMV基础知识介绍:EMV的计算公式_EMV应用法则
  3. Java实现 蓝桥杯 算法训练 递归求二项式系数
  4. Matplotlib-自定义虚线样式
  5. python爬取行业数据_Python爬取拉钩招聘网,让你清楚了解Python行业
  6. mysql mtq_Mysql常用简介 - osc_r3mtqivi的个人空间 - OSCHINA - 中文开源技术交流社区
  7. PDF文件如何加密?分享两种好用方法
  8. 计算机网络【奈氏准则和香农定理】
  9. 项目经理进阶:如何做好一个领导者
  10. python匿名函数优点_python匿名函数定义及实例解析