上篇文章介绍了如何实现gRPC负载均衡,但目前官方只提供了pick_first
和round_robin
两种负载均衡策略,轮询法round_robin
不能知足因服务器配置不一样而承担不一样负载量,这篇文章将介绍如何实现自定义负载均衡策略--加权随机法
。node
加权随机法
能够根据服务器的处理能力而分配不一样的权重,从而实现处理能力高的服务器可承担更多的请求,处理能力低的服务器少承担请求。git
gRPC提供了V2PickerBuilder
和V2Picker
接口让咱们实现本身的负载均衡策略。github
type V2PickerBuilder interface { Build(info PickerBuildInfo) balancer.V2Picker }
V2PickerBuilder
接口:建立V2版本的子链接选择器。服务器
Build
方法:返回一个V2选择器,将用于gRPC选择子链接。app
type V2Picker interface { Pick(info PickInfo) (PickResult, error) }
V2Picker
接口:用于gRPC选择子链接去发送请求。
Pick
方法:子链接选择负载均衡
问题来了,咱们须要把服务器地址的权重添加进去,可是地址resolver.Address
并无提供权重的属性。官方给的答复是:把权重存储到地址的元数据metadata
中。ide
// attributeKey is the type used as the key to store AddrInfo in the Attributes // field of resolver.Address. type attributeKey struct{} // AddrInfo will be stored inside Address metadata in order to use weighted balancer. type AddrInfo struct { Weight int } // SetAddrInfo returns a copy of addr in which the Attributes field is updated // with addrInfo. func SetAddrInfo(addr resolver.Address, addrInfo AddrInfo) resolver.Address { addr.Attributes = attributes.New() addr.Attributes = addr.Attributes.WithValues(attributeKey{}, addrInfo) return addr } // GetAddrInfo returns the AddrInfo stored in the Attributes fields of addr. func GetAddrInfo(addr resolver.Address) AddrInfo { v := addr.Attributes.Value(attributeKey{}) ai, _ := v.(AddrInfo) return ai }
定义AddrInfo
结构体并添加权重Weight
属性,Set
方法把Weight
存储到resolver.Address
中,Get
方法从resolver.Address
获取Weight
。ui
解决权重存储问题后,接下来咱们实现加权随机法负载均衡策略。code
首先实现V2PickerBuilder
接口,返回子链接选择器。server
func (*rrPickerBuilder) Build(info base.PickerBuildInfo) balancer.V2Picker { grpclog.Infof("weightPicker: newPicker called with info: %v", info) if len(info.ReadySCs) == 0 { return base.NewErrPickerV2(balancer.ErrNoSubConnAvailable) } var scs []balancer.SubConn for subConn, addr := range info.ReadySCs { node := GetAddrInfo(addr.Address) if node.Weight <= 0 { node.Weight = minWeight } else if node.Weight > 5 { node.Weight = maxWeight } for i := 0; i < node.Weight; i++ { scs = append(scs, subConn) } } return &rrPicker{ subConns: scs, } }
加权随机法
中,我使用空间换时间的方式,把权重转成地址个数(例如addr1
的权重是3
,那么添加3
个子链接到切片中;addr2
权重为1
,则添加1
个子链接;选择子链接时候,按子链接切片长度生成随机数,以随机数做为下标就是选中的子链接),避免重复计算权重。考虑到内存占用,权重定义从1
到5
权重。
接下来实现子链接的选择,获取随机数,选择子链接
type rrPicker struct { subConns []balancer.SubConn mu sync.Mutex } func (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) { p.mu.Lock() index := rand.Intn(len(p.subConns)) sc := p.subConns[index] p.mu.Unlock() return balancer.PickResult{SubConn: sc}, nil }
关键代码完成后,咱们把加权随机法负载均衡策略命名为weight
,并注册到gRPC的负载均衡策略中。
// Name is the name of weight balancer. const Name = "weight" // NewBuilder creates a new weight balancer builder. func newBuilder() balancer.Builder { return base.NewBalancerBuilderV2(Name, &rrPickerBuilder{}, base.Config{HealthCheck: false}) } func init() { balancer.Register(newBuilder()) }
完整代码weight.go
最后,咱们只须要在服务端注册服务时候附带权重,而后客户端在服务发现时把权重Set
到resolver.Address
中,最后客户端把负载论衡策略改为weight
就完成了。
//SetServiceList 设置服务地址 func (s *ServiceDiscovery) SetServiceList(key, val string) { s.lock.Lock() defer s.lock.Unlock() //获取服务地址 addr := resolver.Address{Addr: strings.TrimPrefix(key, s.prefix)} //获取服务地址权重 nodeWeight, err := strconv.Atoi(val) if err != nil { //非数字字符默认权重为1 nodeWeight = 1 } //把服务地址权重存储到resolver.Address的元数据中 addr = weight.SetAddrInfo(addr, weight.AddrInfo{Weight: nodeWeight}) s.serverList[key] = addr s.cc.UpdateState(resolver.State{Addresses: s.getServices()}) log.Println("put key :", key, "wieght:", val) }
客户端使用weight
负载均衡策略
func main() { r := etcdv3.NewServiceDiscovery(EtcdEndpoints) resolver.Register(r) // 链接服务器 conn, err := grpc.Dial( fmt.Sprintf("%s:///%s", r.Scheme(), SerName), grpc.WithBalancerName("weight"), grpc.WithInsecure(), ) if err != nil { log.Fatalf("net.Connect err: %v", err) } defer conn.Close()
运行效果:
运行服务1
,权重为1
运行服务2
,权重为4
运行客户端
查看前50次请求在服务1
和服务器2
的负载状况。服务1
分配了9
次请求,服务2
分配了41
次请求,接近权重比值。
断开服务2
,全部请求流向服务1
以权重为4
,重启服务2
,请求以加权随机法流向两个服务器
本篇文章以加权随机法为例,介绍了如何实现gRPC自定义负载均衡策略,以知足咱们的需求。