-
Notifications
You must be signed in to change notification settings - Fork 4k
implemented and tested static stride scheduler for weighted round robin load balancing policy #10272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implemented and tested static stride scheduler for weighted round robin load balancing policy #10272
Changes from 37 commits
b5cf7b0
44a5158
32973d4
4bde79d
d99d51a
13a9fd8
3d9e625
2b054d3
4addda3
4a820a7
dc7960a
acd3425
ba1a0b7
9f4a60d
e11e542
c76cbe5
903d2ac
d5a0629
5982115
88a8e48
f9cae20
dba0778
46a463e
142b499
5523337
365ba8d
442bea8
2a0e489
1731862
5e5127f
f0421d2
ec46fe3
8ddf284
347b46f
4a4762e
f526bf6
21ceb85
03de3f9
e2eb7f9
4072907
f749e52
be21c23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,10 +44,10 @@ | |
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.PriorityQueue; | ||
| import java.util.Random; | ||
| 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; | ||
|
|
||
|
|
@@ -120,7 +120,7 @@ private final class UpdateWeightTask implements Runnable { | |
| @Override | ||
| public void run() { | ||
| if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { | ||
| ((WeightedRoundRobinPicker)currentPicker).updateWeight(); | ||
| ((WeightedRoundRobinPicker) currentPicker).updateWeight(); | ||
| } | ||
| weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, | ||
| TimeUnit.NANOSECONDS, timeService); | ||
|
|
@@ -258,7 +258,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { | |
| new HashMap<>(); | ||
| private final boolean enableOobLoadReport; | ||
| private final float errorUtilizationPenalty; | ||
| private volatile EdfScheduler scheduler; | ||
| private volatile StaticStrideScheduler scheduler; | ||
|
|
||
| WeightedRoundRobinPicker(List<Subchannel> list, boolean enableOobLoadReport, | ||
| float errorUtilizationPenalty) { | ||
|
|
@@ -279,7 +279,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { | |
| Subchannel subchannel = list.get(scheduler.pick()); | ||
| if (!enableOobLoadReport) { | ||
| return PickResult.withSubchannel(subchannel, | ||
| OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( | ||
| OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( | ||
| subchannelToReportListenerMap.getOrDefault(subchannel, | ||
| ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); | ||
| } else { | ||
|
|
@@ -288,26 +288,14 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { | |
| } | ||
|
|
||
| private void updateWeight() { | ||
| int weightedChannelCount = 0; | ||
| double avgWeight = 0; | ||
| for (Subchannel value : list) { | ||
| double newWeight = ((WrrSubchannel) value).getWeight(); | ||
| if (newWeight > 0) { | ||
| avgWeight += newWeight; | ||
| weightedChannelCount++; | ||
| } | ||
| } | ||
| EdfScheduler scheduler = new EdfScheduler(list.size(), random); | ||
| if (weightedChannelCount >= 1) { | ||
| avgWeight /= 1.0 * weightedChannelCount; | ||
| } else { | ||
| avgWeight = 1; | ||
| } | ||
| float[] newWeights = new float[list.size()]; | ||
| for (int i = 0; i < list.size(); i++) { | ||
| WrrSubchannel subchannel = (WrrSubchannel) list.get(i); | ||
| double newWeight = subchannel.getWeight(); | ||
| scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); | ||
| newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; | ||
| } | ||
|
|
||
| StaticStrideScheduler scheduler = new StaticStrideScheduler(newWeights, random); | ||
| this.scheduler = scheduler; | ||
| } | ||
|
|
||
|
|
@@ -340,111 +328,100 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * The earliest deadline first implementation in which each object is | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * chosen deterministically and periodically with frequency proportional to its weight. | ||
| * | ||
| * <p>Specifically, each object added to chooser is given a deadline equal to the multiplicative | ||
| * inverse of its weight. The place of each object in its deadline is tracked, and each call to | ||
| * choose returns the object with the least remaining time in its deadline. | ||
| * (Ties are broken by the order in which the children were added to the chooser.) The deadline | ||
| * advances by the multiplicative inverse of the object's weight. | ||
| * For example, if items A and B are added with weights 0.5 and 0.2, successive chooses return: | ||
| /* | ||
| * Implementation of Static Stride Scheduler, replaces EDFScheduler. | ||
ejona86 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * <p> | ||
| * The Static Stride Scheduler works by iterating through the list of subchannel weights | ||
| * and using modular arithmetic to evenly distribute picks and skips, favoring entries with the | ||
| * highest weight. It generates a practically equivalent sequence of picks as the EDFScheduler. | ||
| * Albeit needing more bandwidth, the Static Stride Scheduler is more performant than the | ||
| * EDFScheduler, as it removes the need for a priority queue (and thus mutex locks). | ||
| * <p> | ||
| * go/static-stride-scheduler | ||
| * <p> | ||
| * | ||
| * <ul> | ||
| * <li>In the first call, the deadlines are A=2 (1/0.5) and B=5 (1/0.2), so A is returned. | ||
| * The deadline of A is updated to 4. | ||
| * <li>Next, the remaining deadlines are A=4 and B=5, so A is returned. The deadline of A (2) is | ||
| * updated to A=6. | ||
| * <li>Remaining deadlines are A=6 and B=5, so B is returned. The deadline of B is updated with | ||
| * with B=10. | ||
| * <li>Remaining deadlines are A=6 and B=10, so A is returned. The deadline of A is updated with | ||
| * A=8. | ||
| * <li>Remaining deadlines are A=8 and B=10, so A is returned. The deadline of A is updated with | ||
| * A=10. | ||
| * <li>Remaining deadlines are A=10 and B=10, so A is returned. The deadline of A is updated | ||
| * with A=12. | ||
| * <li>Remaining deadlines are A=12 and B=10, so B is returned. The deadline of B is updated | ||
| * with B=15. | ||
| * <li>etc. | ||
| * </ul> | ||
| * | ||
| * <p>In short: the entry with the highest weight is preferred. | ||
| * | ||
| * <ul> | ||
| * <li>add() - O(lg n) | ||
| * <li>pick() - O(lg n) | ||
| * </ul> | ||
| * | ||
| * <li>nextSequence() - O(1) | ||
| * <li>pick() - O(n) | ||
| */ | ||
| @VisibleForTesting | ||
| static final class EdfScheduler { | ||
| private final PriorityQueue<ObjectState> prioQueue; | ||
|
|
||
| /** | ||
| * Weights below this value will be upped to this minimum weight. | ||
| */ | ||
| private static final double MINIMUM_WEIGHT = 0.0001; | ||
|
|
||
| private final Object lock = new Object(); | ||
| static final class StaticStrideScheduler { | ||
YifeiZhuang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| private final int[] scaledWeights; | ||
| private final int sizeDivisor; | ||
| private final AtomicInteger sequence; | ||
| private static final int K_MAX_WEIGHT = 0xFFFF; | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| StaticStrideScheduler(float[] weights, Random random) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do it in a later PR (by you or someone else), but we will want to replace this Random with |
||
| checkArgument(weights.length >= 1, "Couldn't build scheduler: requires at least one weight"); | ||
| int numChannels = weights.length; | ||
| int numWeightedChannels = 0; | ||
| double sumWeight = 0; | ||
| float maxWeight = 0; | ||
| int meanWeight = 0; | ||
| for (float weight : weights) { | ||
| if (weight > 0.0001) { | ||
YifeiZhuang marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| sumWeight += weight; | ||
| maxWeight = Math.max(weight, maxWeight); | ||
| numWeightedChannels++; | ||
| } | ||
| } | ||
|
|
||
| private final Random random; | ||
| double scalingFactor = K_MAX_WEIGHT / maxWeight; | ||
| if (numWeightedChannels > 0) { | ||
| meanWeight = (int) Math.round(scalingFactor * sumWeight / numWeightedChannels); | ||
| } else { | ||
| meanWeight = 1; | ||
| } | ||
|
|
||
| /** | ||
| * Use the item's deadline as the order in the priority queue. If the deadlines are the same, | ||
| * use the index. Index should be unique. | ||
| */ | ||
| EdfScheduler(int initialCapacity, Random random) { | ||
| this.prioQueue = new PriorityQueue<ObjectState>(initialCapacity, (o1, o2) -> { | ||
| if (o1.deadline == o2.deadline) { | ||
| return Integer.compare(o1.index, o2.index); | ||
| // scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly | ||
| int[] scaledWeights = new int[numChannels]; | ||
| for (int i = 0; i < numChannels; i++) { | ||
| if (weights[i] < 0.0001) { | ||
|
||
| scaledWeights[i] = meanWeight; | ||
| } else { | ||
| return Double.compare(o1.deadline, o2.deadline); | ||
| scaledWeights[i] = (int) Math.round(weights[i] * scalingFactor); | ||
| } | ||
| }); | ||
| this.random = random; | ||
| } | ||
|
|
||
| this.scaledWeights = scaledWeights; | ||
| this.sizeDivisor = numChannels; | ||
| this.sequence = new AtomicInteger(random.nextInt()); | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Adds the item in the scheduler. This is not thread safe. | ||
| * | ||
| * @param index The field {@link ObjectState#index} to be added | ||
| * @param weight positive weight for the added object | ||
| */ | ||
| void add(int index, double weight) { | ||
| checkArgument(weight > 0.0, "Weights need to be positive."); | ||
| ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); | ||
| // Randomize the initial deadline. | ||
| state.deadline = random.nextDouble() * (1 / state.weight); | ||
| prioQueue.add(state); | ||
| /** Returns the next sequence number and atomically increases sequence with wraparound. */ | ||
| private long nextSequence() { | ||
| return Integer.toUnsignedLong(sequence.getAndIncrement()); | ||
| } | ||
|
|
||
| /** | ||
| * Picks the next WRR object. | ||
| public long getSequence() { | ||
tonyjongyoonan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return Integer.toUnsignedLong(sequence.get()); | ||
| } | ||
|
|
||
| /* | ||
| * Selects index of next backend server. | ||
| * <p> | ||
| * A 2D array is compactly represented where the row represents the generation and the column | ||
| * represents the backend index. The value of an element is a boolean value which indicates | ||
| * whether or not a backend should be picked now. An atomically incremented counter keeps track | ||
| * of our backend and generation through modular arithmetic within the pick() method. | ||
| * An offset is also included to minimize consecutive non-picks of a backend. | ||
| */ | ||
| int pick() { | ||
| synchronized (lock) { | ||
| ObjectState minObject = prioQueue.remove(); | ||
| minObject.deadline += 1.0 / minObject.weight; | ||
| prioQueue.add(minObject); | ||
| return minObject.index; | ||
| while (true) { | ||
| long sequence = this.nextSequence(); | ||
| int backendIndex = (int) (sequence % this.sizeDivisor); | ||
| long generation = sequence / this.sizeDivisor; | ||
| long weight = this.scaledWeights[backendIndex]; | ||
| long offset = (long) K_MAX_WEIGHT / 2 * backendIndex; | ||
| if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { | ||
| continue; | ||
| } | ||
| return backendIndex; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Holds the state of the object. */ | ||
| @VisibleForTesting | ||
| static class ObjectState { | ||
| private final double weight; | ||
| private final int index; | ||
| private volatile double deadline; | ||
|
|
||
| ObjectState(double weight, int index) { | ||
| this.weight = weight; | ||
| this.index = index; | ||
| } | ||
| } | ||
|
|
||
| static final class WeightedRoundRobinLoadBalancerConfig { | ||
| final long blackoutPeriodNanos; | ||
| final long weightExpirationPeriodNanos; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.