基于gRPC的注册发现与负载均衡的原理和实战
gRPC是一个现代的、高性能、开源的和语言无关的通用RPC框架,基于HTTP2协议设计,序列化使用PB(Protocol Buffer),PB是一种语言无关的高性能序列化框架,基于HTTP2+PB保证了的高性能。go-zero是一个开源的微服务框架,支持http和rpc协议,其中rpc底层依赖gRPC,本文会结合gRPC和go-zero源码从实战的角度和大家一起分析下服务注册与发现和负载均衡的实现原理
基本原理
原理流程图如下:

从图中可以看出go-zero实现了gRPC的resolver和balancer接口,然后通过gprc.Register方法注册到gRPC中,resolver模块提供了服务注册的功能,balancer模块提供了负载均衡的功能。当client发起服务调用的时候会根据resolver注册进来的服务列表,使用注册进来的balancer选择一个服务发起请求,如果没有进行注册gRPC会使用默认的resolver和balancer。服务地址的变更会同步到etcd中,go-zero监听etcd的变化通过resolver更新服务列表
Resolver模块
通过resolver.Register方法可以注册自定义的Resolver,Register方法定义如下,其中Builder为interface类型,因此自定义resolver需要实现该接口,Builder定义如下
// Register 注册自定义resolver
func Register(b Builder) {
m[b.Scheme()] = b
}
// Builder 定义resolver builder
type Builder interface {
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
Scheme() string
}
Build方法的第一个参数target的类型为Target定义如下,创建ClientConn调用grpc.DialContext的第二个参数target经过解析后需要符合这个结构定义,target定义格式为: scheme://authority/endpoint_name
type Target struct {
Scheme string // 表示要使用的名称系统
Authority string // 表示一些特定于方案的引导信息
Endpoint string // 指出一个具体的名字
}
Build方法返回的Resolver也是一个接口类型。定义如下
type Resolver interface {
ResolveNow(ResolveNowOptions)
Close()
}
流程图下图

因此可以看出自定义Resolver需要实现如下步骤:
- 定义target
- 实现resolver.Builder
- 实现resolver.Resolver
- 调用resolver.Register注册自定义的Resolver,其中name为target中的scheme
- 实现服务发现逻辑(etcd、consul、zookeeper)
- 通过resolver.ClientConn实现服务地址的更新
go-zero中target的定义如下,默认的名字为discov
// BuildDiscovTarget 构建target
func BuildDiscovTarget(endpoints []string, key string) string {
return fmt.Sprintf("%s://%s/%s", resolver.DiscovScheme,
strings.Join(endpoints, resolver.EndpointSep), key)
}
// RegisterResolver 注册自定义的Resolver
func RegisterResolver() {
resolver.Register(&dirBuilder)
resolver.Register(&disBuilder)
}
Build方法的实现如下
func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
return r == EndpointSepChar
})
// 获取服务列表
sub, err := discov.NewSubscriber(hosts, target.Endpoint)
if err != nil {
return nil, err
}
update := func() {
var addrs []resolver.Address
for _, val := range subset(sub.Values(), subsetSize) {
addrs = append(addrs, resolver.Address{
Addr: val,
})
}
// 调用UpdateState方法更新
cc.UpdateState(resolver.State{
Addresses: addrs,
})
}
// 添加监听,当服务地址发生变化会触发更新
sub.AddListener(update)
// 更新服务列表
update()
return &nopResolver{cc: cc}, nil
}
那么注册进来的resolver在哪里用到的呢?当创建客户端的时候调用DialContext方法创建ClientConn的时候回进行如下操作
- 拦截器处理
- 各种配置项处理
- 解析target
- 获取resolver
- 创建ccResolverWrapper
创建clientConn的时候回根据target解析出scheme,然后根据scheme去找已注册对应的resolver,如果没有找到则使用默认的resolver

ccResolverWrapper的流程如下图,在这里resolver会和balancer会进行关联,balancer的处理方式和resolver类似也是通过wrapper进行了一次封装

紧着着会根据获取到的地址创建htt2的链接

