实现自定义 gRPC Resolver 遇到的问题
package main
import (
"fmt"
"strconv"
)
type Bar struct {
Num int
}
func (b *Bar) String() string {
return strconv.Itoa(b.Num)
}
type Foo struct {
Num int
B *Bar
}
func main() {
m := map[Foo]int{}
f0 := Foo{Num: 1, B: &Bar{Num: 2}}
m[f0] = 0
f1 := Foo{Num: 1, B: &Bar{Num: 2}}
m[f1] = 1
fmt.Println(m)
}
// output:
// map[{1 2}:0 {1 2}:1]
正式开始,我们知道 gRPC 支持自定义 Resolver 来实现自定义的服务发现机制,自定义 Balancer 来实现自定义的负载均衡策略。
假如我们要实现一个基于权重的负载均衡策略,通常这么做:
type Address struct {
// Addr is the server address on which a connection will be established.
Addr string
// Attributes contains arbitrary data about this address intended for
// consumption by the load balancing policy.
Attributes *attributes.Attributes
// 省略了其他不重要的字段
}
其中,Attributes 字段可以用来保存负载均衡策略所使用的信息,比如权重信息。
再实现一个 Balancer 来根据 Resolver 提供的实例权重来做负载均衡。
gRPC 提供了一个 baseBalancer 封装了一些通用的更新连接状态的逻辑,同时抽象了一个 Picker 的接口,可以简单理解为它是用来定义如何从 Resolver 提供的服务实例中选择一个可用实例。所以,通常如果没有特殊需求的话,我们只需要实现一个 Picker 就可以,将实现的 Picker 和 baseBalancer 组合就可以生成一个新的 Balancer。而这次遇到的问题就是出在这个 baseBalancer 和 Address 定义上。
在 gRPC v1.35.0 之前的版本,baseBalancer 对连接的管理是这样实现的:
func (b *baseBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
// 省略部分代码...
// addrsSet is the set converted from addrs, it's used for quick lookup of an address.
addrsSet := make(map[resolver.Address]struct{})
for _, a := range s.ResolverState.Addresses {
addrsSet[a] = struct{}{}
if _, ok := b.subConns[a]; !ok {
// a is a new address (not existing in b.subConns).
sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: b.config.HealthCheck})
if err != nil {
logger.Warningf("base.baseBalancer: failed to create new SubConn: %v", err)
continue
}
b.subConns[a] = sc
b.scStates[sc] = connectivity.Idle
sc.Connect()
}
}
for a, sc := range b.subConns {
// a was removed by resolver.
if _, ok := addrsSet[a]; !ok {
b.cc.RemoveSubConn(sc)
delete(b.subConns, a)
// Keep the state of this sc in b.scStates until sc's state becomes Shutdown.
// The entry will be deleted in UpdateSubConnState.
}
}
// 省略一些不重要的代码...
return nil
}
对于这个问题,有两个解决方案:
自己实现一个 Balancer,把 resolver.Address.Addr 作为 map 的 Key,权重不参与比较,可以单独保存;
考虑到方法2实现上相对简单,所以我们采用了方案 2。
但是当我们把 gRPC 升级到 v1.35.0 及以上的版本时,发现服务实例的权重信息丢失了。查看v1.35.0 的 Release Notes 时找到了相关的一个 pr #4024 (https://github.com/grpc/grpc-go/pull/4024),修复了上面说到的问题,在使用 resolver.Address 作为 baseBalancer.subConns map 的 key 之前,先去掉了 Attributes 字段(设置为 nil)。但是,我们实现的 Picker 是通过 baseBalancer.subConns 的 key 来获取实现权重,然后实现按权重选择实例的。但是目前 Attributes 被去掉了,导致权重丢失。只能回到方案 1 的解决办法上了。
如果你也遇到了,希望能有帮助。