/* * * Copyright 2023 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package weightedroundrobin import ( "math" ) type scheduler interface { nextIndex() int } // newScheduler uses scWeights to create a new scheduler for selecting subconns // in a picker. It will return a round robin implementation if at least // len(scWeights)-1 are zero or there is only a single subconn, otherwise it // will return an Earliest Deadline First (EDF) scheduler implementation that // selects the subchannels according to their weights. func newScheduler(scWeights []float64, inc func() uint32) scheduler { n := len(scWeights) if n == 0 { return nil } if n == 1 { return &rrScheduler{numSCs: 1, inc: inc} } sum := float64(0) numZero := 0 max := float64(0) for _, w := range scWeights { sum += w if w > max { max = w } if w == 0 { numZero++ } } if numZero >= n-1 { return &rrScheduler{numSCs: uint32(n), inc: inc} } unscaledMean := sum / float64(n-numZero) scalingFactor := maxWeight / max mean := uint16(math.Round(scalingFactor * unscaledMean)) weights := make([]uint16, n) allEqual := true for i, w := range scWeights { if w == 0 { // Backends with weight = 0 use the mean. weights[i] = mean } else { scaledWeight := uint16(math.Round(scalingFactor * w)) weights[i] = scaledWeight if scaledWeight != mean { allEqual = false } } } if allEqual { return &rrScheduler{numSCs: uint32(n), inc: inc} } logger.Infof("using edf scheduler with weights: %v", weights) return &edfScheduler{weights: weights, inc: inc} } const maxWeight = math.MaxUint16 // edfScheduler implements EDF using the same algorithm as grpc-c++ here: // // https://github.com/grpc/grpc/blob/master/src/core/ext/filters/client_channel/lb_policy/weighted_round_robin/static_stride_scheduler.cc type edfScheduler struct { inc func() uint32 weights []uint16 } // Returns the index in s.weights for the picker to choose. func (s *edfScheduler) nextIndex() int { const offset = maxWeight / 2 for { idx := uint64(s.inc()) // The sequence number (idx) is split in two: the lower %n gives the // index of the backend, and the rest gives the number of times we've // iterated through all backends. `generation` is used to // deterministically decide whether we pick or skip the backend on this // iteration, in proportion to the backend's weight. backendIndex := idx % uint64(len(s.weights)) generation := idx / uint64(len(s.weights)) weight := uint64(s.weights[backendIndex]) // We pick a backend `weight` times per `maxWeight` generations. The // multiply and modulus ~evenly spread out the picks for a given // backend between different generations. The offset by `backendIndex` // helps to reduce the chance of multiple consecutive non-picks: if we // have two consecutive backends with an equal, say, 80% weight of the // max, with no offset we would see 1/5 generations that skipped both. // TODO(b/190488683): add test for offset efficacy. mod := uint64(weight*generation+backendIndex*offset) % maxWeight if mod < maxWeight-weight { continue } return int(backendIndex) } } // A simple RR scheduler to use for fallback when fewer than two backends have // non-zero weights, or all backends have the same weight, or when only one // subconn exists. type rrScheduler struct { inc func() uint32 numSCs uint32 } func (s *rrScheduler) nextIndex() int { idx := s.inc() return int(idx % s.numSCs) }