到此ClientConn创建过程基本结束,我们再一起梳理一下整个过程,首先获取resolver,其中ccResolverWrapper实现了resovler.ClientConn接口,通过Resolver的UpdateState方法触发获取Balancer,获取Balancer,其中ccBalancerWrapper实现了balancer.ClientConn接口,通过Balnacer的UpdateClientConnState方法触发创建连接(SubConn),最后创建HTTP2 Client
Balancer模块
balancer模块用来在客户端发起请求时进行负载均衡,如果没有注册自定义的balancer的话gRPC会采用默认的负载均衡算法,流程图如下

在go-zero中自定义的balancer主要实现了如下步骤:
- 实现PickerBuilder,Build方法返回balancer.Picker
- 实现balancer.Picker,Pick方法实现负载均衡算法逻辑
- 调用balancer.Registet注册自定义Balancer
- 使用baseBuilder注册,框架已提供了baseBuilder和baseBalancer实现了Builer和Balancer
Build方法的实现如下
func (b *p2cPickerBuilder) Build(readySCs map[resolver.Address]balancer.SubConn) balancer.Picker {
if len(readySCs) == 0 {
return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}
var conns []*subConn
for addr, conn := range readySCs {
conns = append(conns, &subConn{
addr: addr,
conn: conn,
success: initSuccess,
})
}
return &p2cPicker{
conns: conns,
r: rand.New(rand.NewSource(time.Now().UnixNano())),
stamp: syncx.NewAtomicDuration(),
}
}
go-zero中默认实现了p2c负载均衡算法,该算法的优势是能弹性的处理各个节点的请求,Pick的实现如下
func (p *p2cPicker) Pick(ctx context.Context, info balancer.PickInfo) (
conn balancer.SubConn, done func(balancer.DoneInfo), err error) {
p.lock.Lock()
defer p.lock.Unlock()
var chosen *subConn
switch len(p.conns) {
case 0:
return nil, nil, balancer.ErrNoSubConnAvailable // 没有可用链接
case 1:
chosen = p.choose(p.conns[0], nil) // 只有一个链接
case 2:
chosen = p.choose(p.conns[0], p.conns[1])
default: // 选择一个健康的节点
var node1, node2 *subConn
for i := 0; i < pickTimes; i++ {
a := p.r.Intn(len(p.conns))
b := p.r.Intn(len(p.conns) - 1)
if b >= a {
b++
}
node1 = p.conns[a]
node2 = p.conns[b]
if node1.healthy() && node2.healthy() {
break
}
}
chosen = p.choose(node1, node2)
}
atomic.AddInt64(&chosen.inflight, 1)
atomic.AddInt64(&chosen.requests, 1)
return chosen.conn, p.buildDoneFunc(chosen), nil
}
客户端发起调用的流程如下,会调用pick方法获取一个transport进行处理

