All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.grpc.xds.LeastRequestLoadBalancer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021 The gRPC Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.grpc.xds;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.ConnectivityState.CONNECTING;
import static io.grpc.ConnectivityState.IDLE;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
import static io.grpc.xds.LeastRequestLoadBalancerProvider.DEFAULT_CHOICE_COUNT;
import static io.grpc.xds.LeastRequestLoadBalancerProvider.MAX_CHOICE_COUNT;
import static io.grpc.xds.LeastRequestLoadBalancerProvider.MIN_CHOICE_COUNT;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import io.grpc.Attributes;
import io.grpc.ClientStreamTracer;
import io.grpc.ClientStreamTracer.StreamInfo;
import io.grpc.ConnectivityState;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancerProvider;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.util.MultiChildLoadBalancer;
import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A {@link LoadBalancer} that provides least request load balancing based on
 * outstanding request counters.
 * It works by sampling a number of subchannels and picking the one with the
 * fewest amount of outstanding requests.
 * The default sampling amount of two is also known as
 * the "power of two choices" (P2C).
 */
final class LeastRequestLoadBalancer extends MultiChildLoadBalancer {
  private final ThreadSafeRandom random;

  private SubchannelPicker currentPicker = new EmptyPicker();
  private int choiceCount = DEFAULT_CHOICE_COUNT;

  LeastRequestLoadBalancer(Helper helper) {
    this(helper, ThreadSafeRandomImpl.instance);
  }

  @VisibleForTesting
  LeastRequestLoadBalancer(Helper helper, ThreadSafeRandom random) {
    super(helper);
    this.random = checkNotNull(random, "random");
  }

  @Override
  public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
    // Need to update choiceCount before calling super so that the updateBalancingState call has the
    // new value.  However, if the update fails we need to revert it.
    int oldChoiceCount = choiceCount;
    LeastRequestConfig config =
        (LeastRequestConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
    if (config != null) {
      choiceCount = config.choiceCount;
    }

    Status addressAcceptanceStatus = super.acceptResolvedAddresses(resolvedAddresses);

    if (!addressAcceptanceStatus.isOk()) {
      choiceCount = oldChoiceCount;
    }

    return addressAcceptanceStatus;
  }

