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  }