基于v1.64.0;

注:详细的传递等交叉部分的逻辑 及部分负载均衡处理的细节,请见 resolver

负载均衡

  负载均衡的粒度是请求级别,流量管理更灵活;

请求流程

基础实现流程

  1. 构建注册中心,提供服务发现(如 etcd、ZK 等);
  2. grpc 服务端注册后,服务与命名服务建立连接,并注册自身相关元数据(ip+port…),并开启 goroutine 续约;
  3. 将命名服务注册给 resolver(如 etcd 客户端),获得本地解析器,注册 grpc 客户端时,将服务发现的地址和解析器和负载均衡策略等 opts 传入(局部有效),客户端和所有在线服务建立 HTTP2.0 长连接;
  4. 发起 RPC 调用时,根据负载均衡器的策略(默认为pick_first,即选第一个能用的),选择其中一个 HTTP2.0 长连接与该服务端通信;

balancer.Balancer(负载均衡器) 的职责:

  1. 接收 Resolver 传递过来的服务节点更新信息(通过 ClientConn),维护连接和对应连接池的状态;
  2. 内含选择器用于实现真正的负载均衡逻辑,选择器的 warpper 会通过 updatePicker 方法更新 picker的状态;

  连接为 HTTP2.0 的长连接,每次 RPC 调用都直接复用该连接;

  当一个服务挂掉后,且客户端可通过重试连接另一个服务节点;

  负载均衡器的实现负责维护和管理连接池。决定了客户端使用哪些 SubConn 与服务实例通信,以及如何分配请求到这些 SubConn 上;

自定义实现的思路

详细请见 implement

  如果仅自定义简单的负载均衡策略,只需实现 PickerBuilderPicker 接口即可:

  PickerBuilder 接口用于创建 Picker,每次 balancer 收到服务节点的变化通知时,最后都需要重新 build 出一个新 picker,或者说以此更新选择器的负载均衡策略;

  Picker 接口则负责提供服务发现功能,就单纯实现负载均衡相关逻辑即可,比如最简单的就是把获取到的第一个节点返回给 ClientConn 以发起 RPC 请求;

  如果涉及较为复杂的负载均衡策略,或者说核心是想要自己管理连接、连接池并管理相关 UpdateState 的逻辑时,就需要再向上将 balancer.Balancer 和 balancer.Builder 接口自定义实现出来,涉及维护的元数据较多,暂不展开讨论;

连接相关

type acBalancerWrapper struct {
	ac            *addrConn          // read-only
	ccb           *ccBalancerWrapper // read-only
	stateListener func(balancer.SubConnState)
    ...
}


type SubConnState struct {
    ConnectivityState connectivity.State 
    ConnectionError error
}

ClientConn 的状态转换:

  • Idle -> Connecting -> Ready:客户端启动后,在第一次发起 RPC 请求前处于 Idle,然后尝试连接服务端进入 Connecting 状态,连接成功后进入 Ready 状态;
  • Ready -> TransientFailure -> Connecting → Ready:连接中途断开时,客户端会进入 TransientFailure 状态,接着自动重试连接进入 Connecting,最终重新连接成功进入 Ready;
  • Ready -> Shutdown:当客户端调用 Close() 时,连接被关闭,状态变为 Shutdown;
  • idle -> connecting -> TransientFailure -> shutdown:正常连接尝试重试(发现服务终止);

  当 SubConn 状态发生变化时,addrConn 会调用 updateConnectivityState 方法更新状态,并通过 acBalancerWrapper 的回调函数 stateListener,传递给 baseBalancer(会更新 picker),首先会尝试重连,如果服务挂了,则最后会转变为 shutdown 状态;

balancer 相关核心接口

// SubConn 表示到gRPC后端服务的单个连接
// 
// 所有 SubConn 都从 IDLE(空闲状态)开始,不会主动连接后端服务,
// 只有当负载均衡器调用 Connect 时,才会尝试连接;
// 
// 如果 SubConn 断开连接,会进入IDLE,需要负载均衡器再次 Connect;
type SubConn interface {
    // 弃用,直接创建新的 SubConn 实例,而不是更新现有连接的地址;
	UpdateAddresses([]resolver.Address)
    
	Connect()

        // 生产者指客户端发送消息的行为?
	GetOrBuildProducer(ProducerBuilder) (p Producer, close func())

        // 不接受新请求,完成当前rpc后关闭
	Shutdown()
}

// 创建负载均衡器
type Builder interface {
	// 通过客户端连接创建新负载均衡器
	Build(cc ClientConn, opts BuildOptions) Balancer

        // 返回负载均衡器的名字,用于客户端选择LB策略
	Name() string
}

type Picker interface {
    // 为请求选择一个连接,及负载均衡策略实现的地方
    // 不可阻塞
    // 返回错误的情况:
    // 		ErrNoSubConnAvailable:grpc阻塞直到获得新连接处理请求;
    //	
    //		如果返回 grpc/status 包的状态错误,则以此终止该rpc
    // 
    //		其他错误,等待就绪的rpc会阻塞等待,非等待则会返回错误的string和Unavailable
    //		注:WithWaitForReady(true)即等待就绪
	Pick(info PickInfo) (PickResult, error)
}

// 接收从 Resolver 发送的服务端列表,建立并维护(长)连接(SubConn)状态
// 当 client 发起 RPC 调用时,按照算法从连接池取出一个连接用于 RPC 调用
type Balancer interface {
    // 动态感知服务地址或配置(负载均衡策略切换)的变化,可触发服务发现器的重试;
	UpdateClientConnState(ClientConnState) error
	// 回调通知,grpc解析服务地址时发生了错误
	ResolverError(error)
    
	UpdateSubConnState(SubConn, SubConnState)

        // 考虑关闭所有子连接,目前不强制要求全关闭
	Close()
}

type ClientConn interface {
        // 在负载均衡器的实现中使用,创建新连接
        // 未来一个SubConn只对应一个address
	NewSubConn([]resolver.Address, NewSubConnOptions) (SubConn, error)
    
	RemoveSubConn(SubConn) // 弃用,应该用SubConn.Shutdown关闭连接
	UpdateAddresses(SubConn, []resolver.Address) //弃用,应该创建而不是更新

        // 负载均衡器的状态被更新了,grpc会更新 ClientConn 的连接状态
        // 并调用新 Picker 的 Pick 方法获取新 SubConns
	UpdateState(State)

        // 负载均衡器发现状态变化后,通知grpc进行服务发现
	ResolveNow(resolver.ResolveNowOptions)

	Target() string // 弃用
        // 使用 BuildOptions 中的 Target 字段获取后端服务(逻辑标识或者说服务名)位置
}

serviceconfig:

// 用于表示负载均衡 配置 的数据结构;
// 为不同类型的负载均衡 配置 提供一个通用的基类;
// 不透明
type LoadBalancingConfig interface {
	isLoadBalancingConfig()
}