上篇文章介紹瞭如何實現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自定義負載均衡策略,以知足咱們的需求。