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 }