google.golang.org/grpc@v1.72.2/xds/internal/balancer/clusterresolver/configbuilder.go (about)

     1  /*
     2   *
     3   * Copyright 2021 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package clusterresolver
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"sort"
    25  
    26  	"google.golang.org/grpc/internal/balancer/weight"
    27  	"google.golang.org/grpc/internal/hierarchy"
    28  	internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
    29  	"google.golang.org/grpc/resolver"
    30  	"google.golang.org/grpc/resolver/ringhash"
    31  	"google.golang.org/grpc/xds/internal"
    32  	"google.golang.org/grpc/xds/internal/balancer/clusterimpl"
    33  	"google.golang.org/grpc/xds/internal/balancer/outlierdetection"
    34  	"google.golang.org/grpc/xds/internal/balancer/priority"
    35  	"google.golang.org/grpc/xds/internal/balancer/wrrlocality"
    36  	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
    37  )
    38  
    39  const million = 1000000
    40  
    41  // priorityConfig is config for one priority. For example, if there's an EDS and a
    42  // DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}].
    43  //
    44  // Each priorityConfig corresponds to one discovery mechanism from the LBConfig
    45  // generated by the CDS balancer. The CDS balancer resolves the cluster name to
    46  // an ordered list of discovery mechanisms (if the top cluster is an aggregated
    47  // cluster), one for each underlying cluster.
    48  type priorityConfig struct {
    49  	mechanism DiscoveryMechanism
    50  	// edsResp is set only if type is EDS.
    51  	edsResp xdsresource.EndpointsUpdate
    52  	// endpoints is set only if type is DNS.
    53  	endpoints []resolver.Endpoint
    54  	// Each discovery mechanism has a name generator so that the child policies
    55  	// can reuse names between updates (EDS updates for example).
    56  	childNameGen *nameGenerator
    57  }
    58  
    59  // buildPriorityConfigJSON builds balancer config for the passed in
    60  // priorities.
    61  //
    62  // The built tree of balancers (see test for the output struct).
    63  //
    64  //	          ┌────────┐
    65  //	          │priority│
    66  //	          └┬──────┬┘
    67  //	           │      │
    68  //	┌──────────▼─┐  ┌─▼──────────┐
    69  //	│cluster_impl│  │cluster_impl│
    70  //	└──────┬─────┘  └─────┬──────┘
    71  //	       │              │
    72  //	┌──────▼─────┐  ┌─────▼──────┐
    73  //	│xDSLBPolicy │  │xDSLBPolicy │ (Locality and Endpoint picking layer)
    74  //	└────────────┘  └────────────┘
    75  func buildPriorityConfigJSON(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Endpoint, error) {
    76  	pc, endpoints, err := buildPriorityConfig(priorities, xdsLBPolicy)
    77  	if err != nil {
    78  		return nil, nil, fmt.Errorf("failed to build priority config: %v", err)
    79  	}
    80  	ret, err := json.Marshal(pc)
    81  	if err != nil {
    82  		return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err)
    83  	}
    84  	return ret, endpoints, nil
    85  }
    86  
    87  func buildPriorityConfig(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Endpoint, error) {
    88  	var (
    89  		retConfig    = &priority.LBConfig{Children: make(map[string]*priority.Child)}
    90  		retEndpoints []resolver.Endpoint
    91  	)
    92  	for _, p := range priorities {
    93  		switch p.mechanism.Type {
    94  		case DiscoveryMechanismTypeEDS:
    95  			names, configs, endpoints, err := buildClusterImplConfigForEDS(p.childNameGen, p.edsResp, p.mechanism, xdsLBPolicy)
    96  			if err != nil {
    97  				return nil, nil, err
    98  			}
    99  			retConfig.Priorities = append(retConfig.Priorities, names...)
   100  			retEndpoints = append(retEndpoints, endpoints...)
   101  			odCfgs := convertClusterImplMapToOutlierDetection(configs, p.mechanism.outlierDetection)
   102  			for n, c := range odCfgs {
   103  				retConfig.Children[n] = &priority.Child{
   104  					Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: c},
   105  					// Ignore all re-resolution from EDS children.
   106  					IgnoreReresolutionRequests: true,
   107  				}
   108  			}
   109  			continue
   110  		case DiscoveryMechanismTypeLogicalDNS:
   111  			name, config, endpoints := buildClusterImplConfigForDNS(p.childNameGen, p.endpoints, p.mechanism)
   112  			retConfig.Priorities = append(retConfig.Priorities, name)
   113  			retEndpoints = append(retEndpoints, endpoints...)
   114  			odCfg := makeClusterImplOutlierDetectionChild(config, p.mechanism.outlierDetection)
   115  			retConfig.Children[name] = &priority.Child{
   116  				Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: odCfg},
   117  				// Not ignore re-resolution from DNS children, they will trigger
   118  				// DNS to re-resolve.
   119  				IgnoreReresolutionRequests: false,
   120  			}
   121  			continue
   122  		}
   123  	}
   124  	return retConfig, retEndpoints, nil
   125  }
   126  
   127  func convertClusterImplMapToOutlierDetection(ciCfgs map[string]*clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) map[string]*outlierdetection.LBConfig {
   128  	odCfgs := make(map[string]*outlierdetection.LBConfig, len(ciCfgs))
   129  	for n, c := range ciCfgs {
   130  		odCfgs[n] = makeClusterImplOutlierDetectionChild(c, odCfg)
   131  	}
   132  	return odCfgs
   133  }
   134  
   135  func makeClusterImplOutlierDetectionChild(ciCfg *clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) *outlierdetection.LBConfig {
   136  	odCfgRet := odCfg
   137  	odCfgRet.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: ciCfg}
   138  	return &odCfgRet
   139  }
   140  
   141  func buildClusterImplConfigForDNS(g *nameGenerator, endpoints []resolver.Endpoint, mechanism DiscoveryMechanism) (string, *clusterimpl.LBConfig, []resolver.Endpoint) {
   142  	// Endpoint picking policy for DNS is hardcoded to pick_first.
   143  	const childPolicy = "pick_first"
   144  	retEndpoints := make([]resolver.Endpoint, len(endpoints))
   145  	pName := fmt.Sprintf("priority-%v", g.prefix)
   146  	for i, e := range endpoints {
   147  		retEndpoints[i] = hierarchy.SetInEndpoint(e, []string{pName})
   148  		// Copy the nested address field as slice fields are shared by the
   149  		// iteration variable and the original slice.
   150  		retEndpoints[i].Addresses = append([]resolver.Address{}, e.Addresses...)
   151  	}
   152  	return pName, &clusterimpl.LBConfig{
   153  		Cluster:               mechanism.Cluster,
   154  		TelemetryLabels:       mechanism.TelemetryLabels,
   155  		ChildPolicy:           &internalserviceconfig.BalancerConfig{Name: childPolicy},
   156  		MaxConcurrentRequests: mechanism.MaxConcurrentRequests,
   157  		LoadReportingServer:   mechanism.LoadReportingServer,
   158  	}, retEndpoints
   159  }
   160  
   161  // buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for
   162  // each priority, sorted by priority, and the addresses for each priority (with
   163  // hierarchy attributes set).
   164  //
   165  // For example, if there are two priorities, the returned values will be
   166  // - ["p0", "p1"]
   167  // - map{"p0":p0_config, "p1":p1_config}
   168  // - [p0_address_0, p0_address_1, p1_address_0, p1_address_1]
   169  //   - p0 addresses' hierarchy attributes are set to p0
   170  func buildClusterImplConfigForEDS(g *nameGenerator, edsResp xdsresource.EndpointsUpdate, mechanism DiscoveryMechanism, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Endpoint, error) {
   171  	drops := make([]clusterimpl.DropConfig, 0, len(edsResp.Drops))
   172  	for _, d := range edsResp.Drops {
   173  		drops = append(drops, clusterimpl.DropConfig{
   174  			Category:           d.Category,
   175  			RequestsPerMillion: d.Numerator * million / d.Denominator,
   176  		})
   177  	}
   178  
   179  	// Localities of length 0 is triggered by an NACK or resource-not-found
   180  	// error before update, or an empty localities list in an update. In either
   181  	// case want to create a priority, and send down empty address list, causing
   182  	// TF for that priority. "If any discovery mechanism instance experiences an
   183  	// error retrieving data, and it has not previously reported any results, it
   184  	// should report a result that is a single priority with no endpoints." -
   185  	// A37
   186  	priorities := [][]xdsresource.Locality{{}}
   187  	if len(edsResp.Localities) != 0 {
   188  		priorities = groupLocalitiesByPriority(edsResp.Localities)
   189  	}
   190  	retNames := g.generate(priorities)
   191  	retConfigs := make(map[string]*clusterimpl.LBConfig, len(retNames))
   192  	var retEndpoints []resolver.Endpoint
   193  	for i, pName := range retNames {
   194  		priorityLocalities := priorities[i]
   195  		cfg, endpoints, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, mechanism, drops, xdsLBPolicy)
   196  		if err != nil {
   197  			return nil, nil, nil, err
   198  		}
   199  		retConfigs[pName] = cfg
   200  		retEndpoints = append(retEndpoints, endpoints...)
   201  	}
   202  	return retNames, retConfigs, retEndpoints, nil
   203  }
   204  
   205  // groupLocalitiesByPriority returns the localities grouped by priority.
   206  //
   207  // The returned list is sorted from higher priority to lower. Each item in the
   208  // list is a group of localities.
   209  //
   210  // For example, for L0-p0, L1-p0, L2-p1, results will be
   211  // - [[L0, L1], [L2]]
   212  func groupLocalitiesByPriority(localities []xdsresource.Locality) [][]xdsresource.Locality {
   213  	var priorityIntSlice []int
   214  	priorities := make(map[int][]xdsresource.Locality)
   215  	for _, locality := range localities {
   216  		priority := int(locality.Priority)
   217  		priorities[priority] = append(priorities[priority], locality)
   218  		priorityIntSlice = append(priorityIntSlice, priority)
   219  	}
   220  	// Sort the priorities based on the int value, deduplicate, and then turn
   221  	// the sorted list into a string list. This will be child names, in priority
   222  	// order.
   223  	sort.Ints(priorityIntSlice)
   224  	priorityIntSliceDeduped := dedupSortedIntSlice(priorityIntSlice)
   225  	ret := make([][]xdsresource.Locality, 0, len(priorityIntSliceDeduped))
   226  	for _, p := range priorityIntSliceDeduped {
   227  		ret = append(ret, priorities[p])
   228  	}
   229  	return ret
   230  }
   231  
   232  func dedupSortedIntSlice(a []int) []int {
   233  	if len(a) == 0 {
   234  		return a
   235  	}
   236  	i, j := 0, 1
   237  	for ; j < len(a); j++ {
   238  		if a[i] == a[j] {
   239  			continue
   240  		}
   241  		i++
   242  		if i != j {
   243  			a[i] = a[j]
   244  		}
   245  	}
   246  	return a[:i+1]
   247  }
   248  
   249  // priorityLocalitiesToClusterImpl takes a list of localities (with the same
   250  // priority), and generates a cluster impl policy config, and a list of
   251  // addresses with their path hierarchy set to [priority-name, locality-name], so
   252  // priority and the xDS LB Policy know which child policy each address is for.
   253  func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priorityName string, mechanism DiscoveryMechanism, drops []clusterimpl.DropConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Endpoint, error) {
   254  	var retEndpoints []resolver.Endpoint
   255  	for _, locality := range localities {
   256  		var lw uint32 = 1
   257  		if locality.Weight != 0 {
   258  			lw = locality.Weight
   259  		}
   260  		localityStr, err := locality.ID.ToString()
   261  		if err != nil {
   262  			localityStr = fmt.Sprintf("%+v", locality.ID)
   263  		}
   264  		for _, endpoint := range locality.Endpoints {
   265  			// Filter out all "unhealthy" endpoints (unknown and healthy are
   266  			// both considered to be healthy:
   267  			// https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus).
   268  			if endpoint.HealthStatus != xdsresource.EndpointHealthStatusHealthy && endpoint.HealthStatus != xdsresource.EndpointHealthStatusUnknown {
   269  				continue
   270  			}
   271  			resolverEndpoint := resolver.Endpoint{}
   272  			for _, as := range endpoint.Addresses {
   273  				resolverEndpoint.Addresses = append(resolverEndpoint.Addresses, resolver.Address{Addr: as})
   274  			}
   275  			resolverEndpoint = hierarchy.SetInEndpoint(resolverEndpoint, []string{priorityName, localityStr})
   276  			resolverEndpoint = internal.SetLocalityIDInEndpoint(resolverEndpoint, locality.ID)
   277  			// "To provide the xds_wrr_locality load balancer information about
   278  			// locality weights received from EDS, the cluster resolver will
   279  			// populate a new locality weight attribute for each address The
   280  			// attribute will have the weight (as an integer) of the locality
   281  			// the address is part of." - A52
   282  			resolverEndpoint = wrrlocality.SetAddrInfoInEndpoint(resolverEndpoint, wrrlocality.AddrInfo{LocalityWeight: lw})
   283  			var ew uint32 = 1
   284  			if endpoint.Weight != 0 {
   285  				ew = endpoint.Weight
   286  			}
   287  			resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: lw * ew})
   288  			resolverEndpoint = ringhash.SetHashKey(resolverEndpoint, endpoint.HashKey)
   289  			retEndpoints = append(retEndpoints, resolverEndpoint)
   290  		}
   291  	}
   292  	return &clusterimpl.LBConfig{
   293  		Cluster:               mechanism.Cluster,
   294  		EDSServiceName:        mechanism.EDSServiceName,
   295  		LoadReportingServer:   mechanism.LoadReportingServer,
   296  		MaxConcurrentRequests: mechanism.MaxConcurrentRequests,
   297  		TelemetryLabels:       mechanism.TelemetryLabels,
   298  		DropCategories:        drops,
   299  		ChildPolicy:           xdsLBPolicy,
   300  	}, retEndpoints, nil
   301  }