基于gRPC负载均衡与健康检查实现请求只访问到server的leader

前言

工作中有个需求,gRPC Server端只有一个leader对外提供服务,其余follower只用于当leader故障时顶替Leader自动选主,follower不对外提供服务。

如何实现gRPC客户端请求gRPC server端域名时,请求仅落到gRPC server端的Leader?
这里用到了gRPC原生的负载均衡、健康检查,名称解析等功能。下面将分别介绍其原理。

1、相关原理

1.1、gRPC负载均衡

1.1.1、gRPC负载均衡原理

gRPC官网文档关于负载均衡的介绍:https://github.com/grpc/grpc/blob/master/doc/load-balancing.md
gRPC的负载均衡,是以每次调用为基础,而不是以每个连接为基础。这句话可以理解为,即使服务端收到的所有请求都来自同一个客户端,我们仍会将这些请求在gRPC服务端的所有服务器上实现负载均衡。

1.1.2、gRPC客户端负载均衡概述

grpc 客户端提供了API,用户可自己实现负载均衡策略,并通过此API应用到gRPC。

负载均衡策略负载:

  • 从resolver接收 配置更新 即 server端服务器地址列表
  • 对服务器创建subchannels并管理连接
  • 设置总连接状态(通常通过聚合subchannels的连接状态来计算)
  • 为发送到每个channel上的RPC请求,确定使用哪个subchannel发送
  • gRPC提供了许多LB策略,主流的有pick_first(默认的LB策略)、round_robin、grpclb等。此外还有许多其他LB策略来支持xDS,尽管默契他们还不能直接配置。

1.1.3、负载均衡工作流

负载均衡策略是通过命名解析和客户端与服务端连接的gRPC客户端工作流来实现。
在这里插入图片描述

  • 启动时,gRPC客户端会发出对服务端域名的命名解析请求。域名将被解析为IP列表,由gRPC客户端的service config指明采用哪种负载均衡策略,并配置一些属性。
  • gRPC客户端实例化负载均衡策略,并把service config、IP列表等传递给他
  • 负载均衡策略为IP列表上的服务器地址创建一组subchannels。同时他也监听subchannels的连接状态,并决定每个subchannel何时应建立连接。
  • 对于每个RPC请求,负载均衡策略决定使用那个subchannel。

下面将详细讲解grpcLB策略。

1.1.4、gRPC负载均衡策略详解

1.1.4.1、pick_first

pick_first是默认的LB策略,不需要配置。

pick_first从resolver获取IP列表,并按顺序逐个尝试建立连接,直到找到一个可连接的地址。如果IP列表中没有一个可连接成功的,则在连接重试时把chunnel状态设置为TRANSIENT_FAILURE。

如果成功连接到了一个地址,会把chunnel状态设置为READY,之后所有发送到此RPC上的请求都会被发送到这个地址。如果此连接后续断开了,在pick_first策略会把此channel的状态置为IDLE,直到有应用请求前到来,他才会重连。

1.1.4.2、round_robin

round_robin LB策略是通过在service config中选的,他不需要额外任何配置。

round_robin策略从resolver中解析出地址列表,并为每个地址创建一个subchannel,并持续监听每个subchannel的连接状态:一旦某个subchannel连接断开了,round_robin策略会采用connection backoff策略进行自动重连,

round_robin根据subchannels的状态设置channel的链接状态:

  • if 任一个subchannel是READY状态,那么channel的状态就是READY。
  • else if 任一个subchannel的状态为CONNECTING,channel的状态为CONNECTING(CONNECTING表示正在连接)。
  • else if 任一个subchannel的状态为IDEL,channel的状态为IDEL(IDEL表示连接断开)。
  • else if 所有subchannel的状态都为 TRANSIENT_FAILURE(TRANSIENT_FAILURE表示重试后仍连接失败),channel的状态为 TRANSIENT_FAILURE。

当一个subchannel为TRANSIENT_FAILURE状态时,在他连接成功并状态恢复为READY之前,他的状态会被一直认为是TRANSIENT_FAILURE。这里没有从TRANSIENT_FAILURE到CONNECTING的状态转换。

