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

Commit 74c14f5

Browse files
committed
feat: Load balancing options for BigtableChannelPool
1 parent 70c05c9 commit 74c14f5

3 files changed

Lines changed: 154 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: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.time.Clock;
3535
import java.util.ArrayList;
3636
import java.util.List;
37+
import java.util.Random;
3738
import java.util.concurrent.CancellationException;
3839
import java.util.concurrent.ConcurrentLinkedQueue;
3940
import java.util.concurrent.Executors;
@@ -71,6 +72,7 @@ public class BigtableChannelPool extends ManagedChannel {
7172
private final ChannelPoolHealthChecker channelPoolHealthChecker;
7273
private final AtomicInteger indexTicker = new AtomicInteger();
7374
private final String authority;
75+
private final Random rng = new Random();
7476

7577
public static BigtableChannelPool create(
7678
BigtableChannelPoolSettings settings,
@@ -138,19 +140,92 @@ public String authority() {
138140
}
139141

140142
/**
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.
143+
* Create a {@link ClientCall} on a Channel from the pool to the remote operation specified by the
144+
* given {@link MethodDescriptor}. The returned {@link ClientCall} does not trigger any remote
145+
* behavior until {@link ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
145146
*/
146147
@Override
147148
public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
148149
MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions) {
149-
return getChannel(indexTicker.getAndIncrement()).newCall(methodDescriptor, callOptions);
150+
return new AffinityChannel(pickEntryIndex()).newCall(methodDescriptor, callOptions);
150151
}
151152

152-
Channel getChannel(int affinity) {
153-
return new AffinityChannel(affinity);
153+
/**
154+
* Pick the index of an entry to use for the next call. The returned value *should* be within
155+
* range, but callers should not assume that this is always the case as race conditions are
156+
* possible.
157+
*/
158+
private int pickEntryIndex() {
159+
switch (settings.getLoadBalancingStrategy()) {
160+
case ROUND_ROBIN:
161+
return pickEntryIndexRoundRobin();
162+
case LEAST_IN_FLIGHT:
163+
return pickEntryIndexLeastInFlight();
164+
case POWER_OF_TWO_LEAST_IN_FLIGHT:
165+
return pickEntryIndexPowerOfTwoLeastInFlight();
166+
default:
167+
LOG.warning(String.format(
168+
"Unknown load balancing strategy %s, falling back to ROUND_ROBIN.",
169+
settings.getLoadBalancingStrategy()));
170+
return pickEntryIndexRoundRobin();
171+
172+
}
173+
}
174+
175+
/** Pick an entry using the Round Robin algorithm. */
176+
private int pickEntryIndexRoundRobin() {
177+
return Math.abs(indexTicker.getAndIncrement() % entries.get().size());
178+
}
179+
180+
/** Pick an entry at random. */
181+
private int pickEntryIndexRandom() {
182+
return rng.nextInt(entries.get().size());
183+
}
184+
185+
/** Pick an entry using the least-in-flight algorithm. */
186+
private int pickEntryIndexLeastInFlight() {
187+
List<Entry> localEntries = entries.get();
188+
int minRpcs = Integer.MAX_VALUE;
189+
List<Integer> candidates = new ArrayList<>();
190+
191+
for (int i = 0; i < localEntries.size(); i++) {
192+
Entry entry = localEntries.get(i);
193+
int rpcs = entry.outstandingRpcs.get();
194+
if (rpcs < minRpcs) {
195+
minRpcs = rpcs;
196+
candidates.clear();
197+
candidates.add(i);
198+
} else if (rpcs == minRpcs) {
199+
candidates.add(i);
200+
}
201+
}
202+
if (candidates.isEmpty()) {
203+
LOG.warning(
204+
"Least-in-flight picker couldn't find available channel. Picking at random.");
205+
return pickEntryIndexRandom();
206+
}
207+
// If there are multiple matching entries, pick one at random.
208+
return candidates.get(rng.nextInt(candidates.size()));
209+
}
210+
211+
/** Pick an entry using the power-of-two algorithm. */
212+
private int pickEntryIndexPowerOfTwoLeastInFlight() {
213+
List<Entry> localEntries = entries.get();
214+
int choice1 = pickEntryIndexRandom();
215+
int choice2 = pickEntryIndexRandom();
216+
if (choice1 == choice2) {
217+
// Try to pick two different entries. If this picks the same entry again, it's likely that
218+
// there's only one healthy channel in the pool and we should proceed anyway.
219+
choice2 = pickEntryIndexRandom();
220+
}
221+
222+
Entry entry1 = localEntries.get(choice1);
223+
Entry entry2 = localEntries.get(choice2);
224+
return entry1.outstandingRpcs.get() < entry2.outstandingRpcs.get() ? choice1 : choice2;
225+
}
226+
227+
Channel getChannel(int index) {
228+
return new AffinityChannel(index);
154229
}
155230

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

544621
/** Thin wrapper to ensure that new calls are properly reference counted. */
545622
private class AffinityChannel extends Channel {
546-
private final int affinity;
623+
private final int index;
547624

548-
public AffinityChannel(int affinity) {
549-
this.affinity = affinity;
625+
public AffinityChannel(int index) {
626+
this.index = index;
550627
}
551628

552629
@Override
@@ -557,9 +634,7 @@ public String authority() {
557634
@Override
558635
public <RequestT, ResponseT> ClientCall<RequestT, ResponseT> newCall(
559636
MethodDescriptor<RequestT, ResponseT> methodDescriptor, CallOptions callOptions) {
560-
561-
Entry entry = getRetainedEntry(affinity);
562-
637+
Entry entry = getRetainedEntry(index);
563638
return new ReleasingClientCall<>(entry.channel.newCall(methodDescriptor, callOptions), entry);
564639
}
565640
}

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

Lines changed: 53 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,8 @@ public abstract static class Builder {
158208

159209
public abstract Builder setPreemptiveRefreshEnabled(boolean enabled);
160210

211+
public abstract Builder setLoadBalancingStrategy(LoadBalancingStrategy strategy);
212+
161213
abstract BigtableChannelPoolSettings autoBuild();
162214

163215
public BigtableChannelPoolSettings build() {

0 commit comments

Comments
 (0)