-
Notifications
You must be signed in to change notification settings - Fork 1.1k
chore(spanner): use channel affinity #13231
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
Open
rahul2393
wants to merge
8
commits into
main
Choose a base branch
from
use-channel-affinity
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
bee2988
chore(spanner): use channel affinity, and remove dependency on random…
rahul2393 7021df9
reformat
rahul2393 3d07803
use ChannelRef directly
rahul2393 b971db0
fix lint
rahul2393 3dfcb7e
address review comments
rahul2393 36329d8
fix tests
rahul2393 4b99a65
perf optimizations
rahul2393 9c2c0bb
add more tests
rahul2393 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,6 +107,40 @@ public class GcpManagedChannel extends ManagedChannel { | |
| public static final CallOptions.Key<Integer> CHANNEL_ID_KEY = | ||
| CallOptions.Key.create("GcpChannelId"); | ||
|
|
||
| /** CallOptions key for sticky channel routing without affinity-key map state. */ | ||
| public static final CallOptions.Key<ChannelAffinityRef> CHANNEL_AFFINITY_REF_KEY = | ||
| CallOptions.Key.create("GcpChannelAffinityRef"); | ||
|
|
||
| /** Opaque sticky channel reference for callers that should not depend on {@link ChannelRef}. */ | ||
| public static final class ChannelAffinityRef { | ||
| private static final int USE_DIFFERENT_CHANNEL_ON_NEXT_CALL_MASK = 1 << 31; | ||
| private static final int CHANNEL_ID_MASK = ~USE_DIFFERENT_CHANNEL_ON_NEXT_CALL_MASK; | ||
| private static final int NO_CHANNEL_ID = -1; | ||
|
|
||
| // Single allocation hot-path state: | ||
| // * lower 31 bits: channel id + 1, or 0 when unset. | ||
| // * high bit: use a different active channel on the next call. | ||
| private final AtomicInteger state = new AtomicInteger(); | ||
|
|
||
| /** Forces the next RPC to prefer a different active channel if one is available. */ | ||
| public void useDifferentChannelOnNextCall() { | ||
| state.getAndUpdate(value -> value | USE_DIFFERENT_CHANNEL_ON_NEXT_CALL_MASK); | ||
| } | ||
|
|
||
| private static int channelIdFromState(int state) { | ||
| int encodedChannelId = state & CHANNEL_ID_MASK; | ||
| return encodedChannelId == 0 ? NO_CHANNEL_ID : encodedChannelId - 1; | ||
| } | ||
|
|
||
| private static boolean useDifferentChannelOnNextCallFromState(int state) { | ||
| return (state & USE_DIFFERENT_CHANNEL_ON_NEXT_CALL_MASK) != 0; | ||
| } | ||
|
|
||
| private static int stateFromChannelId(int channelId) { | ||
| return (channelId + 1) & CHANNEL_ID_MASK; | ||
| } | ||
| } | ||
|
|
||
| @GuardedBy("this") | ||
| private Integer bindingIndex = -1; | ||
|
|
||
|
|
@@ -140,6 +174,7 @@ public class GcpManagedChannel extends ManagedChannel { | |
|
|
||
| // The channel pool. | ||
| @VisibleForTesting final List<ChannelRef> channelRefs = new CopyOnWriteArrayList<>(); | ||
| private final Map<Integer, ChannelRef> channelIdToChannelRef = new ConcurrentHashMap<>(); | ||
| // A set of channels that we removed from the pool and wait for their RPCs to be completed before | ||
| // we can shut them down. | ||
| final Set<ChannelRef> removedChannelRefs = new HashSet<>(); | ||
|
|
@@ -352,6 +387,7 @@ private synchronized void checkScaleDown() { | |
| channelRef.getChannel().shutdown(); | ||
| // Remove channel from broken channels map. | ||
| fallbackMap.remove(channelRef.getId()); | ||
| channelIdToChannelRef.remove(channelRef.getId()); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -372,6 +408,7 @@ private void removeOldestChannels(int num) { | |
|
|
||
| for (ChannelRef channelRef : channelsToRemove) { | ||
| channelRef.resetAffinityCount(); | ||
| channelRef.deactivate(); | ||
| if (channelRef.getState() == ConnectivityState.READY) { | ||
| decReadyChannels(false); | ||
| } | ||
|
|
@@ -1678,6 +1715,59 @@ protected ChannelRef getChannelRef(@Nullable String key) { | |
| return mappedChannel; | ||
| } | ||
|
|
||
| /** | ||
| * Pick a {@link ChannelRef} using a caller-owned reference instead of grpc-gcp's affinity map. | ||
| */ | ||
| protected ChannelRef getChannelRefByAffinityRef(ChannelAffinityRef affinityRef) { | ||
| maybeDynamicUpscale(); | ||
| // Retry if another thread updates the caller-owned affinity ref while we are picking a channel. | ||
| while (true) { | ||
|
Contributor
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. nit: can we add a clarifying comment for why we are looping here?
Contributor
Author
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. added |
||
| int state = affinityRef.state.get(); | ||
| int channelId = ChannelAffinityRef.channelIdFromState(state); | ||
| boolean useDifferentChannel = | ||
| ChannelAffinityRef.useDifferentChannelOnNextCallFromState(state); | ||
| ChannelRef channelRef = | ||
| channelId == ChannelAffinityRef.NO_CHANNEL_ID | ||
| ? null | ||
| : channelIdToChannelRef.get(channelId); | ||
| if (!useDifferentChannel && channelRef != null && channelRef.isActive()) { | ||
| return channelRef; | ||
| } | ||
|
|
||
| ChannelRef selectedChannelRef = | ||
| useDifferentChannel | ||
| ? pickLeastBusyChannelDifferentFrom(channelRef) | ||
| : pickLeastBusyChannel(/* forFallback= */ false); | ||
| if (affinityRef.state.compareAndSet( | ||
| state, ChannelAffinityRef.stateFromChannelId(selectedChannelRef.getId()))) { | ||
| return selectedChannelRef; | ||
|
rahul2393 marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
rahul2393 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private ChannelRef pickLeastBusyChannelDifferentFrom(@Nullable ChannelRef excludedChannelRef) { | ||
| ChannelRef channelRef = pickLeastBusyChannel(/* forFallback= */ false); | ||
| if (excludedChannelRef == null || channelRefs.size() <= 1) { | ||
| return channelRef; | ||
| } | ||
| if (channelRef != excludedChannelRef && channelRef.isActive()) { | ||
| return channelRef; | ||
| } | ||
| ChannelRef leastBusyChannelRef = null; | ||
| int leastBusyStreams = Integer.MAX_VALUE; | ||
| for (ChannelRef candidate : channelRefs) { | ||
| if (candidate == excludedChannelRef || !candidate.isActive()) { | ||
| continue; | ||
| } | ||
| int streams = candidate.getActiveStreamsCount(); | ||
| if (leastBusyChannelRef == null || streams < leastBusyStreams) { | ||
| leastBusyChannelRef = candidate; | ||
| leastBusyStreams = streams; | ||
| } | ||
| } | ||
|
rahul2393 marked this conversation as resolved.
|
||
| return leastBusyChannelRef == null ? channelRef : leastBusyChannelRef; | ||
| } | ||
|
|
||
| // Create a new channel and add it to channelRefs. | ||
| // If we have a ready channel not in the pool that we wait for completing its RPCs, | ||
| // then re-use that channel instead. | ||
|
|
@@ -1688,6 +1778,8 @@ ChannelRef createNewChannel() { | |
| ChannelRef chRef = reusedChannelRef.get(); | ||
| channelRefs.add(chRef); | ||
| removedChannelRefs.remove(chRef); | ||
| channelIdToChannelRef.put(chRef.getId(), chRef); | ||
| chRef.activate(); | ||
| logger.finer(log("Channel %d reused.", chRef.getId())); | ||
| incReadyChannels(false); | ||
| maxChannels.accumulateAndGet(getNumberOfChannels(), Math::max); | ||
|
|
@@ -1696,6 +1788,7 @@ ChannelRef createNewChannel() { | |
|
|
||
| ChannelRef channelRef = new ChannelRef(delegateChannelBuilder.build()); | ||
| channelRefs.add(channelRef); | ||
| channelIdToChannelRef.put(channelRef.getId(), channelRef); | ||
| logger.finer(log("Channel %d created.", channelRef.getId())); | ||
| maxChannels.accumulateAndGet(getNumberOfChannels(), Math::max); | ||
| return channelRef; | ||
|
|
@@ -1961,6 +2054,12 @@ public String authority() { | |
| @Override | ||
| public <ReqT, RespT> ClientCall<ReqT, RespT> newCall( | ||
| MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions) { | ||
| ChannelAffinityRef channelAffinityRef = callOptions.getOption(CHANNEL_AFFINITY_REF_KEY); | ||
| if (channelAffinityRef != null) { | ||
| return new GcpClientCall.SimpleGcpClientCall<>( | ||
| this, getChannelRefByAffinityRef(channelAffinityRef), methodDescriptor, callOptions); | ||
|
rahul2393 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (callOptions.getOption(DISABLE_AFFINITY_KEY) | ||
| || DISABLE_AFFINITY_CTX_KEY.get(Context.current())) { | ||
| if (logger.isLoggable(Level.FINEST)) { | ||
|
|
@@ -2314,6 +2413,7 @@ protected class ChannelRef { | |
| private final AtomicLong okCalls = new AtomicLong(); | ||
| private final AtomicLong errCalls = new AtomicLong(); | ||
| private final ChannelStateMonitor channelStateMonitor; | ||
| private volatile boolean active = true; | ||
|
|
||
| protected ChannelRef(ManagedChannel channel) { | ||
| this(channel, 0, 0); | ||
|
|
@@ -2343,6 +2443,18 @@ protected int getId() { | |
| return channelId; | ||
| } | ||
|
|
||
| protected boolean isActive() { | ||
| return active; | ||
| } | ||
|
|
||
| private void activate() { | ||
| active = true; | ||
| } | ||
|
|
||
| private void deactivate() { | ||
| active = false; | ||
| } | ||
|
|
||
| protected void affinityCountIncr() { | ||
| int count = affinityCount.incrementAndGet(); | ||
| maxAffinity.accumulateAndGet(count, Math::max); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, there are quite big changes to grpc-gcp being made in this pull request, but there are no tests that verify these changes. Can we add tests that cover the changes that we make to grpc-gcp here? Relying on tests in the Spanner client is not enough, as this is a standalone library that can be used by other clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage for the changes to the Spanner client is also quite thin, but the existing tests generally do cover these changes. One interesting test (if possible) would be a test that really verifies that all requests in a single read/write or multi-use read-only transaction really all use the same gRPC channel (so basically checking the local port where the requests are coming from on the mock server).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some more tests