当一个RPC请求发送到channel,round_robin策略会找出所有subchannel列表中处于READY状态的subchannel,为每个RPC请求轮训分配READY的subchannel。

1.1.4.2.1、gRPC Connection Backoff 协议

当客户端与服务端连接失败时,通常不希望立即重试(为了避免对网络或服务端的大量访问),而是采用某种方式的指数backoff。

backoff的参数如下:

  • INITINAL_BACKOFF:第一次失败后等待多久后重试,默认值1秒
  • MULTIPLIER:重试失败后的回退倍数,默认值1.6
  • JITTER:随机backoff数值,默认值0.2
  • MAX_BACKOFF:backoff的上限,默认值120秒
  • MIN_CONNECT_TIMEOUT:连接超时时间,默认值20秒

推荐的backoff算法
以指数方式将连接重试开始时间back off到MAX_BACKOFF,并伴有JITTER:

ConnectWithBackoff() {
  current_backoff = INITIAL_BACKOFF 
  current_deadline = now() + INITIAL_BACKOFF
  while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
         != SUCCESS) { // 只要未成功,就不断重试
    SleepUntil(current_deadline)
    current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
    current_deadline = now() + current_backoff +
      UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)
  }
}
1.1.4.3、grpclb

此grpclb策略目前已被弃用,官方推荐改用xDS(https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md)。

此LB策略最初旨在为gRPC的LB策略提供扩展机制。其目的是,客户端只能实现round_robin等简单算法,而不是直接在客户端添加新的LB策略,对于复杂的LB算法都交由look-aside(后备的)load balancer来提供。

gRPC依赖load balancer提供负载均衡配置和server端地址列表。load balancer会在 ”handler server不可用“ 或 ”健康检查出问题“ 时更新服务器列表,并做出任何复杂决策,并通知到客户端。load balancer可与后端服务器通信以搜集连接负载及健康信息。

当gRPC客户端与load balancer失去联系时,grpclb策略将临时使用resolver返回的地址列表。

grpclb策略通过resolver返回的attribute获取load balancer的地址列表。

xDS主要应用于service mesh中,在mesh中由sidecar连接到xDS server进行数据交互,由sidecar来控制流量的分发。xDS是一种无proxy的客户端负载均衡方案。

2、gRPC健康检查

gRPC health checking官方文档:https://github.com/grpc/grpc/blob/master/doc/health-checking.md

2.1、gRPC服务端健康检查原理

health check用于检测gRPC服务器是否能处理RPC请求。

客户端对服务端Server的健康检查可以点对点进行,也可以通过控制系统(如负载均衡)进行。服务端server在还没有准备好接受请求、正在关闭等情况下返回“不健康”。如果客户端在某时间窗口没有收到健康检查的响应,或者响应中显示“不健康”,客户端可以根据服务端server返回的状态做相应的策略。

一个gRPC service用作简单的client到server和其他控制系统如负载均衡的健康检查机制。使用gRPC service来实现健康检查有如下好处:

  • 他本身是一个gRPC service, 进行健康检查的格式与普通的RPC相同
  • 具有丰富的语义,如per-service健康状态
  • 作为gRPC服务,他能重用所有的现有的计费、配额等基础设置,这样server可以完全控制健康检查service的访问

2.2、gRPC健康检查服务定义

一个gRPC server通过下面的proto定义来生成一个check service:

syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;  // Used only by the Watch method.
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

客户端可以通过调用Check方法(需要设置超时时间)来查询服务器的健康状况,客户端可以通过设置service name来检查对应service的健康状况。

服务器应该手动注册所有service,并单独设置每个service的状态,包括empty service name及其状态。
对于收到的每个请求,如果在注册表中能找到其service name,则一定会回复OK状态的响应,且status field设置为SERVING或NOT_SERVING;如果在注册表中找不到其service name, 服务器会返回NOT_FOUND gRPC status。

server使用空字符串作为server总体健康状态的key, 这样如果客户端对特定服务的健康状态不感兴趣时就可以使用空HealthCheckRequest来查询server的状态。server只可以做service name的精确匹配,而不支持通配符匹配。但service所有者可以自由地实现客户端和服务端都同意的更复杂的匹配语义。

如果健康检查RPC在指定时间内没有完成,客户端可以认为此server不健康,然后执行服务端挂掉的策略。

客户端也可以调用watch方法来执行流式健康检查,server会立即回复当前service状态的消息给客户端;之后每当service状态发生变更,server都会马上给client发送通知。

gRPC服务端将服务的最新状态,更新到一个channel通道里,健康检查服务Watcher会从此channel通道里获取此服务的最新状态,如果发现此最新状态跟之前的状态不一样,服务端会将此最新状态发送给客户端。

3、gRPC 服务发现与name解析

3.1、服务发现简介

通俗解析下服务发现:客户端要跟服务端建立连接,需要知道服务端的IP+Port,但一个服务对应的IP+Port可能会经常变更,所以给服务取个名字,客户端通过此名字跟服务端建立连接,客户端底层使用服务发现系统,解析此名字来获取真正的IP+Port,并在服务端IP+Port发生变更时,重新建立连接。这样的系统成为name-system。

3.2、gRPC Name Resolution

gRPC默认的name-syatem是DNS。
在实际的部署中,可以使用多种name-system,gRPC提供的API很通用,可支持一些列name-system及相应的语法。

gRPC官网name resolution文档:https://github.com/grpc/grpc/blob/master/doc/naming.md#name-syntax

3.2.1、Name语法

gRPC采用的命名语法遵循RFC 3986中定义的RUI语法:

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

例如:
在这里插入图片描述

  • schema:要使用的服务发现解析器,如未指定默认为DNS
  • host:要通过DNS解析的host
  • port:为每个地址返回的端口,如未指定,则使用443(对于insecure channels,默认为80)

3.2.2、Resolver Plugins

gRPC客户端库使用指定schema来选出正确的resolver插件,并对其传递指定的name字符串

resolver能联系到权威机构并返回解析结果给gRPC客户端库,返回的内容包括:

  • 解析出的地址列表(IP和Port),每个地址可以有预期相关的一组属性(key/value对),这些属性可用于负载均衡策略
  • Service Config

Resolver插件的API让resolver持续监听endpoint,并返回变更的解析结果。

2、请求仅落到leader的实现方案

2.1、设置健康检查接口的状态

下面场景下会设置server健康检查的状态:

2.1.1、设置状态为SERVING

1、服务启动时

health.SetServingStatus(``, serving)

2、follower变为leader时
在gRPC server端,如当前节点是follower,他会定时检测leader心跳是否超时,如果是,会发起leader竞选,如果成功当选为leader, 他会把当前节点的健康检查状态设置为SERVING

health.SetServingStatus(serviceName, serving)

2.1.2、设置状态为NOT_SERVING

1、服务启动失败时
服务启动时,会监听依赖服务的地址,如连接失败,则设置服务状态不可用:

health.SetServingStatus(serviceName, notServing)
health.SetServingStatus(``, notServing)

2、服务关闭时

health.SetServingStatus(serviceName, notServing)
health.SetServingStatus(``, notServing)

3、leader变为follower时

  • 如当前节点是leader,他会定时发送leader心跳,如果他假死复活,发现他已不是leader,他会把当前节点的检查检查状态置为NOT_SERVIING
health.SetServingStatus(serviceName, notServing)

2.1.3、查询此server是否健康

func IsServing() bool {
	rep, _ := health.Check(
		context.Background(),
		&grpchealth.HealthCheckRequest{Service: ``})

	return rep.GetStatus() == serving
}

2.1.4、查询此节点是否为leader

func IsLeader() bool {
	rep, _ := health.Check(
		context.Background(),
		&grpchealth.HealthCheckRequest{Service: serviceName})

	return rep.GetStatus() == serving
}

2.2、gRPC客户端如何使用健康检查接口

2.2.1、通过ServiceConfig配置gRPC客户端

serviceConfig := `{
    "loadBalancingPolicy": "round_robin",
    "healthCheckConfig": {
        "serviceName": "xx.v1.ControllerService"
    }
}`
 
options := []grpc.DialOption{
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithDefaultServiceConfig(serviceConfig),
}
address := `dns://域名:80`
 
conn, err := grpc.Dial(address, options...)
if err != nil {
    log.Fatalf("grpc.Dial(%q): %v", address, err)
}
defer conn.Close()

2.2.2、gRPC客户端请求怎样落到server端leader节点

上述配置采用了round_robin负载均衡策略,从上述关于round_robin章节的描写:
“当一个RPC请求发送到channel,round_robin策略会找出所有subchannel列表中处于READY状态的subchannel,为每个RPC请求轮训分配READY的subchannel”
对于我们只有一个leader的场景,只有leader所在节点的状态是READY的,所以gRPC客户端的请求会被分配给leader所在的节点上。

2.2.3、当leader故障会发生什么

如果leader故障了:

1、开始时gRPC客户端访问leader节点会超时,这是round_robin策略会自动重连,但由于leader节点故障了,此时重连失败,gRPC channel状态变为TRANSIENT_FAILURE。

2、follower有定时任务检测到leader心跳超时,会有一个follower自动被选举为leader,此新leader 的service的健康检查状态会变为SERVING,round_robin通过不断重试会跟此节点建立连接成功,此时channel状态变为READY,当客户端有请求来时,会发给此新的leader节点。

3、负载均衡源码分析

3.1、gRPC的平衡器Balancer

gRPC客户端负载均衡实现架构图:
在这里插入图片描述
可以看出,一个Balancer中内置了一个picker模块,Balancer完成下述功能:

  • 与resolver通信,接收resolver通知的server列表更新,维护Connection Pool及每个连接的状态
  • 对于从resolver获取到的server列表,为每个地址建立一个长连接,并监控每个连接的状态,及时更新到Connection Pool。
  • 创建Picker,Picker执行的算法,就是真正的LB逻辑,当客户端访问gRPC提供的方法时,通过Picker选择一个存活的连接返回给客户端,然后调用UpdatePicker更新LB算法的内存状态,为下一次调用做准备

Balancer源码分析:https://github.com/grpc/grpc-go/blob/master/balancer/balancer.go

Balancer的核心代码如下图:
在这里插入图片描述
Balancer通过Register方法进行注册

// Register registers the balancer builder to the balancer map. b.Name
// (lowercased) will be used as the name registered with this builder.  If the
// Builder implements ConfigParser, ParseConfig will be called when new service
// configs are received by the resolver, and the result will be provided to the
// Balancer in UpdateClientConnState.
//
// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe. If multiple Balancers are
// registered with the same name, the one registered last will take effect.
func Register(b Builder) {
	name := strings.ToLower(b.Name())
	if name != b.Name() {
		// TODO: Skip the use of strings.ToLower() to index the map after v1.59
		// is released to switch to case sensitive balancer registry. Also,
		// remove this warning and update the docstrings for Register and Get.
		logger.Warningf("Balancer registered with name %q. grpc-go will be switching to case sensitive balancer registries soon", b.Name())
	}
	m[name] = b
}
  • Register的入参是Builder接口,Builder接口包含方法Build,其入参为ClientConn接口(一个ClientConn表示一个客户端实例与gRPC server之间的连接)。
  • ClientConn接口中的NewSubConn方法入参[]resolver.Address推荐仅传入一个地址,另一个入参为NewSubConnOptions接口
  • NewSubConnOptions接口的HealthCheckEnable属性表示负载均衡创建SubConn是否采用健康检查service状态为SERVING的来建立连接,另一个方法监听SubConnState的变更.
  • ClientConn接口的UpdateState(State)的入参State struct有两个属性,connectivity.State表示IDLE(连接断开后),CONNECTING(正在连接),READY(连接正常),TRANSIENT_FAILURE(连接失败),SHUTDOWN(连接关闭);另一个属性为Picker
  • Picker执行真正的LB算法,为gRPC请求分配一个SubConn

3.2、round_robin源码解读

源码地址:https://github.com/grpc/grpc-go/blob/master/balancer/roundrobin/roundrobin.go

