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

标签: grpc 发现 负载均衡 | 发表时间:2020-12-07 22:52 | 作者:kevinwan
出处:https://www.v2ex.com/

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

基本原理

原理流程图如下:

yuanli

从图中可以看出 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

因此可以看出自定义 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

dialcontext

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

ccresolverwrapper

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

http2

到此 ClientConn 创建过程基本结束,我们再一起梳理一下整个过程,首先获取 resolver,其中 ccResolverWrapper 实现了 resovler.ClientConn 接口,通过 Resolver 的 UpdateState 方法触发获取 Balancer,获取 Balancer,其中 ccBalancerWrapper 实现了 balancer.ClientConn 接口,通过 Balnacer 的 UpdateClientConnState 方法触发创建连接(SubConn),最后创建 HTTP2 Client

Balancer 模块

balancer 模块用来在客户端发起请求时进行负载均衡,如果没有注册自定义的 balancer 的话 gRPC 会采用默认的负载均衡算法,流程图如下

balancer

在 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 进行处理

client_call

总结

本文主要分析了 gRPC 的 resolver 模块和 balancer 模块,详细介绍了如何自定义 resolver 和 balancer,以及通过分析 go-zero 中对 resolver 和 balancer 的实现了解了自定义 resolver 和 balancer 的过程,同时还分析可客户端创建的流程和调用的流程。希望本文能给大家带来一些帮助

项目地址

https://github.com/tal-tech/go-zero

如果觉得文章不错,欢迎 github 点个 star

相关 [grpc 发现 负载均衡] 推荐:

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

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

Nginx gRPC Streaming 负载均衡的排坑和思考

- - IT瘾-dev
我们知道nginx在1.13版本之后就可以支持grpc的负载均衡了. 官方给出的使用也很简单,类似proxy_pass的语法. 但在使用的过程中遇到短连接的问题. 该文章后续仍在不断的更新修改中, 请移步到原文地址 http://xiaorui.cc/?p=5970. 大量的timewait短连接:.

grpc在k8s中的负载均衡问题

- - holmofy
两台skywalking-oap接受并分析由agent采集的trace数据,但是问题是两台oap服务负载不均衡. #k8s的service四层负载均衡. 为了排除k8s的service负载均衡的问题,在线下环境还原了请求的过程. skywalking提供了grpc(11800端口)和rest(12800端口)两种协议的服务.

Marathon 服务发现及负载均衡 marathon-lb

- - 企业架构 - ITeye博客
       从官网摘抄了Mesos-DNS的缺陷,也是选择使用marathon-lb做服务发现和负载均衡的原因.        DNS does not identify service ports, unless you use an SRV query; most apps are not able to use SRV records “out of the box.”.

Kubernetes中的服务发现与负载均衡

- - Feisky's Blog
Kubernetes在设计之初就充分考虑了针对容器的服务发现与负载均衡机制,提供了Service资源,并通过kube-proxy配合cloud provider来适应不同的应用场景. 随着kubernetes用户的激增,用户场景的不断丰富,又产生了一些新的负载均衡机制. 目前,kubernetes中的负载均衡大致可以分为以下几种机制,每种机制都有其特定的应用场景:.

Docker Swarm - 服务发现和负载均衡原理 - 简书

- -
本文将介绍基于 DNS 的负载均衡、基于 VIP 的负载均衡和路由网格(Routing Mesh). Docker 使用了 Linux 内核 iptables 和 IPVS 的功能来实现服务发现和负载均衡. iptables 是 Linux 内核中可用的包过滤技术,它可用于根据数据包的内容进行分类、修改和转发决策.

浅谈 gRPC

- - SegmentFault 最新的文章
原文地址: 浅谈 gRPC. gRPC 在 Go 语言中大放异彩,越来越多的小伙伴在使用,最近也在公司安利了一波,希望能通过这篇文章能带你一览 gRPC 的爱与恨. 本文篇幅较长,希望你做好阅读准备,目录如下:. gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计.

nginx负载均衡配置

- - 开心平淡对待每一天。热爱生活
  使用负载均衡的话,可以修改配置http节点如下:. #设定http服务器,利用它的反向代理功能提供负载均衡支持. #设定mime类型,类型由mime.type文件定义. #省略上文有的一些配置节点. #设定负载均衡的服务器列表. #weigth参数表示权值,权值越高被分配到的几率越大. server 192.168.8.1x:3128 weight=5;#本机上的Squid开启3128端口.

解析nginx负载均衡

- - 搜索研发部官方博客
摘要:对于一个大型网站来说,负载均衡是永恒的话题. 随着硬件技术的迅猛发展,越来越多的负载均衡硬件设备涌现出来,如F5 BIG-IP、Citrix NetScaler、Radware等等,虽然可以解决问题,但其高昂的价格却往往令人望而却步,因此负载均衡软件仍然是大部分公司的不二之选. nginx作为webserver的后起之秀,其优秀的反向代理功能和灵活的负载均衡策略受到了业界广泛的关注.

Haproxy+KeepAlived 负载均衡

- - CSDN博客系统运维推荐文章
软件负载均衡一般通过两种方式来实现:基于操作系统的软负载实现和基于第三方应用的软负载实现. LVS就是基于Linux操作系统实现的一种软负载,HAProxy就是开源的并且基于第三应用实现的软负载. 还可以使用nginx来实现,不过nginx只工作在7层网络之上. 详细请参考 抚琴煮酒写的“ 软件级负载均衡器(LVS/HAProxy/Nginx)的特点简介和对比”这篇文章,简单很详细,很好.