Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
473 lines
18 KiB
Go
473 lines
18 KiB
Go
/*
|
||
* Copyright 2019 gRPC authors.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
// Package cdsbalancer implements a balancer to handle CDS responses.
|
||
package cdsbalancer
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"google.golang.org/grpc/attributes"
|
||
"google.golang.org/grpc/balancer"
|
||
"google.golang.org/grpc/balancer/base"
|
||
"google.golang.org/grpc/connectivity"
|
||
"google.golang.org/grpc/internal/balancer/nop"
|
||
"google.golang.org/grpc/internal/grpclog"
|
||
"google.golang.org/grpc/internal/pretty"
|
||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
|
||
"google.golang.org/grpc/internal/xds/balancer/priority"
|
||
"google.golang.org/grpc/internal/xds/xdsclient"
|
||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||
"google.golang.org/grpc/internal/xds/xdsdepmgr"
|
||
"google.golang.org/grpc/resolver"
|
||
"google.golang.org/grpc/serviceconfig"
|
||
)
|
||
|
||
const cdsName = "cds_experimental"
|
||
|
||
var (
|
||
// newChildBalancer is a helper function to build a new priority balancer
|
||
// and will be overridden in unittests.
|
||
newChildBalancer = func(cc balancer.ClientConn, opts balancer.BuildOptions) (balancer.Balancer, error) {
|
||
builder := balancer.Get(priority.Name)
|
||
if builder == nil {
|
||
return nil, fmt.Errorf("xds: no balancer builder with name %v", priority.Name)
|
||
}
|
||
// We directly pass the parent clientConn to the underlying priority
|
||
// balancer because the cdsBalancer does not deal with subConns.
|
||
return builder.Build(cc, opts), nil
|
||
}
|
||
)
|
||
|
||
func init() {
|
||
balancer.Register(bb{})
|
||
}
|
||
|
||
// bb implements the balancer.Builder interface to help build a cdsBalancer.
|
||
// It also implements the balancer.ConfigParser interface to help parse the
|
||
// JSON service config, to be passed to the cdsBalancer.
|
||
type bb struct{}
|
||
|
||
// Build creates a new CDS balancer with the ClientConn.
|
||
func (bb) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||
builder := balancer.Get(priority.Name)
|
||
if builder == nil {
|
||
// Shouldn't happen, registered through imported Priority builder. Still,
|
||
// defensive programming.
|
||
logger.Errorf("%q LB policy is needed but not registered", priority.Name)
|
||
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy is needed but not registered", priority.Name))
|
||
}
|
||
parser, ok := builder.(balancer.ConfigParser)
|
||
if !ok {
|
||
// Shouldn't happen, imported Priority builder has this method.
|
||
logger.Errorf("%q LB policy does not implement a config parser", priority.Name)
|
||
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy does not implement a config parser", priority.Name))
|
||
}
|
||
|
||
b := &cdsBalancer{
|
||
bOpts: opts,
|
||
childConfigParser: parser,
|
||
clusterConfigs: make(map[string]*xdsresource.ClusterResult),
|
||
priorityConfigs: make(map[string]*priorityConfig),
|
||
cc: cc,
|
||
}
|
||
b.logger = prefixLogger(b)
|
||
b.logger.Infof("Created")
|
||
return b
|
||
}
|
||
|
||
// Name returns the name of balancers built by this builder.
|
||
func (bb) Name() string {
|
||
return cdsName
|
||
}
|
||
|
||
// lbConfig represents the loadBalancingConfig section of the service config
|
||
// for the cdsBalancer.
|
||
type lbConfig struct {
|
||
serviceconfig.LoadBalancingConfig
|
||
ClusterName string `json:"cluster"`
|
||
IsDynamic bool `json:"isDynamic"`
|
||
}
|
||
|
||
// ParseConfig parses the JSON load balancer config provided into an
|
||
// internal form or returns an error if the config is invalid.
|
||
func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||
var cfg lbConfig
|
||
if err := json.Unmarshal(c, &cfg); err != nil {
|
||
return nil, fmt.Errorf("xds: unable to unmarshal lbconfig: %s, error: %v", string(c), err)
|
||
}
|
||
return &cfg, nil
|
||
}
|
||
|
||
// cdsBalancer implements a CDS based LB policy. It instantiates a
|
||
// cluster_resolver balancer to further resolve the serviceName received from
|
||
// CDS, into localities and endpoints. Implements the balancer.Balancer
|
||
// interface which is exposed to gRPC and implements the balancer.ClientConn
|
||
// interface which is exposed to the cluster_resolver balancer.
|
||
type cdsBalancer struct {
|
||
// The following fields are initialized at build time and are either
|
||
// read-only after that or provide their own synchronization, and therefore
|
||
// do not need to be guarded by a mutex.
|
||
cc balancer.ClientConn // ClientConn interface passed to child LB.
|
||
bOpts balancer.BuildOptions // BuildOptions passed to child LB.
|
||
childConfigParser balancer.ConfigParser // Config parser for cluster_resolver LB policy.
|
||
logger *grpclog.PrefixLogger // Prefix logger for all logging.
|
||
|
||
// All fields below are accessed only from methods implementing the
|
||
// balancer.Balancer interface. Since gRPC guarantees that these methods are
|
||
// never invoked concurrently, no additional synchronization is required to
|
||
// protect access to these fields.
|
||
xdsClient xdsclient.XDSClient
|
||
childLB balancer.Balancer // Child policy, built upon resolution of the cluster graph.
|
||
clusterConfigs map[string]*xdsresource.ClusterResult // Cluster name to the last received result for that cluster.
|
||
priorityConfigs map[string]*priorityConfig // Hostname to priority config for that leaf cluster.
|
||
lbCfg *lbConfig // Current load balancing configuration.
|
||
priorities []*priorityConfig // List of priorities in the order.
|
||
unsubscribe func() // For dynamic cluster unsubscription.
|
||
isSubscribed bool // True if a dynamic cluster has been subscribed to.
|
||
clusterSubscriber xdsdepmgr.ClusterSubscriber // To subscribe to dynamic cluster resource.
|
||
xdsLBPolicy internalserviceconfig.BalancerConfig // Stores the locality and endpoint picking policy.
|
||
attributes *attributes.Attributes // Attributes from resolver state.
|
||
serviceConfig *serviceconfig.ParseResult
|
||
// Each new leaf cluster needs a child name generator to reuse child policy
|
||
// names. But to make sure the names across leaf clusters doesn't conflict,
|
||
// we need a seq ID. This ID is incremented for each new cluster.
|
||
childNameGeneratorSeqID uint64
|
||
}
|
||
|
||
// UpdateClientConnState receives the serviceConfig, xdsConfig,
|
||
// ClusterSubscriber and the xdsClient object from the xdsResolver. If an error
|
||
// is encountered, the parent (clustermanager) sets the corresponding cluster’s
|
||
// picker to transient_failure. Otherwise, the received configuration is
|
||
// processed and forwarded to the appropriate child policy.
|
||
func (b *cdsBalancer) UpdateClientConnState(state balancer.ClientConnState) error {
|
||
if b.xdsClient == nil {
|
||
c := xdsclient.FromResolverState(state.ResolverState)
|
||
if c == nil {
|
||
b.logger.Warningf("Received balancer config with no xDS client")
|
||
return balancer.ErrBadResolverState
|
||
}
|
||
b.xdsClient = c
|
||
}
|
||
b.logger.Infof("Received balancer config update: %s", pretty.ToJSON(state.BalancerConfig))
|
||
|
||
xdsConfig := xdsresource.XDSConfigFromResolverState(state.ResolverState)
|
||
if xdsConfig == nil {
|
||
b.logger.Warningf("Received balancer config with no xDS config")
|
||
return balancer.ErrBadResolverState
|
||
}
|
||
b.clusterConfigs = xdsConfig.Clusters
|
||
b.clusterSubscriber = xdsdepmgr.XDSClusterSubscriberFromResolverState(state.ResolverState)
|
||
if b.clusterSubscriber == nil {
|
||
b.logger.Warningf("Received balancer config with no cluster subscriber")
|
||
return balancer.ErrBadResolverState
|
||
}
|
||
// The errors checked here should ideally never happen because the
|
||
// ServiceConfig in this case is prepared by the xdsResolver and is not
|
||
// something that is received on the wire.
|
||
lbCfg, ok := state.BalancerConfig.(*lbConfig)
|
||
if !ok {
|
||
b.logger.Warningf("Received unexpected balancer config type: %T", state.BalancerConfig)
|
||
return balancer.ErrBadResolverState
|
||
}
|
||
if lbCfg.ClusterName == "" {
|
||
b.logger.Warningf("Received balancer config with no cluster name")
|
||
return balancer.ErrBadResolverState
|
||
}
|
||
|
||
b.lbCfg = lbCfg
|
||
b.serviceConfig = state.ResolverState.ServiceConfig
|
||
b.attributes = state.ResolverState.Attributes
|
||
return b.handleXDSConfigUpdate()
|
||
}
|
||
|
||
// handleXDSConfigUpdate processes the XDSConfig update from the xDS resolver.
|
||
func (b *cdsBalancer) handleXDSConfigUpdate() error {
|
||
clusterName := b.lbCfg.ClusterName
|
||
|
||
// If the cluster is dynamic and we dont have a subscription yet, create
|
||
// one.
|
||
if b.lbCfg.IsDynamic && !b.isSubscribed {
|
||
b.unsubscribe = b.clusterSubscriber.SubscribeToCluster(clusterName)
|
||
b.isSubscribed = true
|
||
return nil
|
||
}
|
||
|
||
clusterUpdate, ok := b.clusterConfigs[clusterName]
|
||
if !ok {
|
||
// If the cluster is missing from the config, check if it is dynamic.
|
||
// For dynamic clusters, the xDS config may be updated before the
|
||
// corresponding cluster resource is received. This should never occur
|
||
// for static clusters.
|
||
if b.lbCfg.IsDynamic {
|
||
return nil
|
||
}
|
||
return b.annotateErrorWithNodeID(fmt.Errorf("did not find the cluster %q in XDSConfig", clusterName))
|
||
}
|
||
// If the cluster resource has an error, return the error.
|
||
if clusterUpdate.Err != nil {
|
||
return clusterUpdate.Err
|
||
}
|
||
return b.handleClusterUpdate()
|
||
}
|
||
|
||
// handleClusterUpdate handles a good XDSConfig update from the xDS resolver.
|
||
// Builds the child policy config and pushes it down.
|
||
func (b *cdsBalancer) handleClusterUpdate() error {
|
||
clusterName := b.lbCfg.ClusterName
|
||
clusterConfig := b.clusterConfigs[clusterName].Config
|
||
|
||
var newPriorities []*priorityConfig
|
||
switch clusterConfig.Cluster.ClusterType {
|
||
case xdsresource.ClusterTypeEDS, xdsresource.ClusterTypeLogicalDNS:
|
||
p := b.updatePriorityConfig(clusterName, &clusterConfig)
|
||
newPriorities = append(newPriorities, p)
|
||
case xdsresource.ClusterTypeAggregate:
|
||
for _, leaf := range clusterConfig.AggregateConfig.LeafClusters {
|
||
leafCluster := b.clusterConfigs[leaf]
|
||
// Update priority config for leaf clusters.
|
||
p := b.updatePriorityConfig(leaf, &leafCluster.Config)
|
||
newPriorities = append(newPriorities, p)
|
||
}
|
||
}
|
||
b.priorities = newPriorities
|
||
|
||
if err := b.updateOutlierDetection(); err != nil {
|
||
return b.annotateErrorWithNodeID(fmt.Errorf("failed to correctly update Outlier Detection config %v", err))
|
||
}
|
||
|
||
// The LB policy is configured by the root cluster.
|
||
if err := json.Unmarshal(clusterConfig.Cluster.LBPolicy, &b.xdsLBPolicy); err != nil {
|
||
return b.annotateErrorWithNodeID(fmt.Errorf("error unmarshalling xDS LB Policy: %v", err))
|
||
}
|
||
if err := b.updateChildConfig(); err != nil {
|
||
return b.annotateErrorWithNodeID(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// updateChildConfig builds child policy configuration using endpoint addresses
|
||
// returned from the XDSConfig and child policy configuration.
|
||
//
|
||
// A child policy is created if one doesn't already exist. The newly built
|
||
// configuration is then pushed to the child policy.
|
||
func (b *cdsBalancer) updateChildConfig() error {
|
||
if b.childLB == nil {
|
||
childLB, err := newChildBalancer(b.cc, b.bOpts)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create child policy of type %s: %v", priority.Name, err)
|
||
}
|
||
b.childLB = childLB
|
||
}
|
||
|
||
childCfgBytes, endpoints, err := buildPriorityConfigJSON(b.priorities, &b.xdsLBPolicy)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to build child policy config: %v", err)
|
||
}
|
||
childCfg, err := b.childConfigParser.ParseConfig(childCfgBytes)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse child policy config. This should never happen because the config was generated: %v", err)
|
||
}
|
||
if b.logger.V(2) {
|
||
b.logger.Infof("Built child policy config: %s", pretty.ToJSON(childCfg))
|
||
}
|
||
|
||
for i := range endpoints {
|
||
for j := range endpoints[i].Addresses {
|
||
addr := endpoints[i].Addresses[j]
|
||
addr.BalancerAttributes = endpoints[i].Attributes
|
||
// BalancerAttributes are used for the following:
|
||
// * Authority Override.
|
||
// * grpc.lb.backend_service metric label propagation.
|
||
// See https://github.com/grpc/grpc-go/issues/6472
|
||
endpoints[i].Addresses[j] = addr
|
||
}
|
||
}
|
||
if err := b.childLB.UpdateClientConnState(balancer.ClientConnState{
|
||
ResolverState: resolver.State{
|
||
Endpoints: endpoints,
|
||
ServiceConfig: b.serviceConfig,
|
||
Attributes: b.attributes,
|
||
},
|
||
BalancerConfig: childCfg,
|
||
}); err != nil {
|
||
return fmt.Errorf("failed to push config to child policy: %v", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// updatePriorityConfig updates the priority configuration for the specified EDS
|
||
// or DNS cluster, creating it if it does not already exist.
|
||
func (b *cdsBalancer) updatePriorityConfig(clusterName string, clusterConfig *xdsresource.ClusterConfig) *priorityConfig {
|
||
name := hostName(clusterName, *clusterConfig.Cluster)
|
||
pc, ok := b.priorityConfigs[name]
|
||
if !ok {
|
||
pc = &priorityConfig{
|
||
childNameGen: newNameGenerator(b.childNameGeneratorSeqID),
|
||
}
|
||
b.priorityConfigs[name] = pc
|
||
// Increment the seq ID for the next new cluster. This is done to make
|
||
// sure that the child policy names generated for different clusters
|
||
// don't conflict with each other.
|
||
b.childNameGeneratorSeqID++
|
||
}
|
||
pc.clusterConfig = clusterConfig
|
||
return pc
|
||
}
|
||
|
||
// updateOutlierDetection updates Outlier Detection config for all priorities.
|
||
func (b *cdsBalancer) updateOutlierDetection() error {
|
||
odBuilder := balancer.Get(outlierdetection.Name)
|
||
if odBuilder == nil {
|
||
// Shouldn't happen, registered through imported Outlier Detection,
|
||
// defensive programming.
|
||
return fmt.Errorf("%q LB policy is needed but not registered", outlierdetection.Name)
|
||
}
|
||
|
||
odParser, ok := odBuilder.(balancer.ConfigParser)
|
||
if !ok {
|
||
// Shouldn't happen, imported Outlier Detection builder has this method.
|
||
return fmt.Errorf("%q LB policy does not implement a config parser", outlierdetection.Name)
|
||
}
|
||
|
||
for _, p := range b.priorities {
|
||
// Update Outlier Detection Config.
|
||
odJSON := p.clusterConfig.Cluster.OutlierDetection
|
||
if odJSON == nil {
|
||
odJSON = json.RawMessage(`{}`)
|
||
}
|
||
|
||
lbCfg, err := odParser.ParseConfig(odJSON)
|
||
if err != nil {
|
||
return fmt.Errorf("error parsing Outlier Detection config %v: %v", odJSON, err)
|
||
}
|
||
|
||
odCfg, ok := lbCfg.(*outlierdetection.LBConfig)
|
||
if !ok {
|
||
// Shouldn't happen, Parser built at build time with Outlier
|
||
// Detection builder pulled from gRPC LB Registry.
|
||
return fmt.Errorf("config parser for Outlier Detection returned config with unexpected type %T: %v", lbCfg, lbCfg)
|
||
}
|
||
p.outlierDetection = *odCfg
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ResolverError handles errors reported by the xdsResolver.
|
||
func (b *cdsBalancer) ResolverError(err error) {
|
||
// Missing Listener or RouteConfiguration on the management server
|
||
// results in a 'resource not found' error from the xDS resolver. In
|
||
// these cases, we should report transient failure.
|
||
if xdsresource.ErrType(err) == xdsresource.ErrorTypeResourceNotFound {
|
||
b.closeChildPolicyAndReportTF(err)
|
||
return
|
||
}
|
||
var root string
|
||
if b.lbCfg != nil {
|
||
root = b.lbCfg.ClusterName
|
||
}
|
||
b.onClusterError(root, err)
|
||
}
|
||
|
||
// UpdateSubConnState handles subConn updates from gRPC.
|
||
func (b *cdsBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
|
||
b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
|
||
}
|
||
|
||
// closeChildPolicyAndReportTF closes the child policy, if it exists, and
|
||
// updates the connectivity state of the channel to TransientFailure with an
|
||
// error picker.
|
||
func (b *cdsBalancer) closeChildPolicyAndReportTF(err error) {
|
||
if b.childLB != nil {
|
||
b.childLB.Close()
|
||
b.childLB = nil
|
||
}
|
||
b.cc.UpdateState(balancer.State{
|
||
ConnectivityState: connectivity.TransientFailure,
|
||
Picker: base.NewErrPicker(err),
|
||
})
|
||
}
|
||
|
||
// Close closes the child policy, unsubscribes to the dynamic cluster, and
|
||
// closes the cdsBalancer.
|
||
func (b *cdsBalancer) Close() {
|
||
if b.childLB != nil {
|
||
b.childLB.Close()
|
||
b.childLB = nil
|
||
}
|
||
if b.unsubscribe != nil {
|
||
b.unsubscribe()
|
||
}
|
||
b.logger.Infof("Shutdown")
|
||
}
|
||
|
||
func (b *cdsBalancer) ExitIdle() {
|
||
if b.childLB == nil {
|
||
b.logger.Warningf("Received ExitIdle with no child policy")
|
||
return
|
||
}
|
||
// This implementation assumes the child balancer supports
|
||
// ExitIdle (but still checks for the interface's existence to
|
||
// avoid a panic if not). If the child does not, no subconns
|
||
// will be connected.
|
||
b.childLB.ExitIdle()
|
||
}
|
||
|
||
// Node ID needs to be manually added to errors generated in the following
|
||
// scenarios:
|
||
// - resource-does-not-exist: since the xDS watch API uses a separate callback
|
||
// instead of returning an error value. TODO(gRFC A88): Once A88 is
|
||
// implemented, the xDS client will be able to add the node ID to
|
||
// resource-does-not-exist errors as well, and we can get rid of this
|
||
// special handling.
|
||
// - received a good update from the xDS client, but the update either contains
|
||
// an invalid security configuration or contains invalid aggragate cluster
|
||
// config.
|
||
func (b *cdsBalancer) annotateErrorWithNodeID(err error) error {
|
||
nodeID := b.xdsClient.BootstrapConfig().Node().GetId()
|
||
return fmt.Errorf("[xDS node id: %v]: %w", nodeID, err)
|
||
}
|
||
|
||
// onClusterAmbientError handles an ambient error, if a childLB already has a
|
||
// good update, it should continue using that.
|
||
func (b *cdsBalancer) onClusterAmbientError(name string, err error) {
|
||
b.logger.Warningf("Cluster resource %q received ambient error update: %v", name, err)
|
||
|
||
if xdsresource.ErrType(err) != xdsresource.ErrorTypeConnection && b.childLB != nil {
|
||
// Connection errors will be sent to the child balancers directly.
|
||
// There's no need to forward them.
|
||
b.childLB.ResolverError(err)
|
||
}
|
||
}
|
||
|
||
// onClusterResourceError handles errors to stop using the previously seen
|
||
// resource. Propagates the error down to the child policy if one exists, and
|
||
// puts the channel in TRANSIENT_FAILURE.
|
||
func (b *cdsBalancer) onClusterResourceError(name string, err error) {
|
||
b.logger.Warningf("CDS watch for resource %q reported resource error", name)
|
||
b.closeChildPolicyAndReportTF(err)
|
||
}
|
||
|
||
func (b *cdsBalancer) onClusterError(name string, err error) {
|
||
if b.childLB != nil {
|
||
b.onClusterAmbientError(name, err)
|
||
} else {
|
||
b.onClusterResourceError(name, err)
|
||
}
|
||
}
|