// newBuilder creates a new roundrobin balancer builder.
func newBuilder() balancer.Builder {
	return base.NewBalancerBuilder(Name, &rrPickerBuilder{}, base.Config{HealthCheck: true})
}

func init() {
	balancer.Register(newBuilder())
}

通过调用Register方法来注册一个Balancer,可以看到,round_robin负载均衡启动了健康检查功能。

round_robin在构建连接时,仅使用READY状态的SubConn:

func (*rrPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
	logger.Infof("roundrobinPicker: Build called with info: %v", info)
	if len(info.ReadySCs) == 0 {
		return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
	}
	scs := make([]balancer.SubConn, 0, len(info.ReadySCs))
	for sc := range info.ReadySCs {
		scs = append(scs, sc)
	}
	return &rrPicker{
		subConns: scs,
		// Start at a random index, as the same RR balancer rebuilds a new
		// picker when SubConn states change, and we don't want to apply excess
		// load to the first server in the list.
		next: uint32(grpcrand.Intn(len(scs))),
	}
}

当picker为gRPC请求选择SubConn时,采用轮询算法:

unc (p *rrPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
	subConnsLen := uint32(len(p.subConns))
	nextIndex := atomic.AddUint32(&p.next, 1)

	sc := p.subConns[nextIndex%subConnsLen]
	return balancer.PickResult{SubConn: sc}, nil
}

3.3、pick_first源码解读

https://github.com/grpc/grpc-go/blob/master/pickfirst.go

pick_first也是通过Register方法注册一个Balancer:

func init() {
	balancer.Register(newPickfirstBuilder())
}

pick_first调用demo:
https://github.com/grpc/grpc-go/blob/master/balancer/rls/balancer_test.go

stub.Register(childPolicyName, stub.BalancerFuncs{
		Init: func(bd *stub.BalancerData) {
			bd.Data = balancer.Get(grpc.PickFirstBalancerName).Build(bd.ClientConn, bd.BuildOptions)
		},
		ParseConfig: func(sc json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
			cfg := &childPolicyConfig{}
			if err := json.Unmarshal(sc, cfg); err != nil {
				return nil, err
			}
			return cfg, nil
		},
		UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
			bal := bd.Data.(balancer.Balancer)
			bd.ClientConn.UpdateState(balancer.State{ConnectivityState: connectivity.Idle, Picker: &testutils.TestConstPicker{Err: balancer.ErrNoSubConnAvailable}})
			bd.ClientConn.UpdateState(balancer.State{ConnectivityState: connectivity.Connecting, Picker: &testutils.TestConstPicker{Err: balancer.ErrNoSubConnAvailable}})

			cfg := ccs.BalancerConfig.(*childPolicyConfig)
			return bal.UpdateClientConnState(balancer.ClientConnState{
				ResolverState: resolver.State{Addresses: []resolver.Address{{Addr: cfg.Backend}}},
			})
		},
	})

调用UpdateClientConnState方法来初始化SubConn:
看pickfirst.go的UpdateClientConnState方法来创建SubConn并建立连接:

func (b *pickfirstBalancer) UpdateClientConnState(state balancer.ClientConnState) error {
	...
	var subConn balancer.SubConn
	subConn, err := b.cc.NewSubConn(addrs, balancer.NewSubConnOptions{
		StateListener: func(state balancer.SubConnState) {
			b.updateSubConnState(subConn, state)
		},
	})
	...
	b.subConn = subConn
	b.state = connectivity.Idle
	b.cc.UpdateState(balancer.State{
		ConnectivityState: connectivity.Connecting,
		Picker:            &picker{err: balancer.ErrNoSubConnAvailable},
	})
	b.subConn.Connect()
	return nil
}

可以看到,在balancer.NewSubConnOptions时并没有传递HealthCheckEnabled,即pick_first未基于健康检查service是否可用来对SubConn建立连接。

pick_first的Pick:

type picker struct {
	result balancer.PickResult
	err    error
}

func (p *picker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
	return p.result, p.err
}

