mirror of https://github.com/grpc/grpc-node.git
Merge pull request #1517 from murgatroid99/grpc-js_lrs_load_balancer
grpc-js: Add LrsLoadBalancer class
This commit is contained in:
commit
f08d3aefd0
|
@ -297,16 +297,33 @@ export class EdsLoadBalancer implements LoadBalancer {
|
||||||
WeightedTarget
|
WeightedTarget
|
||||||
>();
|
>();
|
||||||
for (const localityObj of localityArray) {
|
for (const localityObj of localityArray) {
|
||||||
|
/* Use the endpoint picking policy from the config, default to
|
||||||
|
* round_robin. */
|
||||||
|
const endpointPickingPolicy: LoadBalancingConfig[] = [
|
||||||
|
...this.lastestConfig.eds.endpointPickingPolicy,
|
||||||
|
{ name: 'round_robin', round_robin: {} },
|
||||||
|
];
|
||||||
|
let childPolicy: LoadBalancingConfig[];
|
||||||
|
if (this.lastestConfig.eds.lrsLoadReportingServerName) {
|
||||||
|
childPolicy = [
|
||||||
|
{
|
||||||
|
name: 'lrs',
|
||||||
|
lrs: {
|
||||||
|
cluster_name: this.lastestConfig.eds.cluster,
|
||||||
|
eds_service_name: this.lastestConfig.eds.edsServiceName ?? '',
|
||||||
|
lrs_load_reporting_server_name: this.lastestConfig.eds
|
||||||
|
.lrsLoadReportingServerName,
|
||||||
|
locality: localityObj.locality,
|
||||||
|
child_policy: endpointPickingPolicy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
childPolicy = endpointPickingPolicy;
|
||||||
|
}
|
||||||
childTargets.set(localityToName(localityObj.locality), {
|
childTargets.set(localityToName(localityObj.locality), {
|
||||||
weight: localityObj.weight,
|
weight: localityObj.weight,
|
||||||
/* TODO(murgatroid99): Insert an lrs config around the round_robin
|
child_policy: childPolicy,
|
||||||
* config after implementing lrs */
|
|
||||||
/* Use the endpoint picking policy from the config, default to
|
|
||||||
* round_robin. */
|
|
||||||
child_policy: [
|
|
||||||
...this.lastestConfig.eds.endpointPickingPolicy,
|
|
||||||
{ name: 'round_robin', round_robin: {} },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
for (const address of localityObj.addresses) {
|
for (const address of localityObj.addresses) {
|
||||||
addressList.push({
|
addressList.push({
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoadBalancer,
|
||||||
|
ChannelControlHelper,
|
||||||
|
registerLoadBalancerType,
|
||||||
|
getFirstUsableConfig,
|
||||||
|
} from './load-balancer';
|
||||||
|
import { SubchannelAddress } from './subchannel';
|
||||||
|
import {
|
||||||
|
LoadBalancingConfig,
|
||||||
|
isLrsLoadBalancingConfig,
|
||||||
|
} from './load-balancing-config';
|
||||||
|
import { ChildLoadBalancerHandler } from './load-balancer-child-handler';
|
||||||
|
import { ConnectivityState } from './channel';
|
||||||
|
import { Picker, PickArgs, PickResultType, PickResult } from './picker';
|
||||||
|
import { XdsClusterLocalityStats, XdsClient } from './xds-client';
|
||||||
|
import { Filter, BaseFilter, FilterFactory } from './filter';
|
||||||
|
import { StatusObject, Call } from './call-stream';
|
||||||
|
import { Status } from './constants';
|
||||||
|
import { FilterStackFactory } from './filter-stack';
|
||||||
|
|
||||||
|
const TYPE_NAME = 'lrs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter class that reports when the call ends.
|
||||||
|
*/
|
||||||
|
class CallEndTrackingFilter extends BaseFilter implements Filter {
|
||||||
|
constructor(private localityStatsReporter: XdsClusterLocalityStats) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveTrailers(status: StatusObject) {
|
||||||
|
this.localityStatsReporter.addCallFinished(status.code !== Status.OK);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallEndTrackingFilterFactory
|
||||||
|
implements FilterFactory<CallEndTrackingFilter> {
|
||||||
|
constructor(private localityStatsReporter: XdsClusterLocalityStats) {}
|
||||||
|
|
||||||
|
createFilter(callStream: Call): CallEndTrackingFilter {
|
||||||
|
return new CallEndTrackingFilter(this.localityStatsReporter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picker that delegates picking to another picker, and reports when calls
|
||||||
|
* created using those picks start and end.
|
||||||
|
*/
|
||||||
|
class LoadReportingPicker implements Picker {
|
||||||
|
constructor(
|
||||||
|
private wrappedPicker: Picker,
|
||||||
|
private localityStatsReporter: XdsClusterLocalityStats
|
||||||
|
) {}
|
||||||
|
|
||||||
|
pick(pickArgs: PickArgs): PickResult {
|
||||||
|
const wrappedPick = this.wrappedPicker.pick(pickArgs);
|
||||||
|
if (wrappedPick.pickResultType === PickResultType.COMPLETE) {
|
||||||
|
const trackingFilterFactory = new CallEndTrackingFilterFactory(
|
||||||
|
this.localityStatsReporter
|
||||||
|
);
|
||||||
|
/* In the unlikely event that the wrappedPick already has an
|
||||||
|
* extraFilterFactory, preserve it in a FilterStackFactory. */
|
||||||
|
const extraFilterFactory = wrappedPick.extraFilterFactory
|
||||||
|
? new FilterStackFactory([
|
||||||
|
wrappedPick.extraFilterFactory,
|
||||||
|
trackingFilterFactory,
|
||||||
|
])
|
||||||
|
: trackingFilterFactory;
|
||||||
|
return {
|
||||||
|
pickResultType: PickResultType.COMPLETE,
|
||||||
|
subchannel: wrappedPick.subchannel,
|
||||||
|
status: null,
|
||||||
|
onCallStarted: () => {
|
||||||
|
wrappedPick.onCallStarted?.();
|
||||||
|
this.localityStatsReporter.addCallStarted();
|
||||||
|
},
|
||||||
|
extraFilterFactory: extraFilterFactory,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return wrappedPick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Load balancer" that delegates the actual load balancing logic to another
|
||||||
|
* LoadBalancer class and adds hooks to track when calls started using that
|
||||||
|
* LoadBalancer start and end, and uses the XdsClient to report that
|
||||||
|
* information back to the xDS server.
|
||||||
|
*/
|
||||||
|
export class LrsLoadBalancer implements LoadBalancer {
|
||||||
|
private childBalancer: ChildLoadBalancerHandler;
|
||||||
|
private localityStatsReporter: XdsClusterLocalityStats | null = null;
|
||||||
|
|
||||||
|
constructor(private channelControlHelper: ChannelControlHelper) {
|
||||||
|
this.childBalancer = new ChildLoadBalancerHandler({
|
||||||
|
createSubchannel: (subchannelAddress, subchannelArgs) =>
|
||||||
|
channelControlHelper.createSubchannel(
|
||||||
|
subchannelAddress,
|
||||||
|
subchannelArgs
|
||||||
|
),
|
||||||
|
requestReresolution: () => channelControlHelper.requestReresolution(),
|
||||||
|
updateState: (connectivityState: ConnectivityState, picker: Picker) => {
|
||||||
|
if (this.localityStatsReporter !== null) {
|
||||||
|
picker = new LoadReportingPicker(picker, this.localityStatsReporter);
|
||||||
|
}
|
||||||
|
channelControlHelper.updateState(connectivityState, picker);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAddressList(
|
||||||
|
addressList: SubchannelAddress[],
|
||||||
|
lbConfig: LoadBalancingConfig,
|
||||||
|
attributes: { [key: string]: unknown }
|
||||||
|
): void {
|
||||||
|
if (!isLrsLoadBalancingConfig(lbConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(attributes.xdsClient instanceof XdsClient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lrsConfig = lbConfig.lrs;
|
||||||
|
this.localityStatsReporter = attributes.xdsClient.addClusterLocalityStats(
|
||||||
|
lrsConfig.lrs_load_reporting_server_name,
|
||||||
|
lrsConfig.cluster_name,
|
||||||
|
lrsConfig.eds_service_name,
|
||||||
|
lrsConfig.locality
|
||||||
|
);
|
||||||
|
const childPolicy: LoadBalancingConfig = getFirstUsableConfig(
|
||||||
|
lrsConfig.child_policy
|
||||||
|
) ?? { name: 'pick_first', pick_first: {} };
|
||||||
|
this.childBalancer.updateAddressList(addressList, childPolicy, attributes);
|
||||||
|
}
|
||||||
|
exitIdle(): void {
|
||||||
|
this.childBalancer.exitIdle();
|
||||||
|
}
|
||||||
|
resetBackoff(): void {
|
||||||
|
this.childBalancer.resetBackoff();
|
||||||
|
}
|
||||||
|
destroy(): void {
|
||||||
|
this.childBalancer.destroy();
|
||||||
|
}
|
||||||
|
getTypeName(): string {
|
||||||
|
return TYPE_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
registerLoadBalancerType(TYPE_NAME, LrsLoadBalancer);
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import * as load_balancer_priority from './load-balancer-priority';
|
||||||
import * as load_balancer_weighted_target from './load-balancer-weighted-target';
|
import * as load_balancer_weighted_target from './load-balancer-weighted-target';
|
||||||
import * as load_balancer_eds from './load-balancer-eds';
|
import * as load_balancer_eds from './load-balancer-eds';
|
||||||
import * as load_balancer_cds from './load-balancer-cds';
|
import * as load_balancer_cds from './load-balancer-cds';
|
||||||
|
import * as load_balancer_lrs from './load-balancer-lrs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of functions associated with a channel that a load balancer
|
* A collection of functions associated with a channel that a load balancer
|
||||||
|
@ -145,4 +146,5 @@ export function registerAll() {
|
||||||
load_balancer_weighted_target.setup();
|
load_balancer_weighted_target.setup();
|
||||||
load_balancer_eds.setup();
|
load_balancer_eds.setup();
|
||||||
load_balancer_cds.setup();
|
load_balancer_cds.setup();
|
||||||
|
load_balancer_lrs.setup();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Locality__Output } from './generated/envoy/api/v2/core/Locality';
|
||||||
|
|
||||||
/* This file is an implementation of gRFC A24:
|
/* This file is an implementation of gRFC A24:
|
||||||
* https://github.com/grpc/proposal/blob/master/A24-lb-policy-config.md. Each
|
* https://github.com/grpc/proposal/blob/master/A24-lb-policy-config.md. Each
|
||||||
* function here takes an object with unknown structure and returns its
|
* function here takes an object with unknown structure and returns its
|
||||||
|
@ -79,6 +81,14 @@ export interface CdsLbConfig {
|
||||||
cluster: string;
|
cluster: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LrsLbConfig {
|
||||||
|
cluster_name: string;
|
||||||
|
eds_service_name: string;
|
||||||
|
lrs_load_reporting_server_name: string;
|
||||||
|
locality: Locality__Output;
|
||||||
|
child_policy: LoadBalancingConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PickFirstLoadBalancingConfig {
|
export interface PickFirstLoadBalancingConfig {
|
||||||
name: 'pick_first';
|
name: 'pick_first';
|
||||||
pick_first: PickFirstConfig;
|
pick_first: PickFirstConfig;
|
||||||
|
@ -119,6 +129,11 @@ export interface CdsLoadBalancingConfig {
|
||||||
cds: CdsLbConfig;
|
cds: CdsLbConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LrsLoadBalancingConfig {
|
||||||
|
name: 'lrs';
|
||||||
|
lrs: LrsLbConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export type LoadBalancingConfig =
|
export type LoadBalancingConfig =
|
||||||
| PickFirstLoadBalancingConfig
|
| PickFirstLoadBalancingConfig
|
||||||
| RoundRobinLoadBalancingConfig
|
| RoundRobinLoadBalancingConfig
|
||||||
|
@ -127,7 +142,8 @@ export type LoadBalancingConfig =
|
||||||
| PriorityLoadBalancingConfig
|
| PriorityLoadBalancingConfig
|
||||||
| WeightedTargetLoadBalancingConfig
|
| WeightedTargetLoadBalancingConfig
|
||||||
| EdsLoadBalancingConfig
|
| EdsLoadBalancingConfig
|
||||||
| CdsLoadBalancingConfig;
|
| CdsLoadBalancingConfig
|
||||||
|
| LrsLoadBalancingConfig;
|
||||||
|
|
||||||
export function isRoundRobinLoadBalancingConfig(
|
export function isRoundRobinLoadBalancingConfig(
|
||||||
lbconfig: LoadBalancingConfig
|
lbconfig: LoadBalancingConfig
|
||||||
|
@ -171,6 +187,12 @@ export function isCdsLoadBalancingConfig(
|
||||||
return lbconfig.name === 'cds';
|
return lbconfig.name === 'cds';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLrsLoadBalancingConfig(
|
||||||
|
lbconfig: LoadBalancingConfig
|
||||||
|
): lbconfig is LrsLoadBalancingConfig {
|
||||||
|
return lbconfig.name === 'lrs';
|
||||||
|
}
|
||||||
|
|
||||||
/* In these functions we assume the input came from a JSON object. Therefore we
|
/* In these functions we assume the input came from a JSON object. Therefore we
|
||||||
* expect that the prototype is uninteresting and that `in` can be used
|
* expect that the prototype is uninteresting and that `in` can be used
|
||||||
* effectively */
|
* effectively */
|
||||||
|
|
|
@ -38,7 +38,10 @@ import { Cluster__Output } from './generated/envoy/api/v2/Cluster';
|
||||||
import { LoadReportingServiceClient } from './generated/envoy/service/load_stats/v2/LoadReportingService';
|
import { LoadReportingServiceClient } from './generated/envoy/service/load_stats/v2/LoadReportingService';
|
||||||
import { LoadStatsRequest } from './generated/envoy/service/load_stats/v2/LoadStatsRequest';
|
import { LoadStatsRequest } from './generated/envoy/service/load_stats/v2/LoadStatsRequest';
|
||||||
import { LoadStatsResponse__Output } from './generated/envoy/service/load_stats/v2/LoadStatsResponse';
|
import { LoadStatsResponse__Output } from './generated/envoy/service/load_stats/v2/LoadStatsResponse';
|
||||||
import { Locality__Output } from './generated/envoy/api/v2/core/Locality';
|
import {
|
||||||
|
Locality__Output,
|
||||||
|
Locality,
|
||||||
|
} from './generated/envoy/api/v2/core/Locality';
|
||||||
import {
|
import {
|
||||||
ClusterStats,
|
ClusterStats,
|
||||||
_envoy_api_v2_endpoint_ClusterStats_DroppedRequests,
|
_envoy_api_v2_endpoint_ClusterStats_DroppedRequests,
|
||||||
|
@ -99,6 +102,17 @@ function loadAdsProtos(): Promise<
|
||||||
return loadedProtos;
|
return loadedProtos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localityEqual(
|
||||||
|
loc1: Locality__Output,
|
||||||
|
loc2: Locality__Output
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
loc1.region === loc2.region &&
|
||||||
|
loc1.zone === loc2.zone &&
|
||||||
|
loc1.sub_zone === loc2.sub_zone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface Watcher<UpdateType> {
|
export interface Watcher<UpdateType> {
|
||||||
onValidUpdate(update: UpdateType): void;
|
onValidUpdate(update: UpdateType): void;
|
||||||
onTransientError(error: StatusObject): void;
|
onTransientError(error: StatusObject): void;
|
||||||
|
@ -109,6 +123,11 @@ export interface XdsClusterDropStats {
|
||||||
addCallDropped(category: string): void;
|
addCallDropped(category: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface XdsClusterLocalityStats {
|
||||||
|
addCallStarted(): void;
|
||||||
|
addCallFinished(fail: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface ClusterLocalityStats {
|
interface ClusterLocalityStats {
|
||||||
locality: Locality__Output;
|
locality: Locality__Output;
|
||||||
callsStarted: number;
|
callsStarted: number;
|
||||||
|
@ -792,12 +811,12 @@ export class XdsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param lrsServer The target name of the server to send stats to. An empty
|
* @param lrsServer The target name of the server to send stats to. An empty
|
||||||
* string indicates that the default LRS client should be used. Currently
|
* string indicates that the default LRS client should be used. Currently
|
||||||
* only the empty string is supported here.
|
* only the empty string is supported here.
|
||||||
* @param clusterName
|
* @param clusterName
|
||||||
* @param edsServiceName
|
* @param edsServiceName
|
||||||
*/
|
*/
|
||||||
addClusterDropStats(
|
addClusterDropStats(
|
||||||
lrsServer: string,
|
lrsServer: string,
|
||||||
|
@ -806,7 +825,7 @@ export class XdsClient {
|
||||||
): XdsClusterDropStats {
|
): XdsClusterDropStats {
|
||||||
if (lrsServer !== '') {
|
if (lrsServer !== '') {
|
||||||
return {
|
return {
|
||||||
addCallDropped: category => {}
|
addCallDropped: (category) => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const clusterStats = this.clusterStatsMap.getOrCreate(
|
const clusterStats = this.clusterStatsMap.getOrCreate(
|
||||||
|
@ -821,6 +840,58 @@ export class XdsClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addClusterLocalityStats(
|
||||||
|
lrsServer: string,
|
||||||
|
clusterName: string,
|
||||||
|
edsServiceName: string,
|
||||||
|
locality: Locality__Output
|
||||||
|
): XdsClusterLocalityStats {
|
||||||
|
if (lrsServer !== '') {
|
||||||
|
return {
|
||||||
|
addCallStarted: () => {},
|
||||||
|
addCallFinished: (fail) => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const clusterStats = this.clusterStatsMap.getOrCreate(
|
||||||
|
clusterName,
|
||||||
|
edsServiceName
|
||||||
|
);
|
||||||
|
let localityStats: ClusterLocalityStats | null = null;
|
||||||
|
for (const statsObj of clusterStats.localityStats) {
|
||||||
|
if (localityEqual(locality, statsObj.locality)) {
|
||||||
|
localityStats = statsObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (localityStats === null) {
|
||||||
|
localityStats = {
|
||||||
|
locality: locality,
|
||||||
|
callsInProgress: 0,
|
||||||
|
callsStarted: 0,
|
||||||
|
callsSucceeded: 0,
|
||||||
|
callsFailed: 0,
|
||||||
|
};
|
||||||
|
clusterStats.localityStats.push(localityStats);
|
||||||
|
}
|
||||||
|
/* Help the compiler understand that this object is always non-null in the
|
||||||
|
* closure */
|
||||||
|
const finalLocalityStats: ClusterLocalityStats = localityStats;
|
||||||
|
return {
|
||||||
|
addCallStarted: () => {
|
||||||
|
finalLocalityStats.callsStarted += 1;
|
||||||
|
finalLocalityStats.callsInProgress += 1;
|
||||||
|
},
|
||||||
|
addCallFinished: (fail) => {
|
||||||
|
if (fail) {
|
||||||
|
finalLocalityStats.callsFailed += 1;
|
||||||
|
} else {
|
||||||
|
finalLocalityStats.callsSucceeded += 1;
|
||||||
|
}
|
||||||
|
finalLocalityStats.callsInProgress -= 1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
this.adsCall?.cancel();
|
this.adsCall?.cancel();
|
||||||
this.adsClient?.close();
|
this.adsClient?.close();
|
||||||
|
|
Loading…
Reference in New Issue