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  }