总结
本文主要分析了gRPC的resolver模块和balancer模块,详细介绍了如何自定义resolver和balancer,以及通过分析go-zero中对resolver和balancer的实现了解了自定义resolver和balancer的过程,同时还分析可客户端创建的流程和调用的流程。希望本文能给大家带来一些帮助
项目地址
https://github.com/tal-tech/go-zero
如果觉得文章不错,欢迎 github 点个 star
基于gRPC的注册发现与负载均衡的原理和实战的更多相关文章
- springboot使用zookeeper(curator)实现注册发现与负载均衡
最简单的实现服务高可用的方法就是集群化,也就是分布式部署,但是分布式部署会带来一些问题.比如: 1.各个实例之间的协同(锁) 2.负载均衡 3.热删除 这里通过一个简单的实例来说明如何解决注册发现和负 ...
- grpc服务发现与负载均衡
前言 在后台服务开发中,高可用性是构建中核心且重要的一环.服务发现(Service discovery)和负载均衡(Load Balance)一直都是我关注的话题.今天来谈一下我在实际中是如何理解及落 ...
- 服务发现与负载均衡 dubbo zk原理
服务发现与负载均衡 拓展阅读 : dubbo 原理概念图 2016-03-03 杜亦舒 性能与架构 性能与架构 性能与架构 微信号 yogoup 功能介绍 网站性能提升与架构设计 内容整理自文章“实施 ...
- 基于marathon-lb的服务自发现与负载均衡
参考文档: Marathon-lb介绍:https://docs.mesosphere.com/1.9/networking/marathon-lb/ 参考:http://www.cnblogs.co ...
- java版gRPC实战之七:基于eureka的注册发现
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Nginx基于TCP/UDP端口的四层负载均衡(stream模块)配置梳理
通过我们会用Nginx的upstream做基于http/https端口的7层负载均衡,由于Nginx老版本不支持tcp协议,所以基于tcp/udp端口的四层负载均衡一般用LVS或Haproxy来做.至 ...
- (转) Docker - Docker1.12服务发现,负载均衡和Routing Mesh
看到一篇介绍 Docker swarm以及如何编排的好文章,挪放到这里,自己学习的同时也分享出来. 原文链接: http://wwwbuild.net/dockerone/414200.html -- ...
- 【云计算】mesos+marathon 服务发现、负载均衡、监控告警方案
Mesos-dns 和 Marathon-lb 是mesosphere 官网提供的两种服务发现和负载均衡工具.官方的文档主要针对DCOS,针对其它系统的相关中文文档不多,下面是我在Centos7上的安 ...
- 从零开始入门 | Kubernetes 中的服务发现与负载均衡
作者 | 阿里巴巴技术专家 溪恒 一.需求来源 为什么需要服务发现 在 K8s 集群里面会通过 pod 去部署应用,与传统的应用部署不同,传统应用部署在给定的机器上面去部署,我们知道怎么去调用别的机 ...
随机推荐
- RabbitMq 基本命令
rabbitmqadmin 命令汇总: 命令 解释 rabbitmqadmin list users 查看所有用户 User rabbitmqadmin list users name 查看所有用户名 ...
- C++实现学校运动会管理系统
本文实例为大家分享了C++实现学校运动会管理系统的具体代码,供大家参考,具体内容如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...
- 安装使用Pycharm及Anaconda最全教程
网上安装anaconda和pycharm的教程很多,然而很少有人能够很详细地讲解,特别是对于pycharm的虚拟环境相关的说明很少,我也是懵逼的用了两年多,经常发现之前pycharm安装的第三方库,明 ...
- S5830 android 2.3.4和2.3.7
12年元旦买的手机S5830,原机自带2.3.4的系统. 看到人家的机子2.3.6的效果稍微绚一点,动了想刷机的念头. 前两天刷了2.3.7,效果还满意,用的还舒服,感觉就是有些费电, 本来就对智能手 ...
- [MIT6.006] 22. Daynamic Programming IV: Guitar Fingering, Tetris, Super Mario Bro. 动态规划IV:吉他指弹,俄罗斯方块,超级玛丽奥
之前我们讲到动态规划五步中有个Guessing猜,一般情况下猜有两种情况: 在猜和递归上:猜的是用于解决更大问题的子问题: 在子问题定义上:如果要猜更多,就要增加更多子问题. 下面我们来看如果像背包问 ...
- prometheus监控golang服务实践
一.prometheus基本原理介绍 prometheus是基于metric采样的监控,可以自定义监控指标,如:服务每秒请求数.请求失败数.请求执行时间等,每经过一个时间间隔,数据都会从运行的服务中流 ...
- spring mvc 基础知识
spring mvc 在web.xml中的配置: 例子: <?xml version="1.0" encoding="UTF-8"?> <we ...
- 阻塞队列的take、offer、put、add的一些比较
LinkedBlockingQueue的put,add和offer的区别 最近在学习<<Java并发编程实践>>,有很多java.util.concurrent包下的新类.Li ...
- oracle的三种连接方式
1.通过sid jdbc:oracle:thin:@host:port:SID Example: jdbc:oracle:thin:@localhost:1521:sid_test 2.通过servi ...
- 云计算之路-出海记:建一个免费仓库 Amazon RDS for SQL Server
上周由于园子后院起火,不得不调兵回去救火,出海记暂时停更,这周继续更新,"出海记"记录的是我们在 AWS 上建设博客园海外站的历程. 在这一记中记录的是我们基于 AWS 免费套餐( ...