基于gRPC的注册发现与负载均衡的原理和实战

简介: 基于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进行了一次封装

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

到此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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
相关文章
|
9月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
负载均衡 算法 应用服务中间件
5大负载均衡算法及原理,图解易懂!
本文详细介绍负载均衡的5大核心算法:轮询、加权轮询、随机、最少连接和源地址散列,帮助你深入理解分布式架构中的关键技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5大负载均衡算法及原理,图解易懂!
|
负载均衡 网络协议
slb健康检查的基本原理
slb健康检查的基本原理
276 6
|
负载均衡 应用服务中间件 Apache
Tomcat负载均衡原理详解及配置Apache2.2.22+Tomcat7
Tomcat负载均衡原理详解及配置Apache2.2.22+Tomcat7
277 3
|
负载均衡 算法 调度
负载均衡原理分析与源码解读
负载均衡原理分析与源码解读
|
消息中间件 负载均衡 API
RocketMQ生产者负载均衡(轮询机制)核心原理
文章深入分析了RocketMQ生产者的负载均衡机制,特别是轮询机制的实现原理,揭示了如何通过`ThreadLocal`技术和消息队列的选播策略来确保消息在多个队列之间均衡发送,以及如何通过灵活的API支持自定义负载均衡策略。
|
存储 负载均衡 监控
自适应负载均衡算法原理和实现
自适应负载均衡算法原理和实现
|
负载均衡 网络协议 Linux
在Linux中,负载均衡的原理是什么?
在Linux中,负载均衡的原理是什么?
|
9月前
|
负载均衡 前端开发 应用服务中间件
Tomcat的负载均衡和动静分离(与nginx联动)
总的来说,负载均衡和动静分离是提高Web应用性能的两个重要手段。通过合理的配置和使用,我们可以让Web应用更好地服务于用户。
288 21
|
缓存 负载均衡 算法
解读 Nginx:构建高效反向代理和负载均衡的秘密
解读 Nginx:构建高效反向代理和负载均衡的秘密
334 2

相关实验场景

更多