![JAR search and dependency download from the Maven repository](/logo.png)
io.grpc.xds.WeightedRoundRobinLoadBalancer Maven / Gradle / Ivy
/*
* Copyright 2023 The 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 io.grpc.xds;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import io.grpc.ConnectivityState;
import io.grpc.ConnectivityStateInfo;
import io.grpc.Deadline.Ticker;
import io.grpc.DoubleHistogramMetricInstrument;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancerProvider;
import io.grpc.LongCounterMetricInstrument;
import io.grpc.MetricInstrumentRegistry;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
import io.grpc.SynchronizationContext.ScheduledHandle;
import io.grpc.services.MetricReport;
import io.grpc.util.ForwardingSubchannel;
import io.grpc.util.MultiChildLoadBalancer;
import io.grpc.xds.orca.OrcaOobUtil;
import io.grpc.xds.orca.OrcaOobUtil.OrcaOobReportListener;
import io.grpc.xds.orca.OrcaPerRequestUtil;
import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A {@link LoadBalancer} that provides weighted-round-robin load-balancing over the
* {@link EquivalentAddressGroup}s from the {@link NameResolver}. The subchannel weights are
* determined by backend metrics using ORCA.
* To use WRR, users may configure through channel serviceConfig. Example config:
* {@code
* String wrrConfig = "{\"loadBalancingConfig\":" +
* "[{\"weighted_round_robin\":{\"enableOobLoadReport\":true, " +
* "\"blackoutPeriod\":\"10s\"," +
* "\"oobReportingPeriod\":\"10s\"," +
* "\"weightExpirationPeriod\":\"180s\"," +
* "\"errorUtilizationPenalty\":\"1.0\"," +
* "\"weightUpdatePeriod\":\"1s\"}}]}";
* serviceConfig = (Map) JsonParser.parse(wrrConfig);
* channel = ManagedChannelBuilder.forTarget("test:///lb.test.grpc.io")
* .defaultServiceConfig(serviceConfig)
* .build();
* }
*
* Users may also configure through xDS control plane via custom lb policy. But that is much more
* complex to set up. Example config:
*
* localityLbPolicies:
* - customPolicy:
* name: weighted_round_robin
* data: '{ "enableOobLoadReport": true }'
*
* See related documentation: https://cloud.google.com/service-mesh/legacy/load-balancing-apis/proxyless-configure-advanced-traffic-management#custom-lb-config
*/
final class WeightedRoundRobinLoadBalancer extends MultiChildLoadBalancer {
private static final LongCounterMetricInstrument RR_FALLBACK_COUNTER;
private static final LongCounterMetricInstrument ENDPOINT_WEIGHT_NOT_YET_USEABLE_COUNTER;
private static final LongCounterMetricInstrument ENDPOINT_WEIGHT_STALE_COUNTER;
private static final DoubleHistogramMetricInstrument ENDPOINT_WEIGHTS_HISTOGRAM;
private static final Logger log = Logger.getLogger(
WeightedRoundRobinLoadBalancer.class.getName());
private WeightedRoundRobinLoadBalancerConfig config;
private final SynchronizationContext syncContext;
private final ScheduledExecutorService timeService;
private ScheduledHandle weightUpdateTimer;
private final Runnable updateWeightTask;
private final AtomicInteger sequence;
private final long infTime;
private final Ticker ticker;
private String locality = "";
private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult());
// The metric instruments are only registered once and shared by all instances of this LB.
static {
MetricInstrumentRegistry metricInstrumentRegistry
= MetricInstrumentRegistry.getDefaultRegistry();
RR_FALLBACK_COUNTER = metricInstrumentRegistry.registerLongCounter("grpc.lb.wrr.rr_fallback",
"EXPERIMENTAL. Number of scheduler updates in which there were not enough endpoints "
+ "with valid weight, which caused the WRR policy to fall back to RR behavior",
"{update}", Lists.newArrayList("grpc.target"), Lists.newArrayList("grpc.lb.locality"),
false);
ENDPOINT_WEIGHT_NOT_YET_USEABLE_COUNTER = metricInstrumentRegistry.registerLongCounter(
"grpc.lb.wrr.endpoint_weight_not_yet_usable", "EXPERIMENTAL. Number of endpoints "
+ "from each scheduler update that don't yet have usable weight information",
"{endpoint}", Lists.newArrayList("grpc.target"), Lists.newArrayList("grpc.lb.locality"),
false);
ENDPOINT_WEIGHT_STALE_COUNTER = metricInstrumentRegistry.registerLongCounter(
"grpc.lb.wrr.endpoint_weight_stale",
"EXPERIMENTAL. Number of endpoints from each scheduler update whose latest weight is "
+ "older than the expiration period", "{endpoint}", Lists.newArrayList("grpc.target"),
Lists.newArrayList("grpc.lb.locality"), false);
ENDPOINT_WEIGHTS_HISTOGRAM = metricInstrumentRegistry.registerDoubleHistogram(
"grpc.lb.wrr.endpoint_weights",
"EXPERIMENTAL. The histogram buckets will be endpoint weight ranges.",
"{weight}", Lists.newArrayList(), Lists.newArrayList("grpc.target"),
Lists.newArrayList("grpc.lb.locality"),
false);
}
public WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker) {
this(helper, ticker, new Random());
}
@VisibleForTesting
WeightedRoundRobinLoadBalancer(Helper helper, Ticker ticker, Random random) {
super(OrcaOobUtil.newOrcaReportingHelper(helper));
this.ticker = checkNotNull(ticker, "ticker");
this.infTime = ticker.nanoTime() + Long.MAX_VALUE;
this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext");
this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService");
this.updateWeightTask = new UpdateWeightTask();
this.sequence = new AtomicInteger(random.nextInt());
log.log(Level.FINE, "weighted_round_robin LB created");
}
@Override
protected ChildLbState createChildLbState(Object key) {
return new WeightedChildLbState(key, pickFirstLbProvider);
}
@Override
public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
if (resolvedAddresses.getLoadBalancingPolicyConfig() == null) {
Status unavailableStatus = Status.UNAVAILABLE.withDescription(
"NameResolver returned no WeightedRoundRobinLoadBalancerConfig. addrs="
+ resolvedAddresses.getAddresses()
+ ", attrs=" + resolvedAddresses.getAttributes());
handleNameResolutionError(unavailableStatus);
return unavailableStatus;
}
String locality = resolvedAddresses.getAttributes().get(WeightedTargetLoadBalancer.CHILD_NAME);
if (locality != null) {
this.locality = locality;
} else {
this.locality = "";
}
config =
(WeightedRoundRobinLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
AcceptResolvedAddrRetVal acceptRetVal;
try {
resolvingAddresses = true;
acceptRetVal = acceptResolvedAddressesInternal(resolvedAddresses);
if (!acceptRetVal.status.isOk()) {
return acceptRetVal.status;
}
if (weightUpdateTimer != null && weightUpdateTimer.isPending()) {
weightUpdateTimer.cancel();
}
updateWeightTask.run();
createAndApplyOrcaListeners();
// Must update channel picker before return so that new RPCs will not be routed to deleted
// clusters and resolver can remove them in service config.
updateOverallBalancingState();
shutdownRemoved(acceptRetVal.removedChildren);
} finally {
resolvingAddresses = false;
}
return acceptRetVal.status;
}
/**
* Updates picker with the list of active subchannels (state == READY).
*/
@Override
protected void updateOverallBalancingState() {
List activeList = getReadyChildren();
if (activeList.isEmpty()) {
// No READY subchannels
// MultiChildLB will request connection immediately on subchannel IDLE.
boolean isConnecting = false;
for (ChildLbState childLbState : getChildLbStates()) {
ConnectivityState state = childLbState.getCurrentState();
if (state == ConnectivityState.CONNECTING || state == ConnectivityState.IDLE) {
isConnecting = true;
break;
}
}
if (isConnecting) {
updateBalancingState(
ConnectivityState.CONNECTING, new FixedResultPicker(PickResult.withNoResult()));
} else {
updateBalancingState(
ConnectivityState.TRANSIENT_FAILURE, createReadyPicker(getChildLbStates()));
}
} else {
updateBalancingState(ConnectivityState.READY, createReadyPicker(activeList));
}
}
private SubchannelPicker createReadyPicker(Collection activeList) {
WeightedRoundRobinPicker picker = new WeightedRoundRobinPicker(ImmutableList.copyOf(activeList),
config.enableOobLoadReport, config.errorUtilizationPenalty, sequence);
updateWeight(picker);
return picker;
}
private void updateWeight(WeightedRoundRobinPicker picker) {
Helper helper = getHelper();
float[] newWeights = new float[picker.children.size()];
AtomicInteger staleEndpoints = new AtomicInteger();
AtomicInteger notYetUsableEndpoints = new AtomicInteger();
for (int i = 0; i < picker.children.size(); i++) {
double newWeight = ((WeightedChildLbState) picker.children.get(i)).getWeight(staleEndpoints,
notYetUsableEndpoints);
helper.getMetricRecorder()
.recordDoubleHistogram(ENDPOINT_WEIGHTS_HISTOGRAM, newWeight,
ImmutableList.of(helper.getChannelTarget()),
ImmutableList.of(locality));
newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f;
}
if (staleEndpoints.get() > 0) {
helper.getMetricRecorder()
.addLongCounter(ENDPOINT_WEIGHT_STALE_COUNTER, staleEndpoints.get(),
ImmutableList.of(helper.getChannelTarget()),
ImmutableList.of(locality));
}
if (notYetUsableEndpoints.get() > 0) {
helper.getMetricRecorder()
.addLongCounter(ENDPOINT_WEIGHT_NOT_YET_USEABLE_COUNTER, notYetUsableEndpoints.get(),
ImmutableList.of(helper.getChannelTarget()), ImmutableList.of(locality));
}
boolean weightsEffective = picker.updateWeight(newWeights);
if (!weightsEffective) {
helper.getMetricRecorder()
.addLongCounter(RR_FALLBACK_COUNTER, 1, ImmutableList.of(helper.getChannelTarget()),
ImmutableList.of(locality));
}
}
private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) {
if (state != currentConnectivityState || !picker.equals(currentPicker)) {
getHelper().updateBalancingState(state, picker);
currentConnectivityState = state;
currentPicker = picker;
}
}
@VisibleForTesting
final class WeightedChildLbState extends ChildLbState {
private final Set subchannels = new HashSet<>();
private volatile long lastUpdated;
private volatile long nonEmptySince;
private volatile double weight = 0;
private OrcaReportListener orcaReportListener;
public WeightedChildLbState(Object key, LoadBalancerProvider policyProvider) {
super(key, policyProvider);
}
@Override
protected ChildLbStateHelper createChildHelper() {
return new WrrChildLbStateHelper();
}
private double getWeight(AtomicInteger staleEndpoints, AtomicInteger notYetUsableEndpoints) {
if (config == null) {
return 0;
}
long now = ticker.nanoTime();
if (now - lastUpdated >= config.weightExpirationPeriodNanos) {
nonEmptySince = infTime;
staleEndpoints.incrementAndGet();
return 0;
} else if (now - nonEmptySince < config.blackoutPeriodNanos
&& config.blackoutPeriodNanos > 0) {
notYetUsableEndpoints.incrementAndGet();
return 0;
} else {
return weight;
}
}
public void addSubchannel(WrrSubchannel wrrSubchannel) {
subchannels.add(wrrSubchannel);
}
public OrcaReportListener getOrCreateOrcaListener(float errorUtilizationPenalty) {
if (orcaReportListener != null
&& orcaReportListener.errorUtilizationPenalty == errorUtilizationPenalty) {
return orcaReportListener;
}
orcaReportListener = new OrcaReportListener(errorUtilizationPenalty);
return orcaReportListener;
}
public void removeSubchannel(WrrSubchannel wrrSubchannel) {
subchannels.remove(wrrSubchannel);
}
final class WrrChildLbStateHelper extends ChildLbStateHelper {
@Override
public Subchannel createSubchannel(CreateSubchannelArgs args) {
return new WrrSubchannel(super.createSubchannel(args), WeightedChildLbState.this);
}
@Override
public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
super.updateBalancingState(newState, newPicker);
if (!resolvingAddresses && newState == ConnectivityState.IDLE) {
getLb().requestConnection();
}
}
}
final class OrcaReportListener implements OrcaPerRequestReportListener, OrcaOobReportListener {
private final float errorUtilizationPenalty;
OrcaReportListener(float errorUtilizationPenalty) {
this.errorUtilizationPenalty = errorUtilizationPenalty;
}
@Override
public void onLoadReport(MetricReport report) {
double newWeight = 0;
// Prefer application utilization and fallback to CPU utilization if unset.
double utilization =
report.getApplicationUtilization() > 0 ? report.getApplicationUtilization()
: report.getCpuUtilization();
if (utilization > 0 && report.getQps() > 0) {
double penalty = 0;
if (report.getEps() > 0 && errorUtilizationPenalty > 0) {
penalty = report.getEps() / report.getQps() * errorUtilizationPenalty;
}
newWeight = report.getQps() / (utilization + penalty);
}
if (newWeight == 0) {
return;
}
if (nonEmptySince == infTime) {
nonEmptySince = ticker.nanoTime();
}
lastUpdated = ticker.nanoTime();
weight = newWeight;
}
}
}
private final class UpdateWeightTask implements Runnable {
@Override
public void run() {
if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) {
updateWeight((WeightedRoundRobinPicker) currentPicker);
}
weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos,
TimeUnit.NANOSECONDS, timeService);
}
}
private void createAndApplyOrcaListeners() {
for (ChildLbState child : getChildLbStates()) {
WeightedChildLbState wChild = (WeightedChildLbState) child;
for (WrrSubchannel weightedSubchannel : wChild.subchannels) {
if (config.enableOobLoadReport) {
OrcaOobUtil.setListener(weightedSubchannel,
wChild.getOrCreateOrcaListener(config.errorUtilizationPenalty),
OrcaOobUtil.OrcaReportingConfig.newBuilder()
.setReportInterval(config.oobReportingPeriodNanos, TimeUnit.NANOSECONDS)
.build());
} else {
OrcaOobUtil.setListener(weightedSubchannel, null, null);
}
}
}
}
@Override
public void shutdown() {
if (weightUpdateTimer != null) {
weightUpdateTimer.cancel();
}
super.shutdown();
}
@VisibleForTesting
final class WrrSubchannel extends ForwardingSubchannel {
private final Subchannel delegate;
private final WeightedChildLbState owner;
WrrSubchannel(Subchannel delegate, WeightedChildLbState owner) {
this.delegate = checkNotNull(delegate, "delegate");
this.owner = checkNotNull(owner, "owner");
}
@Override
public void start(SubchannelStateListener listener) {
owner.addSubchannel(this);
delegate().start(new SubchannelStateListener() {
@Override
public void onSubchannelState(ConnectivityStateInfo newState) {
if (newState.getState().equals(ConnectivityState.READY)) {
owner.nonEmptySince = infTime;
}
listener.onSubchannelState(newState);
}
});
}
@Override
protected Subchannel delegate() {
return delegate;
}
@Override
public void shutdown() {
super.shutdown();
owner.removeSubchannel(this);
}
}
@VisibleForTesting
static final class WeightedRoundRobinPicker extends SubchannelPicker {
// Parallel lists (column-based storage instead of normal row-based storage of List).
// The ith element of children corresponds to the ith element of pickers, listeners, and even
// updateWeight(float[]).
private final List children; // May only be accessed from sync context
private final List pickers;
private final List reportListeners;
private final boolean enableOobLoadReport;
private final float errorUtilizationPenalty;
private final AtomicInteger sequence;
private final int hashCode;
private volatile StaticStrideScheduler scheduler;
WeightedRoundRobinPicker(List children, boolean enableOobLoadReport,
float errorUtilizationPenalty, AtomicInteger sequence) {
checkNotNull(children, "children");
Preconditions.checkArgument(!children.isEmpty(), "empty child list");
this.children = children;
List pickers = new ArrayList<>(children.size());
List reportListeners = new ArrayList<>(children.size());
for (ChildLbState child : children) {
WeightedChildLbState wChild = (WeightedChildLbState) child;
pickers.add(wChild.getCurrentPicker());
reportListeners.add(wChild.getOrCreateOrcaListener(errorUtilizationPenalty));
}
this.pickers = pickers;
this.reportListeners = reportListeners;
this.enableOobLoadReport = enableOobLoadReport;
this.errorUtilizationPenalty = errorUtilizationPenalty;
this.sequence = checkNotNull(sequence, "sequence");
// For equality we treat pickers as a set; use hash code as defined by Set
int sum = 0;
for (SubchannelPicker picker : pickers) {
sum += picker.hashCode();
}
this.hashCode = sum
^ Boolean.hashCode(enableOobLoadReport)
^ Float.hashCode(errorUtilizationPenalty);
}
@Override
public PickResult pickSubchannel(PickSubchannelArgs args) {
int pick = scheduler.pick();
PickResult pickResult = pickers.get(pick).pickSubchannel(args);
Subchannel subchannel = pickResult.getSubchannel();
if (subchannel == null) {
return pickResult;
}
if (!enableOobLoadReport) {
return PickResult.withSubchannel(subchannel,
OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory(
reportListeners.get(pick)));
} else {
return PickResult.withSubchannel(subchannel);
}
}
/** Returns {@code true} if weights are different than round_robin. */
private boolean updateWeight(float[] newWeights) {
this.scheduler = new StaticStrideScheduler(newWeights, sequence);
return !this.scheduler.usesRoundRobin();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(WeightedRoundRobinPicker.class)
.add("enableOobLoadReport", enableOobLoadReport)
.add("errorUtilizationPenalty", errorUtilizationPenalty)
.add("pickers", pickers)
.toString();
}
@VisibleForTesting
List getChildren() {
return children;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof WeightedRoundRobinPicker)) {
return false;
}
WeightedRoundRobinPicker other = (WeightedRoundRobinPicker) o;
if (other == this) {
return true;
}
// the lists cannot contain duplicate subchannels
return hashCode == other.hashCode
&& sequence == other.sequence
&& enableOobLoadReport == other.enableOobLoadReport
&& Float.compare(errorUtilizationPenalty, other.errorUtilizationPenalty) == 0
&& pickers.size() == other.pickers.size()
&& new HashSet<>(pickers).containsAll(other.pickers);
}
}
/*
* The Static Stride Scheduler is an implementation of an earliest deadline first (EDF) scheduler
* in which each object's deadline is the multiplicative inverse of the object's weight.
*
* The way in which this is implemented is through a static stride scheduler.
* The Static Stride Scheduler works by iterating through the list of subchannel weights
* and using modular arithmetic to proportionally distribute picks, favoring entries
* with higher weights. It is based on the observation that the intended sequence generated
* from an EDF scheduler is a periodic one that can be achieved through modular arithmetic.
* The Static Stride Scheduler is more performant than other implementations of the EDF
* Scheduler, as it removes the need for a priority queue (and thus mutex locks).
*
* go/static-stride-scheduler
*
*
*
* - nextSequence() - O(1)
*
- pick() - O(n)
*/
@VisibleForTesting
static final class StaticStrideScheduler {
private final short[] scaledWeights;
private final AtomicInteger sequence;
private final boolean usesRoundRobin;
private static final int K_MAX_WEIGHT = 0xFFFF;
// Assuming the mean of all known weights is M, StaticStrideScheduler will clamp
// weights bigger than M*kMaxRatio and weights smaller than M*kMinRatio.
//
// This is done as a performance optimization by limiting the number of rounds for picks
// for edge cases where channels have large differences in subchannel weights.
// In this case, without these clips, it would potentially require the scheduler to
// frequently traverse through the entire subchannel list within the pick method.
//
// The current values of 10 and 0.1 were chosen without any experimenting. It should
// decrease the amount of sequences that the scheduler must traverse through in order
// to pick a high weight subchannel in such corner cases.
// But, it also makes WeightedRoundRobin to send slightly more requests to
// potentially very bad tasks (that would have near-zero weights) than zero.
// This is not necessarily a downside, though. Perhaps this is not a problem at
// all, and we can increase this value if needed to save CPU cycles.
private static final double K_MAX_RATIO = 10;
private static final double K_MIN_RATIO = 0.1;
StaticStrideScheduler(float[] weights, AtomicInteger sequence) {
checkArgument(weights.length >= 1, "Couldn't build scheduler: requires at least one weight");
int numChannels = weights.length;
int numWeightedChannels = 0;
double sumWeight = 0;
double unscaledMeanWeight;
float unscaledMaxWeight = 0;
for (float weight : weights) {
if (weight > 0) {
sumWeight += weight;
unscaledMaxWeight = Math.max(weight, unscaledMaxWeight);
numWeightedChannels++;
}
}
// Adjust max value s.t. ratio does not exceed K_MAX_RATIO. This should
// ensure that we on average do at most K_MAX_RATIO rounds for picks.
if (numWeightedChannels > 0) {
unscaledMeanWeight = sumWeight / numWeightedChannels;
unscaledMaxWeight = Math.min(unscaledMaxWeight, (float) (K_MAX_RATIO * unscaledMeanWeight));
} else {
// Fall back to round robin if all values are non-positives. Note that
// numWeightedChannels == 1 also behaves like RR because the weights are all the same, but
// the weights aren't 1, so it doesn't go through this path.
unscaledMeanWeight = 1;
unscaledMaxWeight = 1;
}
// We need at least two weights for WRR to be distinguishable from round_robin.
usesRoundRobin = numWeightedChannels < 2;
// Scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly.
// Note that, since we cap the weights to stay within K_MAX_RATIO, meanWeight might not
// match the actual mean of the values that end up in the scheduler.
double scalingFactor = K_MAX_WEIGHT / unscaledMaxWeight;
// We compute weightLowerBound and clamp it to 1 from below so that in the
// worst case, we represent tiny weights as 1.
int weightLowerBound = (int) Math.ceil(scalingFactor * unscaledMeanWeight * K_MIN_RATIO);
short[] scaledWeights = new short[numChannels];
for (int i = 0; i < numChannels; i++) {
if (weights[i] <= 0) {
scaledWeights[i] = (short) Math.round(scalingFactor * unscaledMeanWeight);
} else {
int weight = (int) Math.round(scalingFactor * Math.min(weights[i], unscaledMaxWeight));
scaledWeights[i] = (short) Math.max(weight, weightLowerBound);
}
}
this.scaledWeights = scaledWeights;
this.sequence = sequence;
}
// Without properly weighted channels, we do plain vanilla round_robin.
boolean usesRoundRobin() {
return usesRoundRobin;
}
/**
* Returns the next sequence number and atomically increases sequence with wraparound.
*/
private long nextSequence() {
return Integer.toUnsignedLong(sequence.getAndIncrement());
}
/*
* Selects index of next backend server.
*
* A 2D array is compactly represented as a function of W(backend), where the row
* represents the generation and the column represents the backend index:
* X(backend,generation) | generation ∈ [0,kMaxWeight).
* Each element in the conceptual array is a boolean indicating whether the backend at
* this index should be picked now. If false, the counter is incremented again,
* and the new element is checked. An atomically incremented counter keeps track of our
* backend and generation through modular arithmetic within the pick() method.
*
* Modular arithmetic allows us to evenly distribute picks and skips between
* generations based on W(backend).
* X(backend,generation) = (W(backend) * generation) % kMaxWeight >= kMaxWeight - W(backend)
* If we have the same three backends with weights:
* W(backend) = {2,3,6} scaled to max(W(backend)) = 6, then X(backend,generation) is:
*
* B0 B1 B2
* T T T
* F F T
* F T T
* T F T
* F T T
* F F T
* The sequence of picked backend indices is given by
* walking across and down: {0,1,2,2,1,2,0,2,1,2,2}.
*
* To reduce the variance and spread the wasted work among different picks,
* an offset that varies per backend index is also included to the calculation.
*/
int pick() {
while (true) {
long sequence = this.nextSequence();
int backendIndex = (int) (sequence % scaledWeights.length);
long generation = sequence / scaledWeights.length;
int weight = Short.toUnsignedInt(scaledWeights[backendIndex]);
long offset = (long) K_MAX_WEIGHT / 2 * backendIndex;
if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) {
continue;
}
return backendIndex;
}
}
}
static final class WeightedRoundRobinLoadBalancerConfig {
final long blackoutPeriodNanos;
final long weightExpirationPeriodNanos;
final boolean enableOobLoadReport;
final long oobReportingPeriodNanos;
final long weightUpdatePeriodNanos;
final float errorUtilizationPenalty;
public static Builder newBuilder() {
return new Builder();
}
private WeightedRoundRobinLoadBalancerConfig(long blackoutPeriodNanos,
long weightExpirationPeriodNanos,
boolean enableOobLoadReport,
long oobReportingPeriodNanos,
long weightUpdatePeriodNanos,
float errorUtilizationPenalty) {
this.blackoutPeriodNanos = blackoutPeriodNanos;
this.weightExpirationPeriodNanos = weightExpirationPeriodNanos;
this.enableOobLoadReport = enableOobLoadReport;
this.oobReportingPeriodNanos = oobReportingPeriodNanos;
this.weightUpdatePeriodNanos = weightUpdatePeriodNanos;
this.errorUtilizationPenalty = errorUtilizationPenalty;
}
static final class Builder {
long blackoutPeriodNanos = 10_000_000_000L; // 10s
long weightExpirationPeriodNanos = 180_000_000_000L; //3min
boolean enableOobLoadReport = false;
long oobReportingPeriodNanos = 10_000_000_000L; // 10s
long weightUpdatePeriodNanos = 1_000_000_000L; // 1s
float errorUtilizationPenalty = 1.0F;
private Builder() {
}
@SuppressWarnings("UnusedReturnValue")
Builder setBlackoutPeriodNanos(long blackoutPeriodNanos) {
this.blackoutPeriodNanos = blackoutPeriodNanos;
return this;
}
@SuppressWarnings("UnusedReturnValue")
Builder setWeightExpirationPeriodNanos(long weightExpirationPeriodNanos) {
this.weightExpirationPeriodNanos = weightExpirationPeriodNanos;
return this;
}
Builder setEnableOobLoadReport(boolean enableOobLoadReport) {
this.enableOobLoadReport = enableOobLoadReport;
return this;
}
Builder setOobReportingPeriodNanos(long oobReportingPeriodNanos) {
this.oobReportingPeriodNanos = oobReportingPeriodNanos;
return this;
}
Builder setWeightUpdatePeriodNanos(long weightUpdatePeriodNanos) {
this.weightUpdatePeriodNanos = weightUpdatePeriodNanos;
return this;
}
Builder setErrorUtilizationPenalty(float errorUtilizationPenalty) {
this.errorUtilizationPenalty = errorUtilizationPenalty;
return this;
}
WeightedRoundRobinLoadBalancerConfig build() {
return new WeightedRoundRobinLoadBalancerConfig(blackoutPeriodNanos,
weightExpirationPeriodNanos, enableOobLoadReport, oobReportingPeriodNanos,
weightUpdatePeriodNanos, errorUtilizationPenalty);
}
}
}
}