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的注册发现与负载均衡的原理和实战的更多相关文章

  1. springboot使用zookeeper(curator)实现注册发现与负载均衡

    最简单的实现服务高可用的方法就是集群化,也就是分布式部署,但是分布式部署会带来一些问题.比如: 1.各个实例之间的协同(锁) 2.负载均衡 3.热删除 这里通过一个简单的实例来说明如何解决注册发现和负 ...

  2. grpc服务发现与负载均衡

    前言 在后台服务开发中,高可用性是构建中核心且重要的一环.服务发现(Service discovery)和负载均衡(Load Balance)一直都是我关注的话题.今天来谈一下我在实际中是如何理解及落 ...

  3. 服务发现与负载均衡 dubbo zk原理

    服务发现与负载均衡 拓展阅读 : dubbo 原理概念图 2016-03-03 杜亦舒 性能与架构 性能与架构 性能与架构 微信号 yogoup 功能介绍 网站性能提升与架构设计 内容整理自文章“实施 ...

  4. 基于marathon-lb的服务自发现与负载均衡

    参考文档: Marathon-lb介绍:https://docs.mesosphere.com/1.9/networking/marathon-lb/ 参考:http://www.cnblogs.co ...

  5. java版gRPC实战之七:基于eureka的注册发现

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  6. Nginx基于TCP/UDP端口的四层负载均衡(stream模块)配置梳理

    通过我们会用Nginx的upstream做基于http/https端口的7层负载均衡,由于Nginx老版本不支持tcp协议,所以基于tcp/udp端口的四层负载均衡一般用LVS或Haproxy来做.至 ...

  7. (转) Docker - Docker1.12服务发现,负载均衡和Routing Mesh

    看到一篇介绍 Docker swarm以及如何编排的好文章,挪放到这里,自己学习的同时也分享出来. 原文链接: http://wwwbuild.net/dockerone/414200.html -- ...

  8. 【云计算】mesos+marathon 服务发现、负载均衡、监控告警方案

    Mesos-dns 和 Marathon-lb 是mesosphere 官网提供的两种服务发现和负载均衡工具.官方的文档主要针对DCOS,针对其它系统的相关中文文档不多,下面是我在Centos7上的安 ...

  9. 从零开始入门 | Kubernetes 中的服务发现与负载均衡

    作者 | 阿里巴巴技术专家  溪恒 一.需求来源 为什么需要服务发现 在 K8s 集群里面会通过 pod 去部署应用,与传统的应用部署不同,传统应用部署在给定的机器上面去部署,我们知道怎么去调用别的机 ...

随机推荐

  1. 庐山真面目之二微服务架构NGINX版本实现

    一.简介          在上一篇文章<庐山真面目之微服务的简介和技术栈>中,我们已经探讨了微服务的来龙去脉,也说了想要实现微服务架构所需要的技术栈,今天我们开始实现一个微服务,当然这个 ...

  2. MYSQL字段

    这里我的测试环境是wampserver 选择数据库 选择表 或者创建数据库和表 SHOW DATABASES; 查看数据库 CREATE DATABASE 数据库名; 创建数据库 在 MySQL 中, ...

  3. 基于CSS3伪元素和动画绘制旋转太极图

    通过CSS3的动画知识来完成一个旋转的太极. 任务 1.创建一个div,用CSS控制其大小.边框.位置等,做成一个静态的圆形,一半为红色一半为白色. 2.用div的伪元素位置两个圆环并放置核实位置,使 ...

  4. redis重点ppt

  5. 1. 线性DP 300. 最长上升子序列 (LIS)

    最经典单串: 300. 最长上升子序列 (LIS) https://leetcode-cn.com/problems/longest-increasing-subsequence/submission ...

  6. 适合 Go 新手学习的开源项目——在 GitHub 学编程

    作者:HelloGitHub-小鱼干&卤蛋 故事要从 2007 年说起.因为受够了 C++ 煎熬的 Google 首席软件工程师 Rob Pike 召集 Robert Griesemer 和 ...

  7. 双数组字典树(Double Array Trie)

    参考文献 1.双数组字典树(DATrie)详解及实现 2.小白详解Trie树 3.论文<基于双数组Trie树算法的字典改进和实现> DAT的基本内容介绍这里就不展开说了,从Trie过来的同 ...

  8. git , repo out off memory

    解决方法,建立swap # dd if=/dev/zero of=/root/myswapfile bs=1M count=1024 1024+0 records in 1024+0 records ...

  9. linux之NTP服务

    1. NTP服务(网络时间协议) Network Time Protocol(NTP)是用来使计算机时间同步化的一种协议,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒) ...

  10. SpringBoot WebSocket 消息交互

    1. Websocket原理 Websocket协议本质上是一个基于TCP的独立协议,能够在浏览器和服务器之间建立双向连接,以基于消息的机制,赋予浏览器和服务器间实时通信能力. WebSocket资源 ...