dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/balancer/clusterresolver/configbuilder.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one or more
     3   * contributor license agreements.  See the NOTICE file distributed with
     4   * this work for additional information regarding copyright ownership.
     5   * The ASF licenses this file to You under the Apache License, Version 2.0
     6   * (the "License"); you may not use this file except in compliance with
     7   * the License.  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   *
    20   * Copyright 2021 gRPC authors.
    21   *
    22   */
    23  
    24  package clusterresolver
    25  
    26  import (
    27  	"encoding/json"
    28  	"fmt"
    29  	"sort"
    30  )
    31  
    32  import (
    33  	dubbogoLogger "github.com/dubbogo/gost/log/logger"
    34  
    35  	"google.golang.org/grpc/balancer/roundrobin"
    36  	"google.golang.org/grpc/balancer/weightedroundrobin"
    37  	"google.golang.org/grpc/balancer/weightedtarget"
    38  
    39  	"google.golang.org/grpc/resolver"
    40  )
    41  
    42  import (
    43  	"dubbo.apache.org/dubbo-go/v3/xds/balancer/clusterimpl"
    44  	"dubbo.apache.org/dubbo-go/v3/xds/balancer/priority"
    45  	"dubbo.apache.org/dubbo-go/v3/xds/balancer/ringhash"
    46  	"dubbo.apache.org/dubbo-go/v3/xds/client/resource"
    47  	"dubbo.apache.org/dubbo-go/v3/xds/utils/hierarchy"
    48  	internalserviceconfig "dubbo.apache.org/dubbo-go/v3/xds/utils/serviceconfig"
    49  )
    50  
    51  const million = 1000000
    52  
    53  // priorityConfig is config for one priority. For example, if there an EDS and a
    54  // DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}].
    55  //
    56  // Each priorityConfig corresponds to one discovery mechanism from the LBConfig
    57  // generated by the CDS balancer. The CDS balancer resolves the cluster name to
    58  // an ordered list of discovery mechanisms (if the top cluster is an aggregated
    59  // cluster), one for each underlying cluster.
    60  type priorityConfig struct {
    61  	mechanism DiscoveryMechanism
    62  	// edsResp is set only if type is EDS.
    63  	edsResp resource.EndpointsUpdate
    64  	// addresses is set only if type is DNS.
    65  	addresses []string
    66  }
    67  
    68  // buildPriorityConfigJSON builds balancer config for the passed in
    69  // priorities.
    70  //
    71  // The built tree of balancers (see test for the output struct).
    72  //
    73  // If xds lb policy is ROUND_ROBIN, the children will be weighted_target for
    74  // locality picking, and round_robin for endpoint picking.
    75  //
    76  //	                         ┌────────┐
    77  //	                         │priority│
    78  //	                         └┬──────┬┘
    79  //	                          │      │
    80  //	              ┌───────────▼┐    ┌▼───────────┐
    81  //	              │cluster_impl│    │cluster_impl│
    82  //	              └─┬──────────┘    └──────────┬─┘
    83  //	                │                          │
    84  //	 ┌──────────────▼─┐                      ┌─▼──────────────┐
    85  //	 │locality_picking│                      │locality_picking│
    86  //	 └┬──────────────┬┘                      └┬──────────────┬┘
    87  //	  │              │                        │              │
    88  //	┌─▼─┐          ┌─▼─┐                    ┌─▼─┐          ┌─▼─┐
    89  //	│LRS│          │LRS│                    │LRS│          │LRS│
    90  //	└─┬─┘          └─┬─┘                    └─┬─┘          └─┬─┘
    91  //	  │              │                        │              │
    92  //
    93  // ┌──────────▼─────┐  ┌─────▼──────────┐  ┌──────────▼─────┐  ┌─────▼──────────┐
    94  // │endpoint_picking│  │endpoint_picking│  │endpoint_picking│  │endpoint_picking│
    95  // └────────────────┘  └────────────────┘  └────────────────┘  └────────────────┘
    96  //
    97  // If xds lb policy is RING_HASH, the children will be just a ring_hash policy.
    98  // The endpoints from all localities will be flattened to one addresses list,
    99  // and the ring_hash policy will pick endpoints from it.
   100  //
   101  //	┌────────┐
   102  //	│priority│
   103  //	└┬──────┬┘
   104  //	 │      │
   105  //
   106  // ┌──────────▼─┐  ┌─▼──────────┐
   107  // │cluster_impl│  │cluster_impl│
   108  // └──────┬─────┘  └─────┬──────┘
   109  //
   110  //	│              │
   111  //
   112  // ┌──────▼─────┐  ┌─────▼──────┐
   113  // │ ring_hash  │  │ ring_hash  │
   114  // └────────────┘  └────────────┘
   115  //
   116  // If endpointPickingPolicy is nil, roundrobin will be used.
   117  //
   118  // Custom locality picking policy isn't support, and weighted_target is always
   119  // used.
   120  func buildPriorityConfigJSON(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Address, error) {
   121  	pc, addrs, err := buildPriorityConfig(priorities, xdsLBPolicy)
   122  	if err != nil {
   123  		return nil, nil, fmt.Errorf("failed to build priority config: %v", err)
   124  	}
   125  	ret, err := json.Marshal(pc)
   126  	if err != nil {
   127  		return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err)
   128  	}
   129  	return ret, addrs, nil
   130  }
   131  
   132  func buildPriorityConfig(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Address, error) {
   133  	var (
   134  		retConfig = &priority.LBConfig{Children: make(map[string]*priority.Child)}
   135  		retAddrs  []resolver.Address
   136  	)
   137  	for i, p := range priorities {
   138  		switch p.mechanism.Type {
   139  		case DiscoveryMechanismTypeEDS:
   140  			names, configs, addrs, err := buildClusterImplConfigForEDS(i, p.edsResp, p.mechanism, xdsLBPolicy)
   141  			if err != nil {
   142  				return nil, nil, err
   143  			}
   144  			retConfig.Priorities = append(retConfig.Priorities, names...)
   145  			for n, c := range configs {
   146  				retConfig.Children[n] = &priority.Child{
   147  					Config: &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: c},
   148  					// Ignore all re-resolution from EDS children.
   149  					IgnoreReresolutionRequests: true,
   150  				}
   151  			}
   152  			retAddrs = append(retAddrs, addrs...)
   153  		case DiscoveryMechanismTypeLogicalDNS:
   154  			name, config, addrs := buildClusterImplConfigForDNS(i, p.addresses)
   155  			retConfig.Priorities = append(retConfig.Priorities, name)
   156  			retConfig.Children[name] = &priority.Child{
   157  				Config: &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: config},
   158  				// Not ignore re-resolution from DNS children, they will trigger
   159  				// DNS to re-resolve.
   160  				IgnoreReresolutionRequests: false,
   161  			}
   162  			retAddrs = append(retAddrs, addrs...)
   163  		}
   164  	}
   165  	return retConfig, retAddrs, nil
   166  }
   167  
   168  func buildClusterImplConfigForDNS(parentPriority int, addrStrs []string) (string, *clusterimpl.LBConfig, []resolver.Address) {
   169  	// Endpoint picking policy for DNS is hardcoded to pick_first.
   170  	const childPolicy = "pick_first"
   171  	retAddrs := make([]resolver.Address, 0, len(addrStrs))
   172  	pName := fmt.Sprintf("priority-%v", parentPriority)
   173  	for _, addrStr := range addrStrs {
   174  		retAddrs = append(retAddrs, hierarchy.Set(resolver.Address{Addr: addrStr}, []string{pName}))
   175  	}
   176  	return pName, &clusterimpl.LBConfig{ChildPolicy: &internalserviceconfig.BalancerConfig{Name: childPolicy}}, retAddrs
   177  }
   178  
   179  // buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for
   180  // each priority, sorted by priority, and the addresses for each priority (with
   181  // hierarchy attributes set).
   182  //
   183  // For example, if there are two priorities, the returned values will be
   184  // - ["p0", "p1"]
   185  // - map{"p0":p0_config, "p1":p1_config}
   186  // - [p0_address_0, p0_address_1, p1_address_0, p1_address_1]
   187  //   - p0 addresses' hierarchy attributes are set to p0
   188  func buildClusterImplConfigForEDS(parentPriority int, edsResp resource.EndpointsUpdate, mechanism DiscoveryMechanism, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Address, error) {
   189  	drops := make([]clusterimpl.DropConfig, 0, len(edsResp.Drops))
   190  	for _, d := range edsResp.Drops {
   191  		drops = append(drops, clusterimpl.DropConfig{
   192  			Category:           d.Category,
   193  			RequestsPerMillion: d.Numerator * million / d.Denominator,
   194  		})
   195  	}
   196  
   197  	priorityChildNames, priorities := groupLocalitiesByPriority(edsResp.Localities)
   198  	retNames := make([]string, 0, len(priorityChildNames))
   199  	retAddrs := make([]resolver.Address, 0, len(priorityChildNames))
   200  	retConfigs := make(map[string]*clusterimpl.LBConfig, len(priorityChildNames))
   201  	for _, priorityName := range priorityChildNames {
   202  		priorityLocalities := priorities[priorityName]
   203  		// Prepend parent priority to the priority names, to avoid duplicates.
   204  		pName := fmt.Sprintf("priority-%v-%v", parentPriority, priorityName)
   205  		retNames = append(retNames, pName)
   206  		cfg, addrs, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, mechanism, drops, xdsLBPolicy)
   207  		if err != nil {
   208  			return nil, nil, nil, err
   209  		}
   210  		retConfigs[pName] = cfg
   211  		retAddrs = append(retAddrs, addrs...)
   212  	}
   213  	return retNames, retConfigs, retAddrs, nil
   214  }
   215  
   216  // groupLocalitiesByPriority returns the localities grouped by priority.
   217  //
   218  // It also returns a list of strings where each string represents a priority,
   219  // and the list is sorted from higher priority to lower priority.
   220  //
   221  // For example, for L0-p0, L1-p0, L2-p1, results will be
   222  // - ["p0", "p1"]
   223  // - map{"p0":[L0, L1], "p1":[L2]}
   224  func groupLocalitiesByPriority(localities []resource.Locality) ([]string, map[string][]resource.Locality) {
   225  	var priorityIntSlice []int
   226  	priorities := make(map[string][]resource.Locality)
   227  	for _, locality := range localities {
   228  		if locality.Weight == 0 {
   229  			continue
   230  		}
   231  		priorityName := fmt.Sprintf("%v", locality.Priority)
   232  		priorities[priorityName] = append(priorities[priorityName], locality)
   233  		priorityIntSlice = append(priorityIntSlice, int(locality.Priority))
   234  	}
   235  	// Sort the priorities based on the int value, deduplicate, and then turn
   236  	// the sorted list into a string list. This will be child names, in priority
   237  	// order.
   238  	sort.Ints(priorityIntSlice)
   239  	priorityIntSliceDeduped := dedupSortedIntSlice(priorityIntSlice)
   240  	priorityNameSlice := make([]string, 0, len(priorityIntSliceDeduped))
   241  	for _, p := range priorityIntSliceDeduped {
   242  		priorityNameSlice = append(priorityNameSlice, fmt.Sprintf("%v", p))
   243  	}
   244  	return priorityNameSlice, priorities
   245  }
   246  
   247  func dedupSortedIntSlice(a []int) []int {
   248  	if len(a) == 0 {
   249  		return a
   250  	}
   251  	i, j := 0, 1
   252  	for ; j < len(a); j++ {
   253  		if a[i] == a[j] {
   254  			continue
   255  		}
   256  		i++
   257  		if i != j {
   258  			a[i] = a[j]
   259  		}
   260  	}
   261  	return a[:i+1]
   262  }
   263  
   264  // rrBalancerConfig is a const roundrobin config, used as child of
   265  // weighted-roundrobin. To avoid allocating memory everytime.
   266  var rrBalancerConfig = &internalserviceconfig.BalancerConfig{Name: roundrobin.Name}
   267  
   268  // priorityLocalitiesToClusterImpl takes a list of localities (with the same
   269  // priority), and generates a cluster impl policy config, and a list of
   270  // addresses.
   271  func priorityLocalitiesToClusterImpl(localities []resource.Locality, priorityName string, mechanism DiscoveryMechanism, drops []clusterimpl.DropConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Address, error) {
   272  	clusterImplCfg := &clusterimpl.LBConfig{
   273  		Cluster:                 mechanism.Cluster,
   274  		EDSServiceName:          mechanism.EDSServiceName,
   275  		LoadReportingServerName: mechanism.LoadReportingServerName,
   276  		MaxConcurrentRequests:   mechanism.MaxConcurrentRequests,
   277  		DropCategories:          drops,
   278  		// ChildPolicy is not set. Will be set based on xdsLBPolicy
   279  	}
   280  
   281  	if xdsLBPolicy == nil || xdsLBPolicy.Name == rrName {
   282  		// If lb policy is ROUND_ROBIN:
   283  		// - locality-picking policy is weighted_target
   284  		// - endpoint-picking policy is round_robin
   285  		dubbogoLogger.Infof("xds lb policy is %q, building config with weighted_target + round_robin", rrName)
   286  		// Child of weighted_target is hardcoded to round_robin.
   287  		wtConfig, addrs := localitiesToWeightedTarget(localities, priorityName, rrBalancerConfig)
   288  		clusterImplCfg.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: weightedtarget.Name, Config: wtConfig}
   289  		return clusterImplCfg, addrs, nil
   290  	}
   291  
   292  	if xdsLBPolicy.Name == rhName {
   293  		// If lb policy is RIHG_HASH, will build one ring_hash policy as child.
   294  		// The endpoints from all localities will be flattened to one addresses
   295  		// list, and the ring_hash policy will pick endpoints from it.
   296  		dubbogoLogger.Infof("xds lb policy is %q, building config with ring_hash", rhName)
   297  		addrs := localitiesToRingHash(localities, priorityName)
   298  		// Set child to ring_hash, note that the ring_hash config is from
   299  		// xdsLBPolicy.
   300  		clusterImplCfg.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: ringhash.Name, Config: xdsLBPolicy.Config}
   301  		return clusterImplCfg, addrs, nil
   302  	}
   303  
   304  	return nil, nil, fmt.Errorf("unsupported xds LB policy %q, not one of {%q,%q}", xdsLBPolicy.Name, rrName, rhName)
   305  }
   306  
   307  // localitiesToRingHash takes a list of localities (with the same priority), and
   308  // generates a list of addresses.
   309  //
   310  // The addresses have path hierarchy set to [priority-name], so priority knows
   311  // which child policy they are for.
   312  func localitiesToRingHash(localities []resource.Locality, priorityName string) []resolver.Address {
   313  	var addrs []resolver.Address
   314  	for _, locality := range localities {
   315  		var lw uint32 = 1
   316  		if locality.Weight != 0 {
   317  			lw = locality.Weight
   318  		}
   319  		localityStr, err := locality.ID.ToString()
   320  		if err != nil {
   321  			localityStr = fmt.Sprintf("%+v", locality.ID)
   322  		}
   323  		for _, endpoint := range locality.Endpoints {
   324  			// Filter out all "unhealthy" endpoints (unknown and healthy are
   325  			// both considered to be healthy:
   326  			// https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus).
   327  			if endpoint.HealthStatus != resource.EndpointHealthStatusHealthy && endpoint.HealthStatus != resource.EndpointHealthStatusUnknown {
   328  				continue
   329  			}
   330  
   331  			var ew uint32 = 1
   332  			if endpoint.Weight != 0 {
   333  				ew = endpoint.Weight
   334  			}
   335  
   336  			// The weight of each endpoint is locality_weight * endpoint_weight.
   337  			ai := weightedroundrobin.AddrInfo{Weight: lw * ew}
   338  			addr := weightedroundrobin.SetAddrInfo(resolver.Address{Addr: endpoint.Address}, ai)
   339  			addr = hierarchy.Set(addr, []string{priorityName, localityStr})
   340  			addr = resource.SetLocalityID(addr, locality.ID)
   341  			addrs = append(addrs, addr)
   342  		}
   343  	}
   344  	return addrs
   345  }
   346  
   347  // localitiesToWeightedTarget takes a list of localities (with the same
   348  // priority), and generates a weighted target config, and list of addresses.
   349  //
   350  // The addresses have path hierarchy set to [priority-name, locality-name], so
   351  // priority and weighted target know which child policy they are for.
   352  func localitiesToWeightedTarget(localities []resource.Locality, priorityName string, childPolicy *internalserviceconfig.BalancerConfig) (*TargetLBConfig, []resolver.Address) {
   353  	weightedTargets := make(map[string]Target)
   354  	var addrs []resolver.Address
   355  	for _, locality := range localities {
   356  		localityStr, err := locality.ID.ToString()
   357  		if err != nil {
   358  			localityStr = fmt.Sprintf("%+v", locality.ID)
   359  		}
   360  		weightedTargets[localityStr] = Target{Weight: locality.Weight, ChildPolicy: childPolicy}
   361  		for _, endpoint := range locality.Endpoints {
   362  			// Filter out all "unhealthy" endpoints (unknown and healthy are
   363  			// both considered to be healthy:
   364  			// https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus).
   365  			if endpoint.HealthStatus != resource.EndpointHealthStatusHealthy && endpoint.HealthStatus != resource.EndpointHealthStatusUnknown {
   366  				continue
   367  			}
   368  
   369  			addr := resolver.Address{Addr: endpoint.Address}
   370  			if childPolicy.Name == weightedroundrobin.Name && endpoint.Weight != 0 {
   371  				ai := weightedroundrobin.AddrInfo{Weight: endpoint.Weight}
   372  				addr = weightedroundrobin.SetAddrInfo(addr, ai)
   373  			}
   374  			addr = hierarchy.Set(addr, []string{priorityName, localityStr})
   375  			addr = resource.SetLocalityID(addr, locality.ID)
   376  			addrs = append(addrs, addr)
   377  		}
   378  	}
   379  	return &TargetLBConfig{Targets: weightedTargets}, addrs
   380  }