Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 78370e0

Browse files
committed
feat: Load balancing options for BigtableChannelPool
1 parent 70c05c9 commit 78370e0

3 files changed

Lines changed: 158 additions & 15 deletions

File tree

google-cloud-bigtable/clirr-ignored-differences.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,4 +456,16 @@
456456
<method>*sendPrimeRequestsAsync*</method>
457457
<to>com.google.api.core.ApiFuture</to>
458458
</difference>
459+
<difference>
460+
<!-- InternalApi was updated -->
461+
<differenceType>7013</differenceType>
462+
<className>com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings</className>
463+
<method>com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings$LoadBalancingStrategy getLoadBalancingStrategy()</method>
464+
</difference>
465+
<difference>
466+
<!-- InternalApi was updated -->
467+
<differenceType>7013</differenceType>
468+
<className>com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings$Builder</className>
469+
<method>com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings$Builder setLoadBalancingStrategy(com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings$LoadBalancingStrategy)</method>
470+
</difference>
459471
</differences>

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import java.io.IOException;
3434
import java.time.Clock;
3535
import java.util.ArrayList;
36+
import java.util.function.Supplier;
3637
import java.util.List;
38+
import java.util.Random;
3739
import java.util.concurrent.CancellationException;
3840
import java.util.concurrent.ConcurrentLinkedQueue;
3941
import java.util.concurrent.Executors;
@@ -71,6 +73,8 @@ public class BigtableChannelPool extends ManagedChannel {
7173
private final ChannelPoolHealthChecker channelPoolHealthChecker;
7274
private final AtomicInteger indexTicker = new AtomicInteger();
7375
private final String authority;
76+
private final Random rng = new Random();
77+
private final Supplier<Integer> picker;
7478

7579
public static BigtableChannelPool create(
7680
BigtableChannelPoolSettings settings,
@@ -113,6 +117,25 @@ public static BigtableChannelPool create(
113117

114118
entries.set(initialListBuilder.build());
115119
authority = entries.get().get(0).channel.authority();
120+
121+
switch (settings.getLoadBalancingStrategy()) {
122+
case ROUND_ROBIN:
123+
picker = this::pickEntryIndexRoundRobin;
124+
break;
125+
case LEAST_IN_FLIGHT:
126+
picker = this::pickEntryIndexLeastInFlight;
127+
break;
128+
case POWER_OF_TWO_LEAST_IN_FLIGHT:
129+
picker = this::pickEntryIndexPowerOfTwoLeastInFlight;
130+
break;
131+
default:
132+
LOG.warning(String.format(
133+
"Unknown load balancing strategy %s, falling back to ROUND_ROBIN.",
134+
settings.getLoadBalancingStrategy()));
135+
picker = this::pickEntryIndexRoundRobin;
136+
137+
}
138+
116139
this.executor = executor;
117140

118141
if (!settings.isStaticSize()) {
@@ -138,19 +161,74 @@ public String authority() {
138161
}
139162

140163
/**
141-
* Create a {@link ClientCall} on a Channel from the pool chosen in a round-robin fashion to the
142-
* remote operation specified by the given {@link MethodDescriptor}. The returned {@link
143-
* ClientCall} does not trigger any remote behavior until {@link
144-
* ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
164+
* Create a {@link ClientCall} on a Channel from the pool to the remote operation specified by the
165+
* given {@link MethodDescriptor}. The returned {@link ClientCall} does not trigger any remote
166+
* behavior until {@link ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
145167
*/
146168
@Override
147169
public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
148170
MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions) {
149-
return getChannel(indexTicker.getAndIncrement()).newCall(methodDescriptor, callOptions);
171+
return new AffinityChannel(pickEntryIndex()).newCall(methodDescriptor, callOptions);
150172
}
151173

152-
Channel getChannel(int affinity) {
153-
return new AffinityChannel(affinity);
174+
/**
175+
* Pick the index of an entry to use for the next call. The returned value *should* be within
176+
* range, but callers should not assume that this is always the case as race conditions are
177+
* possible.
178+
*/
179+
private int pickEntryIndex() {
180+
return picker.get();
181+
}
182+
183+
/** Pick an entry using the Round Robin algorithm. */
184+
private int pickEntryIndexRoundRobin() {
185+
return Math.abs(indexTicker.getAndIncrement() % entries.get().size());
186+
}
187+
188+
/** Pick an entry at random. */
189+
private int pickEntryIndexRandom() {
190+
return rng.nextInt(entries.get().size());
191+
}
192+
193+
/** Pick an entry using the least-in-flight algorithm. */
194+
private int pickEntryIndexLeastInFlight() {
195+
List<Entry> localEntries = entries.get();
196+
int minRpcs = Integer.MAX_VALUE;
197+
List<Integer> candidates = new ArrayList<>();
198+
199+
for (int i = 0; i < localEntries.size(); i++) {
200+
Entry entry = localEntries.get(i);
201+
int rpcs = entry.outstandingRpcs.get();
202+
if (rpcs < minRpcs) {
203+
minRpcs = rpcs;
204+
candidates.clear();
205+
candidates.add(i);
206+
} else if (rpcs == minRpcs) {
207+
candidates.add(i);
208+
}
209+
}
210+
// If there are multiple matching entries, pick one at random.
211+
return candidates.get(rng.nextInt(candidates.size()));
212+
}
213+
214+
/** Pick an entry using the power-of-two algorithm. */
215+
private int pickEntryIndexPowerOfTwoLeastInFlight() {
216+
List<Entry> localEntries = entries.get();
217+
int choice1 = pickEntryIndexRandom();
218+
int choice2 = pickEntryIndexRandom();
219+
if (choice1 == choice2) {
220+
// Try to pick two different entries. If this picks the same entry again, it's likely that
221+
// there's only one healthy channel in the pool and we should proceed anyway.
222+
choice2 = pickEntryIndexRandom();
223+
}
224+
225+
Entry entry1 = localEntries.get(choice1);
226+
Entry entry2 = localEntries.get(choice2);
227+
return entry1.outstandingRpcs.get() < entry2.outstandingRpcs.get() ? choice1 : choice2;
228+
}
229+
230+
Channel getChannel(int index) {
231+
return new AffinityChannel(index);
154232
}
155233

156234
/** {@inheritDoc} */
@@ -395,7 +473,9 @@ void refresh() {
395473
* Get and retain a Channel Entry. The returned Entry will have its rpc count incremented,
396474
* preventing it from getting recycled.
397475
*/
398-
Entry getRetainedEntry(int affinity) {
476+
private Entry getRetainedEntry(int affinity) {
477+
// If an entry is not retainable, that usually means that it's about to be replaced and if we
478+
// retry we should get a new useable entry.
399479
// The maximum number of concurrent calls to this method for any given time span is at most 2,
400480
// so the loop can actually be 2 times. But going for 5 times for a safety margin for potential
401481
// code evolving
@@ -543,10 +623,10 @@ private void shutdown() {
543623

544624
/** Thin wrapper to ensure that new calls are properly reference counted. */
545625
private class AffinityChannel extends Channel {
546-
private final int affinity;
626+
private final int index;
547627

548-
public AffinityChannel(int affinity) {
549-
this.affinity = affinity;
628+
public AffinityChannel(int index) {
629+
this.index = index;
550630
}
551631

552632
@Override
@@ -557,9 +637,7 @@ public String authority() {
557637
@Override
558638
public <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
559639
MethodDescriptor<RequestT, ResponseT> methodDescriptor, CallOptions callOptions) {
560-
561-
Entry entry = getRetainedEntry(affinity);
562-
640+
Entry entry = getRetainedEntry(index);
563641
return new ReleasingClientCall<>(entry.channel.newCall(methodDescriptor, callOptions), entry);
564642
}
565643
}

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
package com.google.cloud.bigtable.gaxx.grpc;
1717

1818
import com.google.api.core.BetaApi;
19+
import com.google.api.core.InternalApi;
1920
import com.google.api.gax.grpc.ChannelPoolSettings;
2021
import com.google.auto.value.AutoValue;
22+
import com.google.common.annotations.VisibleForTesting;
2123
import com.google.common.base.Preconditions;
24+
import com.google.common.base.Strings;
25+
import java.util.logging.Logger;
2226
import java.time.Duration;
2327

2428
/**
@@ -41,12 +45,33 @@
4145
@BetaApi("surface for channel pool sizing is not yet stable")
4246
@AutoValue
4347
public abstract class BigtableChannelPoolSettings {
48+
@VisibleForTesting
49+
static final Logger LOG = Logger.getLogger(BigtableChannelPoolSettings.class.getName());
50+
4451
/** How often to check and possibly resize the {@link BigtableChannelPool}. */
4552
static final Duration RESIZE_INTERVAL = Duration.ofMinutes(1);
4653

4754
/** The maximum number of channels that can be added or removed at a time. */
4855
static final int MAX_RESIZE_DELTA = 2;
4956

57+
/** Environment variable used to set load balancing strategy. */
58+
private static final String CBT_LOAD_BALANCING_STRATEGY_ENV_VAR = "CBT_LOAD_BALANCING_STRATEGY";
59+
60+
/** Load balancing strategy to use if environment variable is unset or invalid. */
61+
private static final LoadBalancingStrategy DEFAULT_LOAD_BALANCING_STRATEGY =
62+
LoadBalancingStrategy.ROUND_ROBIN;
63+
64+
/** Supported load-balancing strategies. */
65+
public enum LoadBalancingStrategy {
66+
// Sequentially iterate across all channels.
67+
ROUND_ROBIN,
68+
// Pick the channel with the fewest in-flight requests. If multiple channels match, pick at
69+
// random.
70+
LEAST_IN_FLIGHT,
71+
// Out of two random channels, pick the channel with the fewest in-flight requests.
72+
POWER_OF_TWO_LEAST_IN_FLIGHT,
73+
}
74+
5075
/**
5176
* Threshold to start scaling down the channel pool.
5277
*
@@ -95,6 +120,10 @@ public abstract class BigtableChannelPoolSettings {
95120
*/
96121
public abstract boolean isPreemptiveRefreshEnabled();
97122

123+
/** The load balancing strategy to use for distributing RPCs across channels. */
124+
@InternalApi("Use CBT_LOAD_BALANCING_STRATEGY environment variable")
125+
public abstract LoadBalancingStrategy getLoadBalancingStrategy();
126+
98127
/**
99128
* Helper to check if the {@link BigtableChannelPool} implementation can skip dynamic size logic
100129
*/
@@ -111,6 +140,24 @@ boolean isStaticSize() {
111140
return false;
112141
}
113142

143+
/**
144+
* Use environment variable CBT_LOAD_BALANCING_STRATEGY to pick a load-balancing strategy.
145+
* @return load-balancing strategy to use.
146+
*/
147+
private static LoadBalancingStrategy loadBalancingStrategyFromEnv() {
148+
String strategyString = System.getenv(CBT_LOAD_BALANCING_STRATEGY_ENV_VAR);
149+
if (Strings.isNullOrEmpty(strategyString)) {
150+
return DEFAULT_LOAD_BALANCING_STRATEGY;
151+
}
152+
try {
153+
return LoadBalancingStrategy.valueOf(strategyString.trim().toUpperCase());
154+
} catch (IllegalArgumentException e) {
155+
LOG.warning(String.format("Invalid load-balancing strategy %s, falling back to default %s.",
156+
strategyString, DEFAULT_LOAD_BALANCING_STRATEGY));
157+
return DEFAULT_LOAD_BALANCING_STRATEGY;
158+
}
159+
}
160+
114161
public abstract Builder toBuilder();
115162

116163
public static BigtableChannelPoolSettings copyFrom(ChannelPoolSettings externalSettings) {
@@ -121,6 +168,7 @@ public static BigtableChannelPoolSettings copyFrom(ChannelPoolSettings externalS
121168
.setMaxChannelCount(externalSettings.getMaxChannelCount())
122169
.setInitialChannelCount(externalSettings.getInitialChannelCount())
123170
.setPreemptiveRefreshEnabled(externalSettings.isPreemptiveRefreshEnabled())
171+
.setLoadBalancingStrategy(loadBalancingStrategyFromEnv())
124172
.build();
125173
}
126174

@@ -131,6 +179,7 @@ public static BigtableChannelPoolSettings staticallySized(int size) {
131179
.setMaxRpcsPerChannel(Integer.MAX_VALUE)
132180
.setMinChannelCount(size)
133181
.setMaxChannelCount(size)
182+
.setLoadBalancingStrategy(loadBalancingStrategyFromEnv())
134183
.build();
135184
}
136185

@@ -141,7 +190,8 @@ public static Builder builder() {
141190
.setMaxChannelCount(200)
142191
.setMinRpcsPerChannel(0)
143192
.setMaxRpcsPerChannel(Integer.MAX_VALUE)
144-
.setPreemptiveRefreshEnabled(false);
193+
.setPreemptiveRefreshEnabled(false)
194+
.setLoadBalancingStrategy(loadBalancingStrategyFromEnv());
145195
}
146196

147197
@AutoValue.Builder
@@ -158,6 +208,9 @@ public abstract static class Builder {
158208

159209
public abstract Builder setPreemptiveRefreshEnabled(boolean enabled);
160210

211+
@InternalApi("Use CBT_LOAD_BALANCING_STRATEGY environment variable")
212+
public abstract Builder setLoadBalancingStrategy(LoadBalancingStrategy strategy);
213+
161214
abstract BigtableChannelPoolSettings autoBuild();
162215

163216
public BigtableChannelPoolSettings build() {

0 commit comments

Comments
 (0)