google.golang.org/grpc@v1.72.2/xds/internal/balancer/ringhash/ring.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 ringhash
    20  
    21  import (
    22  	"math"
    23  	"sort"
    24  	"strconv"
    25  
    26  	xxhash "github.com/cespare/xxhash/v2"
    27  	"google.golang.org/grpc/internal/grpclog"
    28  	"google.golang.org/grpc/resolver"
    29  )
    30  
    31  type ring struct {
    32  	items []*ringEntry
    33  }
    34  
    35  type endpointInfo struct {
    36  	hashKey        string
    37  	scaledWeight   float64
    38  	originalWeight uint32
    39  }
    40  
    41  type ringEntry struct {
    42  	idx     int
    43  	hash    uint64
    44  	hashKey string
    45  	weight  uint32
    46  }
    47  
    48  // newRing creates a ring from the endpoints stored in the EndpointMap. The ring
    49  // size is limited by the passed in max/min.
    50  //
    51  // ring entries will be created for each endpoint, and endpoints with high
    52  // weight (specified by the endpoint) may have multiple entries.
    53  //
    54  // For example, for endpoints with weights {a:3, b:3, c:4}, a generated ring of
    55  // size 10 could be:
    56  // - {idx:0 hash:3689675255460411075  b}
    57  // - {idx:1 hash:4262906501694543955  c}
    58  // - {idx:2 hash:5712155492001633497  c}
    59  // - {idx:3 hash:8050519350657643659  b}
    60  // - {idx:4 hash:8723022065838381142  b}
    61  // - {idx:5 hash:11532782514799973195 a}
    62  // - {idx:6 hash:13157034721563383607 c}
    63  // - {idx:7 hash:14468677667651225770 c}
    64  // - {idx:8 hash:17336016884672388720 a}
    65  // - {idx:9 hash:18151002094784932496 a}
    66  //
    67  // To pick from a ring, a binary search will be done for the given target hash,
    68  // and first item with hash >= given hash will be returned.
    69  //
    70  // Must be called with a non-empty endpoints map.
    71  func newRing(endpoints *resolver.EndpointMap[*endpointState], minRingSize, maxRingSize uint64, logger *grpclog.PrefixLogger) *ring {
    72  	if logger.V(2) {
    73  		logger.Infof("newRing: number of endpoints is %d, minRingSize is %d, maxRingSize is %d", endpoints.Len(), minRingSize, maxRingSize)
    74  	}
    75  
    76  	// https://github.com/envoyproxy/envoy/blob/765c970f06a4c962961a0e03a467e165b276d50f/source/common/upstream/ring_hash_lb.cc#L114
    77  	normalizedWeights, minWeight := normalizeWeights(endpoints)
    78  	if logger.V(2) {
    79  		logger.Infof("newRing: normalized endpoint weights is %v", normalizedWeights)
    80  	}
    81  
    82  	// Normalized weights for {3,3,4} is {0.3,0.3,0.4}.
    83  
    84  	// Scale up the size of the ring such that the least-weighted host gets a
    85  	// whole number of hashes on the ring.
    86  	//
    87  	// Note that size is limited by the input max/min.
    88  	scale := math.Min(math.Ceil(minWeight*float64(minRingSize))/minWeight, float64(maxRingSize))
    89  	ringSize := math.Ceil(scale)
    90  	items := make([]*ringEntry, 0, int(ringSize))
    91  	if logger.V(2) {
    92  		logger.Infof("newRing: creating new ring of size %v", ringSize)
    93  	}
    94  
    95  	// For each entry, scale*weight nodes are generated in the ring.
    96  	//
    97  	// Not all of these are whole numbers. E.g. for weights {a:3,b:3,c:4}, if
    98  	// ring size is 7, scale is 6.66. The numbers of nodes will be
    99  	// {a,a,b,b,c,c,c}.
   100  	//
   101  	// A hash is generated for each item, and later the results will be sorted
   102  	// based on the hash.
   103  	var currentHashes, targetHashes float64
   104  	for _, epInfo := range normalizedWeights {
   105  		targetHashes += scale * epInfo.scaledWeight
   106  		// This index ensures that ring entries corresponding to the same
   107  		// endpoint hash to different values. And since this index is
   108  		// per-endpoint, these entries hash to the same value across address
   109  		// updates.
   110  		idx := 0
   111  		for currentHashes < targetHashes {
   112  			h := xxhash.Sum64String(epInfo.hashKey + "_" + strconv.Itoa(idx))
   113  			items = append(items, &ringEntry{hash: h, hashKey: epInfo.hashKey, weight: epInfo.originalWeight})
   114  			idx++
   115  			currentHashes++
   116  		}
   117  	}
   118  
   119  	// Sort items based on hash, to prepare for binary search.
   120  	sort.Slice(items, func(i, j int) bool { return items[i].hash < items[j].hash })
   121  	for i, ii := range items {
   122  		ii.idx = i
   123  	}
   124  	return &ring{items: items}
   125  }
   126  
   127  // normalizeWeights calculates the normalized weights for each endpoint in the
   128  // given endpoints map. It returns a slice of endpointWithState structs, where
   129  // each struct contains the picker for an endpoint and its corresponding weight.
   130  // The function also returns the minimum weight among all endpoints.
   131  //
   132  // The normalized weight of each endpoint is calculated by dividing its weight
   133  // attribute by the sum of all endpoint weights. If the weight attribute is not
   134  // found on the endpoint, a default weight of 1 is used.
   135  //
   136  // The endpoints are sorted in ascending order to ensure consistent results.
   137  //
   138  // Must be called with a non-empty endpoints map.
   139  func normalizeWeights(endpoints *resolver.EndpointMap[*endpointState]) ([]endpointInfo, float64) {
   140  	var weightSum uint32
   141  	// Since attributes are explicitly ignored in the EndpointMap key, we need
   142  	// to iterate over the values to get the weights.
   143  	endpointVals := endpoints.Values()
   144  	for _, epState := range endpointVals {
   145  		weightSum += epState.weight
   146  	}
   147  	ret := make([]endpointInfo, 0, endpoints.Len())
   148  	min := 1.0
   149  	for _, epState := range endpointVals {
   150  		// (*endpointState).weight is set to 1 if the weight attribute is not
   151  		// found on the endpoint. And since this function is guaranteed to be
   152  		// called with a non-empty endpoints map, weightSum is guaranteed to be
   153  		// non-zero. So, we need not worry about divide by zero error here.
   154  		nw := float64(epState.weight) / float64(weightSum)
   155  		ret = append(ret, endpointInfo{
   156  			hashKey:        epState.hashKey,
   157  			scaledWeight:   nw,
   158  			originalWeight: epState.weight,
   159  		})
   160  		min = math.Min(min, nw)
   161  	}
   162  	// Sort the endpoints to return consistent results.
   163  	//
   164  	// Note: this might not be necessary, but this makes sure the ring is
   165  	// consistent as long as the endpoints are the same, for example, in cases
   166  	// where an endpoint is added and then removed, the RPCs will still pick the
   167  	// same old endpoint.
   168  	sort.Slice(ret, func(i, j int) bool {
   169  		return ret[i].hashKey < ret[j].hashKey
   170  	})
   171  	return ret, min
   172  }
   173  
   174  // pick does a binary search. It returns the item with smallest index i that
   175  // r.items[i].hash >= h.
   176  func (r *ring) pick(h uint64) *ringEntry {
   177  	i := sort.Search(len(r.items), func(i int) bool { return r.items[i].hash >= h })
   178  	if i == len(r.items) {
   179  		// If not found, and h is greater than the largest hash, return the
   180  		// first item.
   181  		i = 0
   182  	}
   183  	return r.items[i]
   184  }
   185  
   186  // next returns the next entry.
   187  func (r *ring) next(e *ringEntry) *ringEntry {
   188  	return r.items[(e.idx+1)%len(r.items)]
   189  }