dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/balancer/ringhash/ring.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 ringhash 25 26 import ( 27 "fmt" 28 "math" 29 "sort" 30 "strconv" 31 ) 32 33 import ( 34 xxhash "github.com/cespare/xxhash/v2" 35 36 "google.golang.org/grpc/resolver" 37 ) 38 39 type ring struct { 40 items []*ringEntry 41 } 42 43 type subConnWithWeight struct { 44 sc *subConn 45 weight float64 46 } 47 48 type ringEntry struct { 49 idx int 50 hash uint64 51 sc *subConn 52 } 53 54 // newRing creates a ring from the subConns. The ring size is limited by the 55 // passed in max/min. 56 // 57 // ring entries will be created for each subConn, and subConn with high weight 58 // (specified by the address) may have multiple entries. 59 // 60 // For example, for subConns with weights {a:3, b:3, c:4}, a generated ring of 61 // size 10 could be: 62 // - {idx:0 hash:3689675255460411075 b} 63 // - {idx:1 hash:4262906501694543955 c} 64 // - {idx:2 hash:5712155492001633497 c} 65 // - {idx:3 hash:8050519350657643659 b} 66 // - {idx:4 hash:8723022065838381142 b} 67 // - {idx:5 hash:11532782514799973195 a} 68 // - {idx:6 hash:13157034721563383607 c} 69 // - {idx:7 hash:14468677667651225770 c} 70 // - {idx:8 hash:17336016884672388720 a} 71 // - {idx:9 hash:18151002094784932496 a} 72 // 73 // To pick from a ring, a binary search will be done for the given target hash, 74 // and first item with hash >= given hash will be returned. 75 func newRing(subConns map[resolver.Address]*subConn, minRingSize, maxRingSize uint64) (*ring, error) { 76 // https://github.com/envoyproxy/envoy/blob/765c970f06a4c962961a0e03a467e165b276d50f/source/common/upstream/ring_hash_lb.cc#L114 77 normalizedWeights, minWeight, err := normalizeWeights(subConns) 78 if err != nil { 79 return nil, err 80 } 81 // Normalized weights for {3,3,4} is {0.3,0.3,0.4}. 82 83 // Scale up the size of the ring such that the least-weighted host gets a 84 // whole number of hashes on the ring. 85 // 86 // Note that size is limited by the input max/min. 87 scale := math.Min(math.Ceil(minWeight*float64(minRingSize))/minWeight, float64(maxRingSize)) 88 ringSize := math.Ceil(scale) 89 items := make([]*ringEntry, 0, int(ringSize)) 90 91 // For each entry, scale*weight nodes are generated in the ring. 92 // 93 // Not all of these are whole numbers. E.g. for weights {a:3,b:3,c:4}, if 94 // ring size is 7, scale is 6.66. The numbers of nodes will be 95 // {a,a,b,b,c,c,c}. 96 // 97 // A hash is generated for each item, and later the results will be sorted 98 // based on the hash. 99 var ( 100 idx int 101 targetIdx float64 102 ) 103 for _, scw := range normalizedWeights { 104 targetIdx += scale * scw.weight 105 for float64(idx) < targetIdx { 106 h := xxhash.Sum64String(scw.sc.addr + strconv.Itoa(len(items))) 107 items = append(items, &ringEntry{idx: idx, hash: h, sc: scw.sc}) 108 idx++ 109 } 110 } 111 112 // Sort items based on hash, to prepare for binary search. 113 sort.Slice(items, func(i, j int) bool { return items[i].hash < items[j].hash }) 114 for i, ii := range items { 115 ii.idx = i 116 } 117 return &ring{items: items}, nil 118 } 119 120 // normalizeWeights divides all the weights by the sum, so that the total weight 121 // is 1. 122 func normalizeWeights(subConns map[resolver.Address]*subConn) (_ []subConnWithWeight, min float64, _ error) { 123 if len(subConns) == 0 { 124 return nil, 0, fmt.Errorf("number of subconns is 0") 125 } 126 var weightSum uint32 127 for a := range subConns { 128 // The address weight was moved from attributes to the Metadata field. 129 // This is necessary (all the attributes need to be stripped) for the 130 // balancer to detect identical {address+weight} combination. 131 weightSum += a.Metadata.(uint32) 132 } 133 if weightSum == 0 { 134 return nil, 0, fmt.Errorf("total weight of all subconns is 0") 135 } 136 weightSumF := float64(weightSum) 137 ret := make([]subConnWithWeight, 0, len(subConns)) 138 min = math.MaxFloat64 139 for a, sc := range subConns { 140 nw := float64(a.Metadata.(uint32)) / weightSumF 141 ret = append(ret, subConnWithWeight{sc: sc, weight: nw}) 142 if nw < min { 143 min = nw 144 } 145 } 146 // Sort the addresses to return consistent results. 147 // 148 // Note: this might not be necessary, but this makes sure the ring is 149 // consistent as long as the addresses are the same, for example, in cases 150 // where an address is added and then removed, the RPCs will still pick the 151 // same old SubConn. 152 sort.Slice(ret, func(i, j int) bool { return ret[i].sc.addr < ret[j].sc.addr }) 153 return ret, min, nil 154 } 155 156 // pick does a binary search. It returns the item with smallest index i that 157 // r.items[i].hash >= h. 158 func (r *ring) pick(h uint64) *ringEntry { 159 i := sort.Search(len(r.items), func(i int) bool { return r.items[i].hash >= h }) 160 if i == len(r.items) { 161 // If not found, and h is greater than the largest hash, return the 162 // first item. 163 i = 0 164 } 165 return r.items[i] 166 } 167 168 // next returns the next entry. 169 func (r *ring) next(e *ringEntry) *ringEntry { 170 return r.items[(e.idx+1)%len(r.items)] 171 }