  /**
   * Updates picker with the list of active subchannels (state == READY).
   *
   *  

* If no active subchannels exist, but some are in TRANSIENT_FAILURE then returns a picker * with all of the children in TF so that the application code will get an error from a varying * random one when it tries to get a subchannel. *

*/ @SuppressWarnings("ReferenceEquality") @Override protected void updateOverallBalancingState() { List activeList = getReadyChildren(); if (activeList.isEmpty()) { // No READY subchannels, determine aggregate state and error status boolean isConnecting = false; List childrenInTf = new ArrayList<>(); for (ChildLbState childLbState : getChildLbStates()) { ConnectivityState state = childLbState.getCurrentState(); if (state == CONNECTING || state == IDLE) { isConnecting = true; } else if (state == TRANSIENT_FAILURE) { childrenInTf.add(childLbState); } } if (isConnecting) { updateBalancingState(CONNECTING, new EmptyPicker()); } else { // Give it all the failing children and let it randomly pick among them updateBalancingState(TRANSIENT_FAILURE, new ReadyPicker(childrenInTf, choiceCount, random)); } } else { updateBalancingState(READY, new ReadyPicker(activeList, choiceCount, random)); } } @Override protected ChildLbState createChildLbState(Object key) { return new LeastRequestLbState(key, pickFirstLbProvider); } private void updateBalancingState(ConnectivityState state, SubchannelPicker picker) { if (state != currentConnectivityState || !picker.equals(currentPicker)) { getHelper().updateBalancingState(state, picker); currentConnectivityState = state; currentPicker = picker; } } /** * This should ONLY be used by tests. */ @VisibleForTesting void setResolvingAddresses(boolean newValue) { super.resolvingAddresses = newValue; } // Expose for tests in this package. private static AtomicInteger getInFlights(ChildLbState childLbState) { return ((LeastRequestLbState)childLbState).activeRequests; } @VisibleForTesting static final class ReadyPicker extends SubchannelPicker { private final List childPickers; // non-empty private final List childInFlights; // 1:1 with childPickers private final int choiceCount; private final ThreadSafeRandom random; private final int hashCode; ReadyPicker(List childLbStates, int choiceCount, ThreadSafeRandom random) { checkArgument(!childLbStates.isEmpty(), "empty list"); this.childPickers = new ArrayList<>(childLbStates.size()); this.childInFlights = new ArrayList<>(childLbStates.size()); for (ChildLbState state : childLbStates) { childPickers.add(state.getCurrentPicker()); childInFlights.add(getInFlights(state)); } this.choiceCount = choiceCount; this.random = checkNotNull(random, "random"); int sum = 0; for (SubchannelPicker child : childPickers) { sum += child.hashCode(); } this.hashCode = sum ^ choiceCount; } @Override public PickResult pickSubchannel(PickSubchannelArgs args) { int child = nextChildToUse(); PickResult childResult = childPickers.get(child).pickSubchannel(args); if (!childResult.getStatus().isOk() || childResult.getSubchannel() == null) { return childResult; } if (childResult.getStreamTracerFactory() != null) { // Already wrapped, so just use the current picker for selected child return childResult; } else { // Wrap the subchannel OutstandingRequestsTracingFactory factory = new OutstandingRequestsTracingFactory(childInFlights.get(child)); return PickResult.withSubchannel(childResult.getSubchannel(), factory); } } @Override public String toString() { return MoreObjects.toStringHelper(ReadyPicker.class) .add("list", childPickers) .add("choiceCount", choiceCount) .toString(); } private int nextChildToUse() { int candidate = random.nextInt(childPickers.size()); for (int i = 0; i < choiceCount - 1; ++i) { int sampled = random.nextInt(childPickers.size()); if (childInFlights.get(sampled).get() < childInFlights.get(candidate).get()) { candidate = sampled; } } return candidate; } @VisibleForTesting List getChildPickers() { return childPickers; } @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object o) { if (!(o instanceof ReadyPicker)) { return false; } ReadyPicker other = (ReadyPicker) o; if (other == this) { return true; } // the lists cannot contain duplicate children return hashCode == other.hashCode && choiceCount == other.choiceCount && childPickers.size() == other.childPickers.size() && new HashSet<>(childPickers).containsAll(other.childPickers); } } @VisibleForTesting static final class EmptyPicker extends SubchannelPicker { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { return PickResult.withNoResult(); } @Override public int hashCode() { return getClass().hashCode(); } @Override public boolean equals(Object o) { return o instanceof EmptyPicker; } @Override public String toString() { return MoreObjects.toStringHelper(EmptyPicker.class).toString(); } } private static final class OutstandingRequestsTracingFactory extends ClientStreamTracer.Factory { private final AtomicInteger inFlights; private OutstandingRequestsTracingFactory(AtomicInteger inFlights) { this.inFlights = checkNotNull(inFlights, "inFlights"); } @Override public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) { return new ClientStreamTracer() { @Override public void streamCreated(Attributes transportAttrs, Metadata headers) { inFlights.incrementAndGet(); } @Override public void streamClosed(Status status) { inFlights.decrementAndGet(); } }; } } static final class LeastRequestConfig { final int choiceCount; LeastRequestConfig(int choiceCount) { checkArgument(choiceCount >= MIN_CHOICE_COUNT, "choiceCount <= 1"); // Even though a choiceCount value larger than 2 is currently considered valid in xDS // we restrict it to 10 here as specified in "A48: xDS Least Request LB Policy". this.choiceCount = Math.min(choiceCount, MAX_CHOICE_COUNT); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("choiceCount", choiceCount) .toString(); } } protected class LeastRequestLbState extends ChildLbState { private final AtomicInteger activeRequests = new AtomicInteger(0); public LeastRequestLbState(Object key, LoadBalancerProvider policyProvider) { super(key, policyProvider); } int getActiveRequests() { return activeRequests.get(); } @Override protected ChildLbStateHelper createChildHelper() { return new ChildLbStateHelper() { @Override public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) { super.updateBalancingState(newState, newPicker); if (!resolvingAddresses && newState == IDLE) { getLb().requestConnection(); } } }; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy