istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/loadbalancer/loadbalancer.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // packages used for load balancer setting
    16  package loadbalancer
    17  
    18  import (
    19  	"math"
    20  	"sort"
    21  	"strings"
    22  
    23  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    24  	endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    25  	wrappers "google.golang.org/protobuf/types/known/wrapperspb"
    26  
    27  	"istio.io/api/networking/v1alpha3"
    28  	"istio.io/istio/pilot/pkg/model"
    29  	"istio.io/istio/pilot/pkg/networking/util"
    30  	"istio.io/istio/pkg/util/sets"
    31  )
    32  
    33  const (
    34  	FailoverPriorityLabelDefaultSeparator = '='
    35  )
    36  
    37  func GetLocalityLbSetting(
    38  	mesh *v1alpha3.LocalityLoadBalancerSetting,
    39  	destrule *v1alpha3.LocalityLoadBalancerSetting,
    40  ) *v1alpha3.LocalityLoadBalancerSetting {
    41  	var enabled bool
    42  	// Locality lb is enabled if its not explicitly disabled in mesh global config
    43  	if mesh != nil && (mesh.Enabled == nil || mesh.Enabled.Value) {
    44  		enabled = true
    45  	}
    46  	// Unless we explicitly override this in destination rule
    47  	if destrule != nil {
    48  		if destrule.Enabled != nil && !destrule.Enabled.Value {
    49  			enabled = false
    50  		} else {
    51  			enabled = true
    52  		}
    53  	}
    54  	if !enabled {
    55  		return nil
    56  	}
    57  
    58  	// Destination Rule overrides mesh config. If its defined, use that
    59  	if destrule != nil {
    60  		return destrule
    61  	}
    62  	// Otherwise fall back to mesh default
    63  	return mesh
    64  }
    65  
    66  func ApplyLocalityLoadBalancer(
    67  	loadAssignment *endpoint.ClusterLoadAssignment,
    68  	wrappedLocalityLbEndpoints []*WrappedLocalityLbEndpoints,
    69  	locality *core.Locality,
    70  	proxyLabels map[string]string,
    71  	localityLB *v1alpha3.LocalityLoadBalancerSetting,
    72  	enableFailover bool,
    73  ) {
    74  	if localityLB == nil || loadAssignment == nil {
    75  		return
    76  	}
    77  
    78  	// one of Distribute or Failover settings can be applied.
    79  	if localityLB.GetDistribute() != nil {
    80  		applyLocalityWeights(locality, loadAssignment, localityLB.GetDistribute())
    81  		// Failover needs outlier detection, otherwise Envoy will never drop down to a lower priority.
    82  		// Do not apply default failover when locality LB is disabled.
    83  	} else if enableFailover && (localityLB.Enabled == nil || localityLB.Enabled.Value) {
    84  		if len(localityLB.FailoverPriority) > 0 {
    85  			// Apply user defined priority failover settings.
    86  			applyFailoverPriorities(loadAssignment, wrappedLocalityLbEndpoints, proxyLabels, localityLB.FailoverPriority)
    87  			// If failover is expliciltly configured with failover priority, apply failover settings also.
    88  			if len(localityLB.Failover) != 0 {
    89  				applyLocalityFailover(locality, loadAssignment, localityLB.Failover)
    90  			}
    91  		} else {
    92  			// Apply default failover settings or user defined region failover settings.
    93  			applyLocalityFailover(locality, loadAssignment, localityLB.Failover)
    94  		}
    95  	}
    96  }
    97  
    98  // set locality loadbalancing weight based on user defined weights.
    99  func applyLocalityWeights(
   100  	locality *core.Locality,
   101  	loadAssignment *endpoint.ClusterLoadAssignment,
   102  	distribute []*v1alpha3.LocalityLoadBalancerSetting_Distribute,
   103  ) {
   104  	if distribute == nil {
   105  		return
   106  	}
   107  
   108  	// Support Locality weighted load balancing
   109  	// (https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/locality_weight#locality-weighted-load-balancing)
   110  	// by providing weights in LocalityLbEndpoints via load_balancing_weight.
   111  	// By setting weights across different localities, it can allow
   112  	// Envoy to do weighted load balancing across different zones and geographical locations.
   113  	for _, localityWeightSetting := range distribute {
   114  		if localityWeightSetting != nil &&
   115  			util.LocalityMatch(locality, localityWeightSetting.From) {
   116  			misMatched := sets.Set[int]{}
   117  			for i := range loadAssignment.Endpoints {
   118  				misMatched.Insert(i)
   119  			}
   120  			for locality, weight := range localityWeightSetting.To {
   121  				// index -> original weight
   122  				destLocMap := map[int]uint32{}
   123  				totalWeight := uint32(0)
   124  				for i, ep := range loadAssignment.Endpoints {
   125  					if misMatched.Contains(i) {
   126  						if util.LocalityMatch(ep.Locality, locality) {
   127  							delete(misMatched, i)
   128  							if ep.LoadBalancingWeight != nil {
   129  								destLocMap[i] = ep.LoadBalancingWeight.Value
   130  							} else {
   131  								destLocMap[i] = 1
   132  							}
   133  							totalWeight += destLocMap[i]
   134  						}
   135  					}
   136  				}
   137  				// in case wildcard dest matching multi groups of endpoints
   138  				// the load balancing weight for a locality is divided by the sum of the weights of all localities
   139  				for index, originalWeight := range destLocMap {
   140  					destWeight := float64(originalWeight*weight) / float64(totalWeight)
   141  					if destWeight > 0 {
   142  						loadAssignment.Endpoints[index].LoadBalancingWeight = &wrappers.UInt32Value{
   143  							Value: uint32(math.Ceil(destWeight)),
   144  						}
   145  					}
   146  				}
   147  			}
   148  
   149  			// remove groups of endpoints in a locality that miss matched
   150  			for i := range misMatched {
   151  				if loadAssignment.Endpoints[i] != nil {
   152  					loadAssignment.Endpoints[i].LbEndpoints = nil
   153  				}
   154  			}
   155  			break
   156  		}
   157  	}
   158  }
   159  
   160  // set locality loadbalancing priority - This is based on Region/Zone/SubZone matching.
   161  func applyLocalityFailover(
   162  	locality *core.Locality,
   163  	loadAssignment *endpoint.ClusterLoadAssignment,
   164  	failover []*v1alpha3.LocalityLoadBalancerSetting_Failover,
   165  ) {
   166  	// key is priority, value is the index of the LocalityLbEndpoints in ClusterLoadAssignment
   167  	priorityMap := map[int][]int{}
   168  
   169  	// 1. calculate the LocalityLbEndpoints.Priority compared with proxy locality
   170  	for i, localityEndpoint := range loadAssignment.Endpoints {
   171  		// if region/zone/subZone all match, the priority is 0.
   172  		// if region/zone match, the priority is 1.
   173  		// if region matches, the priority is 2.
   174  		// if locality not match, the priority is 3.
   175  		priority := util.LbPriority(locality, localityEndpoint.Locality)
   176  		// region not match, apply failover settings when specified
   177  		// update localityLbEndpoints' priority to 4 if failover not match
   178  		if priority == 3 {
   179  			for _, failoverSetting := range failover {
   180  				if failoverSetting.From == locality.Region {
   181  					if localityEndpoint.Locality == nil || localityEndpoint.Locality.Region != failoverSetting.To {
   182  						priority = 4
   183  					}
   184  					break
   185  				}
   186  			}
   187  		}
   188  		// priority is calculated using the already assigned priority using failoverPriority.
   189  		// Since there are at most 5 priorities can be assigned using locality failover(0-4),
   190  		// we multiply the priority by 5 for maintaining the priorities already assigned.
   191  		// Afterwards the final priorities can be calculted from 0 (highest) to N (lowest) without skipping.
   192  		priorityInt := int(loadAssignment.Endpoints[i].Priority*5) + priority
   193  		loadAssignment.Endpoints[i].Priority = uint32(priorityInt)
   194  		priorityMap[priorityInt] = append(priorityMap[priorityInt], i)
   195  	}
   196  
   197  	// since Priorities should range from 0 (highest) to N (lowest) without skipping.
   198  	// 2. adjust the priorities in order
   199  	// 2.1 sort all priorities in increasing order.
   200  	priorities := []int{}
   201  	for priority := range priorityMap {
   202  		priorities = append(priorities, priority)
   203  	}
   204  	sort.Ints(priorities)
   205  	// 2.2 adjust LocalityLbEndpoints priority
   206  	// if the index and value of priorities array is not equal.
   207  	for i, priority := range priorities {
   208  		if i != priority {
   209  			// the LocalityLbEndpoints index in ClusterLoadAssignment.Endpoints
   210  			for _, index := range priorityMap[priority] {
   211  				loadAssignment.Endpoints[index].Priority = uint32(i)
   212  			}
   213  		}
   214  	}
   215  }
   216  
   217  // WrappedLocalityLbEndpoints contain an envoy LocalityLbEndpoints
   218  // and the original IstioEndpoints used to generate it.
   219  // It is used to do failover priority label match with proxy labels.
   220  type WrappedLocalityLbEndpoints struct {
   221  	IstioEndpoints      []*model.IstioEndpoint
   222  	LocalityLbEndpoints *endpoint.LocalityLbEndpoints
   223  }
   224  
   225  // set loadbalancing priority by failover priority label.
   226  func applyFailoverPriorities(
   227  	loadAssignment *endpoint.ClusterLoadAssignment,
   228  	wrappedLocalityLbEndpoints []*WrappedLocalityLbEndpoints,
   229  	proxyLabels map[string]string,
   230  	failoverPriorities []string,
   231  ) {
   232  	if len(proxyLabels) == 0 || len(wrappedLocalityLbEndpoints) == 0 {
   233  		return
   234  	}
   235  	priorityMap := make(map[int][]int, len(failoverPriorities))
   236  	localityLbEndpoints := []*endpoint.LocalityLbEndpoints{}
   237  	for _, wrappedLbEndpoint := range wrappedLocalityLbEndpoints {
   238  		localityLbEndpointsPerLocality := applyFailoverPriorityPerLocality(proxyLabels, wrappedLbEndpoint, failoverPriorities)
   239  		localityLbEndpoints = append(localityLbEndpoints, localityLbEndpointsPerLocality...)
   240  	}
   241  	for i, ep := range localityLbEndpoints {
   242  		priorityMap[int(ep.Priority)] = append(priorityMap[int(ep.Priority)], i)
   243  	}
   244  	// since Priorities should range from 0 (highest) to N (lowest) without skipping.
   245  	// adjust the priorities in order
   246  	// 1. sort all priorities in increasing order.
   247  	priorities := []int{}
   248  	for priority := range priorityMap {
   249  		priorities = append(priorities, priority)
   250  	}
   251  	sort.Ints(priorities)
   252  	// 2. adjust LocalityLbEndpoints priority
   253  	// if the index and value of priorities array is not equal.
   254  	for i, priority := range priorities {
   255  		if i != priority {
   256  			// the LocalityLbEndpoints index in ClusterLoadAssignment.Endpoints
   257  			for _, index := range priorityMap[priority] {
   258  				localityLbEndpoints[index].Priority = uint32(i)
   259  			}
   260  		}
   261  	}
   262  	loadAssignment.Endpoints = localityLbEndpoints
   263  }
   264  
   265  // Returning the label names in a separate array as the iteration of map is not ordered.
   266  func priorityLabelOverrides(labels []string) ([]string, map[string]string) {
   267  	priorityLabels := make([]string, 0, len(labels))
   268  	overriddenValueByLabel := make(map[string]string, len(labels))
   269  	var tempStrings []string
   270  	for _, labelWithValue := range labels {
   271  		tempStrings = strings.Split(labelWithValue, string(FailoverPriorityLabelDefaultSeparator))
   272  		priorityLabels = append(priorityLabels, tempStrings[0])
   273  		if len(tempStrings) == 2 {
   274  			overriddenValueByLabel[tempStrings[0]] = tempStrings[1]
   275  			continue
   276  		}
   277  	}
   278  	return priorityLabels, overriddenValueByLabel
   279  }
   280  
   281  // set loadbalancing priority by failover priority label.
   282  // split one LocalityLbEndpoints to multiple LocalityLbEndpoints based on failover priorities.
   283  func applyFailoverPriorityPerLocality(
   284  	proxyLabels map[string]string,
   285  	ep *WrappedLocalityLbEndpoints,
   286  	failoverPriorities []string,
   287  ) []*endpoint.LocalityLbEndpoints {
   288  	lowestPriority := len(failoverPriorities)
   289  	// key is priority, value is the index of LocalityLbEndpoints.LbEndpoints
   290  	priorityMap := map[int][]int{}
   291  	priorityLabels, priorityLabelOverrides := priorityLabelOverrides(failoverPriorities)
   292  	for i, istioEndpoint := range ep.IstioEndpoints {
   293  		var priority int
   294  		// failoverPriority labels match
   295  		for j, label := range priorityLabels {
   296  			valueForProxy, ok := priorityLabelOverrides[label]
   297  			if !ok {
   298  				valueForProxy = proxyLabels[label]
   299  			}
   300  			if valueForProxy != istioEndpoint.Labels[label] {
   301  				priority = lowestPriority - j
   302  				break
   303  			}
   304  		}
   305  		priorityMap[priority] = append(priorityMap[priority], i)
   306  	}
   307  
   308  	// sort all priorities in increasing order.
   309  	priorities := []int{}
   310  	for priority := range priorityMap {
   311  		priorities = append(priorities, priority)
   312  	}
   313  	sort.Ints(priorities)
   314  
   315  	out := make([]*endpoint.LocalityLbEndpoints, len(priorityMap))
   316  	for i, priority := range priorities {
   317  		out[i] = util.CloneLocalityLbEndpoint(ep.LocalityLbEndpoints)
   318  		out[i].LbEndpoints = nil
   319  		out[i].Priority = uint32(priority)
   320  		var weight uint32
   321  		for _, index := range priorityMap[priority] {
   322  			out[i].LbEndpoints = append(out[i].LbEndpoints, ep.LocalityLbEndpoints.LbEndpoints[index])
   323  			weight += ep.LocalityLbEndpoints.LbEndpoints[index].GetLoadBalancingWeight().GetValue()
   324  		}
   325  		// reset weight
   326  		out[i].LoadBalancingWeight = &wrappers.UInt32Value{
   327  			Value: weight,
   328  		}
   329  	}
   330  
   331  	return out
   332  }