google.golang.org/grpc@v1.72.2/test/xds/xds_client_custom_lb_test.go (about) 1 /* 2 * 3 * Copyright 2023 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 package xds_test 20 21 import ( 22 "context" 23 "fmt" 24 "testing" 25 "time" 26 27 "google.golang.org/grpc" 28 _ "google.golang.org/grpc/balancer/leastrequest" // To register least_request 29 _ "google.golang.org/grpc/balancer/weightedroundrobin" // To register weighted_round_robin 30 "google.golang.org/grpc/credentials/insecure" 31 "google.golang.org/grpc/internal/envconfig" 32 "google.golang.org/grpc/internal/stubserver" 33 "google.golang.org/grpc/internal/testutils" 34 "google.golang.org/grpc/internal/testutils/roundrobin" 35 "google.golang.org/grpc/internal/testutils/xds/e2e" 36 "google.golang.org/grpc/internal/testutils/xds/e2e/setup" 37 "google.golang.org/grpc/resolver" 38 39 v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3" 40 v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 41 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 42 v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 43 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 44 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 45 v3clientsideweightedroundrobinpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3" 46 v3leastrequestpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3" 47 v3roundrobinpb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/round_robin/v3" 48 v3wrrlocalitypb "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3" 49 testgrpc "google.golang.org/grpc/interop/grpc_testing" 50 "google.golang.org/protobuf/proto" 51 "google.golang.org/protobuf/types/known/durationpb" 52 "google.golang.org/protobuf/types/known/structpb" 53 "google.golang.org/protobuf/types/known/wrapperspb" 54 ) 55 56 // wrrLocality is a helper that takes a proto message and returns a 57 // WrrLocalityProto with the proto message marshaled into a proto.Any as a 58 // child. 59 func wrrLocality(t *testing.T, m proto.Message) *v3wrrlocalitypb.WrrLocality { 60 return &v3wrrlocalitypb.WrrLocality{ 61 EndpointPickingPolicy: &v3clusterpb.LoadBalancingPolicy{ 62 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 63 { 64 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 65 TypedConfig: testutils.MarshalAny(t, m), 66 }, 67 }, 68 }, 69 }, 70 } 71 } 72 73 // clusterWithLBConfiguration returns a cluster resource with the proto message 74 // passed Marshaled to an any and specified through the load_balancing_policy 75 // field. 76 func clusterWithLBConfiguration(t *testing.T, clusterName, edsServiceName string, secLevel e2e.SecurityLevel, m proto.Message) *v3clusterpb.Cluster { 77 cluster := e2e.DefaultCluster(clusterName, edsServiceName, secLevel) 78 cluster.LoadBalancingPolicy = &v3clusterpb.LoadBalancingPolicy{ 79 Policies: []*v3clusterpb.LoadBalancingPolicy_Policy{ 80 { 81 TypedExtensionConfig: &v3corepb.TypedExtensionConfig{ 82 TypedConfig: testutils.MarshalAny(t, m), 83 }, 84 }, 85 }, 86 } 87 return cluster 88 } 89 90 // TestWRRLocality tests RPC distribution across a scenario with 5 backends, 91 // with 2 backends in a locality with weight 1, and 3 backends in a second 92 // locality with weight 2. Through xDS, the test configures a 93 // wrr_locality_balancer with either a round robin or custom (specifying pick 94 // first) child load balancing policy, and asserts the correct distribution 95 // based on the locality weights and the endpoint picking policy specified. 96 func (s) TestWrrLocality(t *testing.T) { 97 oldLeastRequestLBSupport := envconfig.LeastRequestLB 98 envconfig.LeastRequestLB = true 99 defer func() { 100 envconfig.LeastRequestLB = oldLeastRequestLBSupport 101 }() 102 103 backend1 := stubserver.StartTestService(t, nil) 104 port1 := testutils.ParsePort(t, backend1.Address) 105 defer backend1.Stop() 106 backend2 := stubserver.StartTestService(t, nil) 107 port2 := testutils.ParsePort(t, backend2.Address) 108 defer backend2.Stop() 109 backend3 := stubserver.StartTestService(t, nil) 110 port3 := testutils.ParsePort(t, backend3.Address) 111 defer backend3.Stop() 112 backend4 := stubserver.StartTestService(t, nil) 113 port4 := testutils.ParsePort(t, backend4.Address) 114 defer backend4.Stop() 115 backend5 := stubserver.StartTestService(t, nil) 116 port5 := testutils.ParsePort(t, backend5.Address) 117 defer backend5.Stop() 118 const serviceName = "my-service-client-side-xds" 119 120 tests := []struct { 121 name string 122 // Configuration will be specified through load_balancing_policy field. 123 wrrLocalityConfiguration *v3wrrlocalitypb.WrrLocality 124 addressDistributionWant []struct { 125 addr string 126 count int 127 } 128 }{ 129 { 130 name: "rr_child", 131 wrrLocalityConfiguration: wrrLocality(t, &v3roundrobinpb.RoundRobin{}), 132 // Each addresses expected probability is locality weight of 133 // locality / total locality weights multiplied by 1 / number of 134 // endpoints in each locality (due to round robin across endpoints 135 // in a locality). Thus, address 1 and address 2 have 1/3 * 1/2 136 // probability, and addresses 3 4 5 have 2/3 * 1/3 probability of 137 // being routed to. 138 addressDistributionWant: []struct { 139 addr string 140 count int 141 }{ 142 {addr: backend1.Address, count: 6}, 143 {addr: backend2.Address, count: 6}, 144 {addr: backend3.Address, count: 8}, 145 {addr: backend4.Address, count: 8}, 146 {addr: backend5.Address, count: 8}, 147 }, 148 }, 149 // This configures custom lb as the child of wrr_locality, which points 150 // to our pick_first implementation. Thus, the expected distribution of 151 // addresses is locality weight of locality / total locality weights as 152 // the probability of picking the first backend within the locality 153 // (e.g. Address 1 for locality 1, and Address 3 for locality 2). 154 { 155 name: "custom_lb_child_pick_first", 156 wrrLocalityConfiguration: wrrLocality(t, &v3xdsxdstypepb.TypedStruct{ 157 TypeUrl: "type.googleapis.com/pick_first", 158 Value: &structpb.Struct{}, 159 }), 160 addressDistributionWant: []struct { 161 addr string 162 count int 163 }{ 164 {addr: backend1.Address, count: 1}, 165 {addr: backend3.Address, count: 2}, 166 }, 167 }, 168 // Sanity check for weighted round robin. Don't need to test super 169 // specific behaviors, as that is covered in unit tests. Set up weighted 170 // round robin as the endpoint picking policy with per RPC load reports 171 // enabled. Due the server not sending trailers with load reports, the 172 // weighted round robin policy should essentially function as round 173 // robin, and thus should have the same distribution as round robin 174 // above. 175 { 176 name: "custom_lb_child_wrr/", 177 wrrLocalityConfiguration: wrrLocality(t, &v3clientsideweightedroundrobinpb.ClientSideWeightedRoundRobin{ 178 EnableOobLoadReport: &wrapperspb.BoolValue{ 179 Value: false, 180 }, 181 // BlackoutPeriod long enough to cause load report weights to 182 // trigger in the scope of test case, but no load reports 183 // configured anyway. 184 BlackoutPeriod: durationpb.New(10 * time.Second), 185 WeightExpirationPeriod: durationpb.New(10 * time.Second), 186 WeightUpdatePeriod: durationpb.New(time.Second), 187 ErrorUtilizationPenalty: &wrapperspb.FloatValue{Value: 1}, 188 }), 189 addressDistributionWant: []struct { 190 addr string 191 count int 192 }{ 193 {addr: backend1.Address, count: 6}, 194 {addr: backend2.Address, count: 6}, 195 {addr: backend3.Address, count: 8}, 196 {addr: backend4.Address, count: 8}, 197 {addr: backend5.Address, count: 8}, 198 }, 199 }, 200 { 201 name: "custom_lb_least_request", 202 wrrLocalityConfiguration: wrrLocality(t, &v3leastrequestpb.LeastRequest{ 203 ChoiceCount: wrapperspb.UInt32(2), 204 }), 205 // The test performs a Unary RPC, and blocks until the RPC returns, 206 // and then makes the next Unary RPC. Thus, over iterations, no RPC 207 // counts are present. This causes least request's randomness of 208 // indexes to sample to converge onto a round robin distribution per 209 // locality. Thus, expect the same distribution as round robin 210 // above. 211 addressDistributionWant: []struct { 212 addr string 213 count int 214 }{ 215 {addr: backend1.Address, count: 6}, 216 {addr: backend2.Address, count: 6}, 217 {addr: backend3.Address, count: 8}, 218 {addr: backend4.Address, count: 8}, 219 {addr: backend5.Address, count: 8}, 220 }, 221 }, 222 } 223 for _, test := range tests { 224 t.Run(test.name, func(t *testing.T) { 225 // Start an xDS management server. 226 managementServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t) 227 228 routeConfigName := "route-" + serviceName 229 clusterName := "cluster-" + serviceName 230 endpointsName := "endpoints-" + serviceName 231 resources := e2e.UpdateOptions{ 232 NodeID: nodeID, 233 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}, 234 Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)}, 235 Clusters: []*v3clusterpb.Cluster{clusterWithLBConfiguration(t, clusterName, endpointsName, e2e.SecurityLevelNone, test.wrrLocalityConfiguration)}, 236 Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.EndpointResourceWithOptions(e2e.EndpointOptions{ 237 ClusterName: endpointsName, 238 Host: "localhost", 239 Localities: []e2e.LocalityOptions{ 240 { 241 Backends: []e2e.BackendOptions{{Ports: []uint32{port1}}, {Ports: []uint32{port2}}}, 242 Weight: 1, 243 }, 244 { 245 Backends: []e2e.BackendOptions{{Ports: []uint32{port3}}, {Ports: []uint32{port4}}, {Ports: []uint32{port5}}}, 246 Weight: 2, 247 }, 248 }, 249 })}, 250 } 251 252 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 253 defer cancel() 254 if err := managementServer.Update(ctx, resources); err != nil { 255 t.Fatal(err) 256 } 257 258 cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver)) 259 if err != nil { 260 t.Fatalf("Failed to dial local test server: %v", err) 261 } 262 defer cc.Close() 263 264 client := testgrpc.NewTestServiceClient(cc) 265 var addrDistWant []resolver.Address 266 for _, addrAndCount := range test.addressDistributionWant { 267 for i := 0; i < addrAndCount.count; i++ { 268 addrDistWant = append(addrDistWant, resolver.Address{Addr: addrAndCount.addr}) 269 } 270 } 271 if err := roundrobin.CheckWeightedRoundRobinRPCs(ctx, client, addrDistWant); err != nil { 272 t.Fatalf("Error in expected round robin: %v", err) 273 } 274 }) 275 } 276 }