为了提高系统的负载能力和稳定性,我们的服务端往往具有多台服务器,负载均衡的目的就是希望请求能分散到不同的服务器,从服务器列表中选择一台服务器的算法就是负载均衡的策略,常见的轮循、加权轮询等
负载均衡器要在多台服务器之间选择,所以通常情况下负载均衡器是具备服务发现的能力的
gRPC的负载均衡
gRPC中的负载平衡是以每次调用为基础,而不是以每个连接为基础。换句话说,即使所有的请求都来自一个客户端,它仍能在所有的服务器上实现负载平衡
gRPC目前内置四种策略
🌲 pick_first
:默认策略,选择第一个
🌲 round_robin
:轮询
使用默认的负载均衡器很简单,只需要在建立连接的时候指定负载均衡策略即可。
⚠️ 注意
旧版本gRPC使用
grpc.WithBalancerName("round_robin"),
已经被废弃,使用grpc.WithDefaultServiceConfig
。
grpc.WithDefaultServiceConfig
可以被上文服务发现中提到的cc.UpdateState(State) error
覆盖配置
conn, err := grpc.Dial("example:cluster@callee",
grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(
`{"loadBalancingPolicy":"round_robin"}`,
),
)
🌲 grpclb
:已废弃
它属于上文介绍的负载均衡中独立负载均衡进程第二种。不必直接在客户端中添加新的LB策略,而只实现诸如round-robin之类的简单算法,任何更复杂的算法都将由lookaside负载平衡器提供
🌲 xDS
如果接触过servicemesh那么对xDS并不会陌生,xDS 本身是Envoy中的概念,现在已经发展为用于配置各种数据平面软件的标准,最新版本的gRPC已经支持xDS。不同于单纯的负载均衡策略,xDS在gRPC包含了服务发现和负载均衡的概念
这里简单的理解下xDS,本质上xDS就是一个标准的协议
它规定了xDS客户端和xDS服务端的交互流程,即API调用的顺序
我们在xDS 服务端实现服务发现,配置负载均衡策略等等,支持xDS的客户端连接到xDS 服务端并通过xDS api来获取各种需要的数据和配置
xDS主要应用于servicemesh中,在mesh中由sidecar连接到xDS server进行数据交互,同时由sidecar来控制流量的分发。也就是上文提到的独立负载均衡进程的第一种模式
gRPC使用xDS是一种无proxy的客户端负载均衡方案。对比Mesh方案,性能更好。我们把servicemesh的负载均衡和grpc的xds负载均衡放在一起感受下区别
📖 这意味着,grpclb
废弃后,gRPC内置的pick_first
、round_robin
、xDS
三种模式都属于客户端的负载均衡模式。pick_first
、round_robin
是单纯的负载均衡策略;xDS
包含了服务发现等一系列能力,并且还在不断拓展中,而我们控制gRPC的行为也被转移到了xDS server上了。
自定义负载均衡器
自定义负载均衡器需要使用google.golang.org/grpc/balancer.Register
提前注册,此函数和服务发现一样接受工厂函数
// Builder creates a balancer.
type Builder interface {
// Build creates a new balancer with the ClientConn.
Build(cc ClientConn, opts BuildOptions) Balancer
// Name returns the name of balancers built by this builder.
// It will be used to pick balancers (for example in service config).
Name() string
}
✨ Name()
是负载均衡策略的名字
✨ Build(...)
需要返回负载均衡器
✨✨ cc ClientConn
代表客户端与服务端的连接,其拥有一系列函数可以让我们更新链接的状态
type Balancer interface {
// UpdateClientConnState is called by gRPC when the state of the ClientConn
// changes. If the error returned is ErrBadResolverState, the ClientConn
// will begin calling ResolveNow on the active name resolver with
// exponential backoff until a subsequent call to UpdateClientConnState
// returns a nil error. Any other errors are currently ignored.
UpdateClientConnState(ClientConnState) error
// ResolverError is called by gRPC when the name resolver reports an error.
ResolverError(error)
// UpdateSubConnState is called by gRPC when the state of a SubConn
// changes.
UpdateSubConnState(SubConn, SubConnState)
// Close closes the balancer. The balancer is not required to call
// ClientConn.RemoveSubConn for its existing SubConns.
Close()
}
负载均衡器需要实现一系列的函数用于gRPC在不同场景下调用
类RR算法负载均衡器
如果要实现一个类round_robin的负载均衡策略,gRPC官方实现提供了一个baseBuilder
,它已经实现了大部Balancer
接口,可以大幅简化了我们创建RR策略的逻辑。使用google.golang.org/grpc/balancer/base.NewBalancerBuilder
创建负载均衡的工厂
func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder
name string
负载均衡器的名字pb PickerBuilder
是我们要实现的负载均衡策略逻辑的地方config Config
可以配置是否要进行健康检查
// PickerBuilder creates balancer.Picker.
type PickerBuilder interface {
// Build returns a picker that will be used by gRPC to pick a SubConn.
Build(info PickerBuildInfo) balancer.Picker
}
type Picker interface {
// 子连接选择
Pick(info PickInfo) (PickResult, error)
}
于是借助base.NewBalancerBuilder
我们仅需要实现Picker
一个函数即可实现类RR的负载均衡策略了
利用Picker
接口来实现一个随机选择策略
# builder.go
package balancer
import (
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
)
var _ base.PickerBuilder = &Builder{}
type Builder struct {
}
func NewBalancerBuilder() balancer.Builder {
return base.NewBalancerBuilder("random_picker", &Builder{}, base.Config{HealthCheck: true})
}
func (b *Builder) Build(info base.PickerBuildInfo) balancer.Picker {
if len(info.ReadySCs) == 0 {
return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}
var scs []balancer.SubConn
for subConn := range info.ReadySCs {
scs = append(scs, subConn)
}
return &Picker{
subConns: scs,
}
}
// picker.go
package balancer
import (
"math/rand"
"google.golang.org/grpc/balancer"
)
var _ balancer.Picker = &Picker{}
type Picker struct {
subConns []balancer.SubConn
}
func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
index := rand.Intn(len(p.subConns))
sc := p.subConns[index]
return balancer.PickResult{SubConn: sc}, nil}
👇 欢迎关注👇
最后推荐一下我的Go项目实战专栏本课程是教大家用Go语言从零开始搭建项目和做需求开发的实战课程,使用的技术栈均为实际开发所常用的组件和框架如:Gin、Viper、Zap、GORM、go-redis 、lo 等等。
课程分为五大部分:
第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项
具体的章节想去课扫码下方的海报二维码或者访问
https://xiaobot.net/p/golang
点击下方阅读原文即可跳转。