gRPC負載均衡(自定義負載均衡策略)
前言
上篇文章介紹了如何實現gRPC負載均衡,但目前官方只提供了pick_first
和round_robin
兩種負載均衡策略,輪詢法round_robin
不能滿足因伺服器配置不同而承擔不同負載量,這篇文章將介紹如何實現自定義負載均衡策略–加權隨機法
。
加權隨機法
可以根據伺服器的處理能力而分配不同的權重,從而實現處理能力高的伺服器可承擔更多的請求,處理能力低的伺服器少承擔請求。
自定義負載均衡策略
gRPC提供了V2PickerBuilder
和V2Picker
介面讓我們實現自己的負載均衡策略。
type V2PickerBuilder interface {
Build(info PickerBuildInfo) balancer.V2Picker
}
V2PickerBuilder
介面:創建V2版本的子連接選擇器。
Build
方法:返回一個V2選擇器,將用於gRPC選擇子連接。
type V2Picker interface {
Pick(info PickInfo) (PickResult, error)
}
V2Picker
介面:用於gRPC選擇子連接去發送請求。
Pick
方法:子連接選擇
問題來了,我們需要把伺服器地址的權重添加進去,但是地址resolver.Address
並沒有提供權重的屬性。官方給的答覆是:把權重存儲到地址的元數據metadata
中。
// 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
介面,返回子連接選擇器。
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自定義負載均衡策略,以滿足我們的需求。
源碼地址://github.com/Bingjian-Zhu/etcd-example