1. 程式人生 > >gRPC負載均衡(自定義負載均衡策略)

gRPC負載均衡(自定義負載均衡策略)

### 前言 上篇文章介紹瞭如何實現gRPC負載均衡,但目前官方只提供了`pick_first`和`round_robin`兩種負載均衡策略,輪詢法`round_robin`不能滿足因伺服器配置不同而承擔不同負載量,這篇文章將介紹如何實現自定義負載均衡策略--`加權隨機法`。 `加權隨機法`可以根據伺服器的處理能力而分配不同的權重,從而實現處理能力高的伺服器可承擔更多的請求,處理能力低的伺服器少承擔請求。 ### 自定義負載均衡策略 gRPC提供了`V2PickerBuilder`和`V2Picker`介面讓我們實現自己的負載均衡策略。 ```go type V2PickerBuilder interface { Build(info PickerBuildInfo) balancer.V2Picker } ``` `V2PickerBuilder`介面:建立V2版本的子連線選擇器。 `Build`方法:返回一個V2選擇器,將用於gRPC選擇子連線。 ```go type V2Picker interface { Pick(info PickInfo) (PickResult, error) } ``` `V2Picker `介面:用於gRPC選擇子連線去傳送請求。 `Pick`方法:子連線選擇 問題來了,我們需要把伺服器地址的權重新增進去,但是地址`resolver.Address`並沒有提供權重的屬性。官方給的答覆是:把權重儲存到地址的元資料`metadata`中。 ```go // 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`。 解決權重儲存問題後,接下來我們實現加權隨機法負載均衡策略。 首先實現`V2PickerBuilder`介面,返回子連線選擇器。 ```go 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`權重。 接下來實現子連線的選擇,獲取隨機數,選擇子連線 ```go 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的負載均衡策略中。 ```go // 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](https://github.com/Bingjian-Zhu/etcd-example/blob/master/5-etcd-grpclb-balancer/balancer/weight/weight.go) 最後,我們只需要在服務端註冊服務時候附帶權重,然後客戶端在服務發現時把權重`Set`到`resolver.Address`中,最後客戶端把負載論衡策略改成`weight`就完成了。 ```go //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`負載均衡策略 ```go 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` ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520162934052-74794177.png) 執行`服務2`,權重為`4` ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520162941378-1116335906.png) 執行客戶端 ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520163515073-1148862720.png) 檢視前50次請求在`服務1`和`伺服器2`的負載情況。`服務1`分配了`9`次請求,`服務2`分配了`41`次請求,接近權重比值。 ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520163753358-1654741743.png) ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520163932810-2034341622.png) 斷開`服務2`,所有請求流向`服務1` ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520164432399-923288256.png) 以權重為`4`,重啟`服務2`,請求以加權隨機法流向兩個伺服器 ![](https://img2020.cnblogs.com/blog/1508611/202005/1508611-20200520164648568-1117742551.png) ### 總結 本篇文章以加權隨機法為例,介紹瞭如何實現gRPC自定義負載均衡策略,以滿足我們的需求。 原始碼地址:https://github.com/Bingjian-Zhu/etcd