// idlePicker is used when the SubConn is IDLE and kicks the SubConn into
// CONNECTING when Pick is called.
type idlePicker struct {
	subConn balancer.SubConn
}

func (i *idlePicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
	i.subConn.Connect()
	return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
}

可以看到,如果gRPC请求来事,如果SubConn为IDLE,即连接断开,则会采用idlePicker的Pick方法重连。

3.4、resolver有新地址怎样通知Balancer的

dns_resolver.watcher --> resolver_wrapper.ClientConn.UpdateState --> clientconn.updateResolverStateAndUnlock --> balancer_wrapper.updateClientConnState --> Balancer.UpdateClientConnState

resolver源码:https://github.com/grpc/grpc-go/blob/master/resolver/resolver.go

在resolover.go中有ClientConn接口:

// ClientConn contains the callbacks for resolver to notify any updates
// to the gRPC ClientConn.
//
// This interface is to be implemented by gRPC. Users should not need a
// brand new implementation of this interface. For the situations like
// testing, the new implementation should embed this interface. This allows
// gRPC to add new methods to this interface.
type ClientConn interface {
	// UpdateState updates the state of the ClientConn appropriately.
	//
	// If an error is returned, the resolver should try to resolve the
	// target again. The resolver should use a backoff timer to prevent
	// overloading the server with requests. If a resolver is certain that
	// reresolving will not change the result, e.g. because it is
	// a watch-based resolver, returned errors can be ignored.
	//
	// If the resolved State is the same as the last reported one, calling
	// UpdateState can be omitted.
	UpdateState(State) error
	// ReportError notifies the ClientConn that the Resolver encountered an
	// error.  The ClientConn will notify the load balancer and begin calling
	// ResolveNow on the Resolver with exponential backoff.
	ReportError(error)
	// NewAddress is called by resolver to notify ClientConn a new list
	// of resolved addresses.
	// The address list should be the complete list of resolved addresses.
	//
	// Deprecated: Use UpdateState instead.
	NewAddress(addresses []Address)
	// ParseServiceConfig parses the provided service config and returns an
	// object that provides the parsed config.
	ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult
}

可以看到,UpdateState方法用于发现有新的state变更时会被调用,他的调用方:
https://github.com/grpc/grpc-go/blob/master/resolver/dns/dns_resolver.go 的 watcher方法:

func (d *dnsResolver) watcher() {
	defer d.wg.Done()
	backoffIndex := 1
	for {
		state, err := d.lookup()
		if err != nil {
			// Report error to the underlying grpc.ClientConn.
			d.cc.ReportError(err)
		} else {
			err = d.cc.UpdateState(*state)
		}

		var waitTime time.Duration
		if err == nil {
			// Success resolving, wait for the next ResolveNow. However, also wait 30
			// seconds at the very least to prevent constantly re-resolving.
			backoffIndex = 1
			waitTime = MinResolutionInterval
			select {
			case <-d.ctx.Done():
				return
			case <-d.rn:
			}
		} else {
			// Poll on an error found in DNS Resolver or an error received from
			// ClientConn.
			waitTime = backoff.DefaultExponential.Backoff(backoffIndex)
			backoffIndex++
		}
		select {
		case <-d.ctx.Done():
			return
		case <-internal.TimeAfterFunc(waitTime):
		}
	}
}

ClientConn接口的UpdateState方法实现详见:
https://github.com/grpc/grpc-go/blob/master/resolver_wrapper.go:

// UpdateState is called by resolver implementations to report new state to gRPC
// which includes addresses and service config.
func (ccr *ccResolverWrapper) UpdateState(s resolver.State) error {
	ccr.cc.mu.Lock()
	ccr.mu.Lock()
	if ccr.closed {
		ccr.mu.Unlock()
		ccr.cc.mu.Unlock()
		return nil
	}
	if s.Endpoints == nil {
		s.Endpoints = make([]resolver.Endpoint, 0, len(s.Addresses))
		for _, a := range s.Addresses {
			ep := resolver.Endpoint{Addresses: []resolver.Address{a}, Attributes: a.BalancerAttributes}
			ep.Addresses[0].BalancerAttributes = nil
			s.Endpoints = append(s.Endpoints, ep)
		}
	}
	ccr.addChannelzTraceEvent(s)
	ccr.curState = s
	ccr.mu.Unlock()
	return ccr.cc.updateResolverStateAndUnlock(s, nil)
}

