dubbo.apache.org/dubbo-go/v3@v3.1.1/cluster/loadbalance/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 package ringhash 19 20 import ( 21 "fmt" 22 "math" 23 "math/bits" 24 "sort" 25 "strconv" 26 ) 27 28 import ( 29 "github.com/cespare/xxhash/v2" 30 ) 31 32 import ( 33 "dubbo.apache.org/dubbo-go/v3/protocol" 34 "dubbo.apache.org/dubbo-go/v3/xds/client/resource" 35 "dubbo.apache.org/dubbo-go/v3/xds/utils/grpcrand" 36 ) 37 38 type invokerWithWeight struct { 39 invoker protocol.Invoker 40 weight float64 41 } 42 43 type ringEntry struct { 44 idx int 45 hash uint64 46 invoker protocol.Invoker 47 } 48 49 func (lb *ringhashLoadBalance) generateRing(invokers []invokerWrapper, minRingSize, maxRingSize uint64) ([]*ringEntry, error) { 50 normalizedWeights, minWeight, err := normalizeWeights(invokers) 51 if err != nil { 52 return nil, err 53 } 54 // Normalized weights for {3,3,4} is {0.3,0.3,0.4}. 55 56 // Scale up the size of the ring such that the least-weighted host gets a 57 // whole number of hashes on the ring. 58 // 59 // Note that size is limited by the input max/min. 60 scale := math.Min(math.Ceil(minWeight*float64(minRingSize))/minWeight, float64(maxRingSize)) 61 ringSize := math.Ceil(scale) 62 items := make([]*ringEntry, 0, int(ringSize)) 63 64 // For each entry, scale*weight nodes are generated in the ring. 65 // 66 // Not all of these are whole numbers. E.g. for weights {a:3,b:3,c:4}, if 67 // ring size is 7, scale is 6.66. The numbers of nodes will be 68 // {a,a,b,b,c,c,c}. 69 // 70 // A hash is generated for each item, and later the results will be sorted 71 // based on the hash. 72 var ( 73 idx int 74 targetIdx float64 75 ) 76 for _, inw := range normalizedWeights { 77 targetIdx += scale * inw.weight 78 for float64(idx) < targetIdx { 79 h := xxhash.Sum64String(inw.invoker.GetURL().String() + strconv.Itoa(len(items))) 80 items = append(items, &ringEntry{idx: idx, hash: h, invoker: inw.invoker}) 81 idx++ 82 } 83 } 84 85 // Sort items based on hash, to prepare for binary search. 86 sort.Slice(items, func(i, j int) bool { return items[i].hash < items[j].hash }) 87 for i, ii := range items { 88 ii.idx = i 89 } 90 return items, nil 91 } 92 93 // normalizeWeights divides all the weights by the sum, so that the total weight 94 // is 1. 95 func normalizeWeights(invokers []invokerWrapper) ([]invokerWithWeight, float64, error) { 96 var weightSum int 97 for _, v := range invokers { 98 weightSum += v.weight 99 } 100 if weightSum == 0 { 101 return nil, 0, fmt.Errorf("total weight of all endpoints is 0") 102 } 103 weightSumF := float64(weightSum) 104 ret := make([]invokerWithWeight, 0, len(invokers)) 105 min := math.MaxFloat64 106 for _, invoker := range invokers { 107 nw := float64(invoker.weight) / weightSumF 108 ret = append(ret, invokerWithWeight{invoker: invoker.invoker, weight: nw}) 109 if nw < min { 110 min = nw 111 } 112 } 113 return ret, min, nil 114 } 115 116 func (lb *ringhashLoadBalance) pick(h uint64, items []*ringEntry) *ringEntry { 117 i := sort.Search(len(items), func(i int) bool { return items[i].hash >= h }) 118 if i == len(items) { 119 // If not found, and h is greater than the largest hash, return the 120 // first item. 121 i = 0 122 } 123 return items[i] 124 } 125 126 func (lb *ringhashLoadBalance) generateHash(invocation protocol.Invocation, hashPolicies []*resource.HashPolicy) uint64 { 127 var ( 128 hash uint64 129 generatedHash bool 130 ) 131 for _, policy := range hashPolicies { 132 var ( 133 policyHash uint64 134 generatedPolicyHash bool 135 ) 136 switch policy.HashPolicyType { 137 case resource.HashPolicyTypeHeader: 138 value, ok := invocation.GetAttachment(policy.HeaderName) 139 if !ok || len(value) == 0 { 140 continue 141 } 142 policyHash = xxhash.Sum64String(value) 143 generatedHash = true 144 generatedPolicyHash = true 145 case resource.HashPolicyTypeChannelID: 146 // Hash the ClientConn pointer which logically uniquely 147 // identifies the client. 148 policyHash = xxhash.Sum64String(fmt.Sprintf("%p", &lb.client)) 149 generatedHash = true 150 generatedPolicyHash = true 151 } 152 153 // Deterministically combine the hash policies. Rotating prevents 154 // duplicate hash policies from canceling each other out and preserves 155 // the 64 bits of entropy. 156 if generatedPolicyHash { 157 hash = bits.RotateLeft64(hash, 1) 158 hash = hash ^ policyHash 159 } 160 161 // If terminal policy and a hash has already been generated, ignore the 162 // rest of the policies and use that hash already generated. 163 if policy.Terminal && generatedHash { 164 break 165 } 166 } 167 168 if generatedHash { 169 return hash 170 } 171 // If no generated hash return a random long. In the grand scheme of things 172 // this logically will map to choosing a random backend to route request to. 173 return grpcrand.Uint64() 174 }