Skip to content

Commit 55a3136

Browse files
committed
fix(spanner): drop sticky AFFINITY_KEY for static gRPC-GCP pool
The static-pool path bounded the affinity key to (-numChannels..numChannels) distinct values. Under a high-concurrency cold start, all keys race through GcpManagedChannel.pickLeastBusyChannel before any caller's start() has incremented activeStreamsCount, so they all tie to channelRefs[0] and bind there permanently. The result is most RPCs route through a single HTTP/2 connection and queue at MAX_CONCURRENT_STREAMS (~100), capping per-client throughput. Regression became default-path behavior in 6.105.0 (#4239). With multiplexed sessions there is no per-transaction locality benefit from sticky channel affinity. Dropping the key for the static-pool case makes GcpManagedChannel.getChannelRef(null) do a fresh per-call least-busy pick with no sticky bind, which self-corrects and matches the dynamic-pool / disableGrpcGcpExtension() throughput curve.
1 parent 15faaaa commit 55a3136

File tree

1 file changed

+18
-16
lines changed

1 file changed

+18
-16
lines changed

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,30 +2275,32 @@ <ReqT, RespT> GrpcCallContext newCallContext(
22752275
Long affinity = options == null ? null : Option.CHANNEL_HINT.getLong(options);
22762276
if (affinity != null) {
22772277
if (this.isGrpcGcpExtensionEnabled) {
2278-
// Set channel affinity in gRPC-GCP.
2279-
String affinityKey;
22802278
if (this.isDynamicChannelPoolEnabled) {
22812279
// When dynamic channel pooling is enabled, we use the raw affinity value as the key.
22822280
// This allows grpc-gcp to use round-robin for new keys, enabling new channels
22832281
// (created during scale-up) to receive requests. The affinity key lifetime setting
22842282
// ensures the affinity map doesn't grow unbounded.
2285-
affinityKey = String.valueOf(affinity);
2286-
} else {
2287-
// When DCP is disabled, compute bounded channel hint to prevent
2288-
// gRPC-GCP affinity map from getting unbounded.
2289-
int boundedChannelHint = affinity.intValue() % this.numChannels;
2290-
affinityKey = String.valueOf(boundedChannelHint);
2291-
}
2292-
context =
2293-
context.withCallOptions(
2294-
context.getCallOptions().withOption(GcpManagedChannel.AFFINITY_KEY, affinityKey));
2295-
// Check if the caller wants to unbind the affinity key after this call completes.
2296-
Boolean unbind = Option.UNBIND_CHANNEL_HINT.get(options);
2297-
if (Boolean.TRUE.equals(unbind)) {
2283+
String affinityKey = String.valueOf(affinity);
22982284
context =
22992285
context.withCallOptions(
2300-
context.getCallOptions().withOption(GcpManagedChannel.UNBIND_AFFINITY_KEY, true));
2286+
context.getCallOptions().withOption(GcpManagedChannel.AFFINITY_KEY, affinityKey));
2287+
// Check if the caller wants to unbind the affinity key after this call completes.
2288+
Boolean unbind = Option.UNBIND_CHANNEL_HINT.get(options);
2289+
if (Boolean.TRUE.equals(unbind)) {
2290+
context =
2291+
context.withCallOptions(
2292+
context
2293+
.getCallOptions()
2294+
.withOption(GcpManagedChannel.UNBIND_AFFINITY_KEY, true));
2295+
}
23012296
}
2297+
// Static pool: do not set AFFINITY_KEY. Multiplexed sessions get no locality
2298+
// benefit from sticky per-transaction channel affinity, and bounding the key
2299+
// to numChannels distinct values caused those keys to permanently bind to a
2300+
// single channel under concurrent first-use (GcpManagedChannel.pickLeastBusyChannel
2301+
// reads activeStreamsCount before any caller's start() has incremented it, so a
2302+
// warmup burst all ties to channelRefs[0]). With no key, getChannelRef(null) does
2303+
// a fresh per-call least-busy pick with no sticky bind, which self-corrects.
23022304
} else {
23032305
// Set channel affinity in GAX.
23042306
context = context.withChannelAffinity(affinity.intValue());

0 commit comments

Comments
 (0)