调用了https://github.com/grpc/grpc-go/blob/master/clientconn.go的updateResolverStateAndUnlock方法:

func (cc *ClientConn) updateResolverStateAndUnlock(s resolver.State, err error) error {
	defer cc.firstResolveEvent.Fire()
	// Check if the ClientConn is already closed. Some fields (e.g.
	// balancerWrapper) are set to nil when closing the ClientConn, and could
	// cause nil pointer panic if we don't have this check.
	if cc.conns == nil {
		cc.mu.Unlock()
		return nil
	}

	if err != nil {
		// May need to apply the initial service config in case the resolver
		// doesn't support service configs, or doesn't provide a service config
		// with the new addresses.
		cc.maybeApplyDefaultServiceConfig()

		cc.balancerWrapper.resolverError(err)

		// No addresses are valid with err set; return early.
		cc.mu.Unlock()
		return balancer.ErrBadResolverState
	}

	var ret error
	if cc.dopts.disableServiceConfig {
		channelz.Infof(logger, cc.channelz, "ignoring service config from resolver (%v) and applying the default because service config is disabled", s.ServiceConfig)
		cc.maybeApplyDefaultServiceConfig()
	} else if s.ServiceConfig == nil {
		cc.maybeApplyDefaultServiceConfig()
		// TODO: do we need to apply a failing LB policy if there is no
		// default, per the error handling design?
	} else {
		if sc, ok := s.ServiceConfig.Config.(*ServiceConfig); s.ServiceConfig.Err == nil && ok {
			configSelector := iresolver.GetConfigSelector(s)
			if configSelector != nil {
				if len(s.ServiceConfig.Config.(*ServiceConfig).Methods) != 0 {
					channelz.Infof(logger, cc.channelz, "method configs in service config will be ignored due to presence of config selector")
				}
			} else {
				configSelector = &defaultConfigSelector{sc}
			}
			cc.applyServiceConfigAndBalancer(sc, configSelector)
		} else {
			ret = balancer.ErrBadResolverState
			if cc.sc == nil {
				// Apply the failing LB only if we haven't received valid service config
				// from the name resolver in the past.
				cc.applyFailingLBLocked(s.ServiceConfig)
				cc.mu.Unlock()
				return ret
			}
		}
	}

	var balCfg serviceconfig.LoadBalancingConfig
	if cc.sc != nil && cc.sc.lbConfig != nil {
		balCfg = cc.sc.lbConfig
	}
	bw := cc.balancerWrapper
	cc.mu.Unlock()

	uccsErr := bw.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg})
	if ret == nil {
		ret = uccsErr // prefer ErrBadResolver state since any other error is
		// currently meaningless to the caller.
	}
	return ret
}

https://github.com/grpc/grpc-go/blob/master/balancer_wrapper.go
的updateClientConnState方法:

// updateClientConnState is invoked by grpc to push a ClientConnState update to
// the underlying balancer.  This is always executed from the serializer, so
// it is safe to call into the balancer here.
func (ccb *ccBalancerWrapper) updateClientConnState(ccs *balancer.ClientConnState) error {
	errCh := make(chan error)
	ok := ccb.serializer.Schedule(func(ctx context.Context) {
		defer close(errCh)
		if ctx.Err() != nil || ccb.balancer == nil {
			return
		}
		name := gracefulswitch.ChildName(ccs.BalancerConfig)
		if ccb.curBalancerName != name {
			ccb.curBalancerName = name
			channelz.Infof(logger, ccb.cc.channelz, "Channel switches to new LB policy %q", name)
		}
		err := ccb.balancer.UpdateClientConnState(*ccs)
		if logger.V(2) && err != nil {
			logger.Infof("error from balancer.UpdateClientConnState: %v", err)
		}
		errCh <- err
	})
	if !ok {
		return nil
	}
	return <-errCh
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值