dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/balancer/clusterresolver/configbuilder.go (about) 1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. 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 * 20 * Copyright 2021 gRPC authors. 21 * 22 */ 23 24 package clusterresolver 25 26 import ( 27 "encoding/json" 28 "fmt" 29 "sort" 30 ) 31 32 import ( 33 dubbogoLogger "github.com/dubbogo/gost/log/logger" 34 35 "google.golang.org/grpc/balancer/roundrobin" 36 "google.golang.org/grpc/balancer/weightedroundrobin" 37 "google.golang.org/grpc/balancer/weightedtarget" 38 39 "google.golang.org/grpc/resolver" 40 ) 41 42 import ( 43 "dubbo.apache.org/dubbo-go/v3/xds/balancer/clusterimpl" 44 "dubbo.apache.org/dubbo-go/v3/xds/balancer/priority" 45 "dubbo.apache.org/dubbo-go/v3/xds/balancer/ringhash" 46 "dubbo.apache.org/dubbo-go/v3/xds/client/resource" 47 "dubbo.apache.org/dubbo-go/v3/xds/utils/hierarchy" 48 internalserviceconfig "dubbo.apache.org/dubbo-go/v3/xds/utils/serviceconfig" 49 ) 50 51 const million = 1000000 52 53 // priorityConfig is config for one priority. For example, if there an EDS and a 54 // DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}]. 55 // 56 // Each priorityConfig corresponds to one discovery mechanism from the LBConfig 57 // generated by the CDS balancer. The CDS balancer resolves the cluster name to 58 // an ordered list of discovery mechanisms (if the top cluster is an aggregated 59 // cluster), one for each underlying cluster. 60 type priorityConfig struct { 61 mechanism DiscoveryMechanism 62 // edsResp is set only if type is EDS. 63 edsResp resource.EndpointsUpdate 64 // addresses is set only if type is DNS. 65 addresses []string 66 } 67 68 // buildPriorityConfigJSON builds balancer config for the passed in 69 // priorities. 70 // 71 // The built tree of balancers (see test for the output struct). 72 // 73 // If xds lb policy is ROUND_ROBIN, the children will be weighted_target for 74 // locality picking, and round_robin for endpoint picking. 75 // 76 // ┌────────┐ 77 // │priority│ 78 // └┬──────┬┘ 79 // │ │ 80 // ┌───────────▼┐ ┌▼───────────┐ 81 // │cluster_impl│ │cluster_impl│ 82 // └─┬──────────┘ └──────────┬─┘ 83 // │ │ 84 // ┌──────────────▼─┐ ┌─▼──────────────┐ 85 // │locality_picking│ │locality_picking│ 86 // └┬──────────────┬┘ └┬──────────────┬┘ 87 // │ │ │ │ 88 // ┌─▼─┐ ┌─▼─┐ ┌─▼─┐ ┌─▼─┐ 89 // │LRS│ │LRS│ │LRS│ │LRS│ 90 // └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ 91 // │ │ │ │ 92 // 93 // ┌──────────▼─────┐ ┌─────▼──────────┐ ┌──────────▼─────┐ ┌─────▼──────────┐ 94 // │endpoint_picking│ │endpoint_picking│ │endpoint_picking│ │endpoint_picking│ 95 // └────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘ 96 // 97 // If xds lb policy is RING_HASH, the children will be just a ring_hash policy. 98 // The endpoints from all localities will be flattened to one addresses list, 99 // and the ring_hash policy will pick endpoints from it. 100 // 101 // ┌────────┐ 102 // │priority│ 103 // └┬──────┬┘ 104 // │ │ 105 // 106 // ┌──────────▼─┐ ┌─▼──────────┐ 107 // │cluster_impl│ │cluster_impl│ 108 // └──────┬─────┘ └─────┬──────┘ 109 // 110 // │ │ 111 // 112 // ┌──────▼─────┐ ┌─────▼──────┐ 113 // │ ring_hash │ │ ring_hash │ 114 // └────────────┘ └────────────┘ 115 // 116 // If endpointPickingPolicy is nil, roundrobin will be used. 117 // 118 // Custom locality picking policy isn't support, and weighted_target is always 119 // used. 120 func buildPriorityConfigJSON(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Address, error) { 121 pc, addrs, err := buildPriorityConfig(priorities, xdsLBPolicy) 122 if err != nil { 123 return nil, nil, fmt.Errorf("failed to build priority config: %v", err) 124 } 125 ret, err := json.Marshal(pc) 126 if err != nil { 127 return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err) 128 } 129 return ret, addrs, nil 130 } 131 132 func buildPriorityConfig(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Address, error) { 133 var ( 134 retConfig = &priority.LBConfig{Children: make(map[string]*priority.Child)} 135 retAddrs []resolver.Address 136 ) 137 for i, p := range priorities { 138 switch p.mechanism.Type { 139 case DiscoveryMechanismTypeEDS: 140 names, configs, addrs, err := buildClusterImplConfigForEDS(i, p.edsResp, p.mechanism, xdsLBPolicy) 141 if err != nil { 142 return nil, nil, err 143 } 144 retConfig.Priorities = append(retConfig.Priorities, names...) 145 for n, c := range configs { 146 retConfig.Children[n] = &priority.Child{ 147 Config: &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: c}, 148 // Ignore all re-resolution from EDS children. 149 IgnoreReresolutionRequests: true, 150 } 151 } 152 retAddrs = append(retAddrs, addrs...) 153 case DiscoveryMechanismTypeLogicalDNS: 154 name, config, addrs := buildClusterImplConfigForDNS(i, p.addresses) 155 retConfig.Priorities = append(retConfig.Priorities, name) 156 retConfig.Children[name] = &priority.Child{ 157 Config: &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: config}, 158 // Not ignore re-resolution from DNS children, they will trigger 159 // DNS to re-resolve. 160 IgnoreReresolutionRequests: false, 161 } 162 retAddrs = append(retAddrs, addrs...) 163 } 164 } 165 return retConfig, retAddrs, nil 166 } 167 168 func buildClusterImplConfigForDNS(parentPriority int, addrStrs []string) (string, *clusterimpl.LBConfig, []resolver.Address) { 169 // Endpoint picking policy for DNS is hardcoded to pick_first. 170 const childPolicy = "pick_first" 171 retAddrs := make([]resolver.Address, 0, len(addrStrs)) 172 pName := fmt.Sprintf("priority-%v", parentPriority) 173 for _, addrStr := range addrStrs { 174 retAddrs = append(retAddrs, hierarchy.Set(resolver.Address{Addr: addrStr}, []string{pName})) 175 } 176 return pName, &clusterimpl.LBConfig{ChildPolicy: &internalserviceconfig.BalancerConfig{Name: childPolicy}}, retAddrs 177 } 178 179 // buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for 180 // each priority, sorted by priority, and the addresses for each priority (with 181 // hierarchy attributes set). 182 // 183 // For example, if there are two priorities, the returned values will be 184 // - ["p0", "p1"] 185 // - map{"p0":p0_config, "p1":p1_config} 186 // - [p0_address_0, p0_address_1, p1_address_0, p1_address_1] 187 // - p0 addresses' hierarchy attributes are set to p0 188 func buildClusterImplConfigForEDS(parentPriority int, edsResp resource.EndpointsUpdate, mechanism DiscoveryMechanism, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Address, error) { 189 drops := make([]clusterimpl.DropConfig, 0, len(edsResp.Drops)) 190 for _, d := range edsResp.Drops { 191 drops = append(drops, clusterimpl.DropConfig{ 192 Category: d.Category, 193 RequestsPerMillion: d.Numerator * million / d.Denominator, 194 }) 195 } 196 197 priorityChildNames, priorities := groupLocalitiesByPriority(edsResp.Localities) 198 retNames := make([]string, 0, len(priorityChildNames)) 199 retAddrs := make([]resolver.Address, 0, len(priorityChildNames)) 200 retConfigs := make(map[string]*clusterimpl.LBConfig, len(priorityChildNames)) 201 for _, priorityName := range priorityChildNames { 202 priorityLocalities := priorities[priorityName] 203 // Prepend parent priority to the priority names, to avoid duplicates. 204 pName := fmt.Sprintf("priority-%v-%v", parentPriority, priorityName) 205 retNames = append(retNames, pName) 206 cfg, addrs, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, mechanism, drops, xdsLBPolicy) 207 if err != nil { 208 return nil, nil, nil, err 209 } 210 retConfigs[pName] = cfg 211 retAddrs = append(retAddrs, addrs...) 212 } 213 return retNames, retConfigs, retAddrs, nil 214 } 215 216 // groupLocalitiesByPriority returns the localities grouped by priority. 217 // 218 // It also returns a list of strings where each string represents a priority, 219 // and the list is sorted from higher priority to lower priority. 220 // 221 // For example, for L0-p0, L1-p0, L2-p1, results will be 222 // - ["p0", "p1"] 223 // - map{"p0":[L0, L1], "p1":[L2]} 224 func groupLocalitiesByPriority(localities []resource.Locality) ([]string, map[string][]resource.Locality) { 225 var priorityIntSlice []int 226 priorities := make(map[string][]resource.Locality) 227 for _, locality := range localities { 228 if locality.Weight == 0 { 229 continue 230 } 231 priorityName := fmt.Sprintf("%v", locality.Priority) 232 priorities[priorityName] = append(priorities[priorityName], locality) 233 priorityIntSlice = append(priorityIntSlice, int(locality.Priority)) 234 } 235 // Sort the priorities based on the int value, deduplicate, and then turn 236 // the sorted list into a string list. This will be child names, in priority 237 // order. 238 sort.Ints(priorityIntSlice) 239 priorityIntSliceDeduped := dedupSortedIntSlice(priorityIntSlice) 240 priorityNameSlice := make([]string, 0, len(priorityIntSliceDeduped)) 241 for _, p := range priorityIntSliceDeduped { 242 priorityNameSlice = append(priorityNameSlice, fmt.Sprintf("%v", p)) 243 } 244 return priorityNameSlice, priorities 245 } 246 247 func dedupSortedIntSlice(a []int) []int { 248 if len(a) == 0 { 249 return a 250 } 251 i, j := 0, 1 252 for ; j < len(a); j++ { 253 if a[i] == a[j] { 254 continue 255 } 256 i++ 257 if i != j { 258 a[i] = a[j] 259 } 260 } 261 return a[:i+1] 262 } 263 264 // rrBalancerConfig is a const roundrobin config, used as child of 265 // weighted-roundrobin. To avoid allocating memory everytime. 266 var rrBalancerConfig = &internalserviceconfig.BalancerConfig{Name: roundrobin.Name} 267 268 // priorityLocalitiesToClusterImpl takes a list of localities (with the same 269 // priority), and generates a cluster impl policy config, and a list of 270 // addresses. 271 func priorityLocalitiesToClusterImpl(localities []resource.Locality, priorityName string, mechanism DiscoveryMechanism, drops []clusterimpl.DropConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Address, error) { 272 clusterImplCfg := &clusterimpl.LBConfig{ 273 Cluster: mechanism.Cluster, 274 EDSServiceName: mechanism.EDSServiceName, 275 LoadReportingServerName: mechanism.LoadReportingServerName, 276 MaxConcurrentRequests: mechanism.MaxConcurrentRequests, 277 DropCategories: drops, 278 // ChildPolicy is not set. Will be set based on xdsLBPolicy 279 } 280 281 if xdsLBPolicy == nil || xdsLBPolicy.Name == rrName { 282 // If lb policy is ROUND_ROBIN: 283 // - locality-picking policy is weighted_target 284 // - endpoint-picking policy is round_robin 285 dubbogoLogger.Infof("xds lb policy is %q, building config with weighted_target + round_robin", rrName) 286 // Child of weighted_target is hardcoded to round_robin. 287 wtConfig, addrs := localitiesToWeightedTarget(localities, priorityName, rrBalancerConfig) 288 clusterImplCfg.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: weightedtarget.Name, Config: wtConfig} 289 return clusterImplCfg, addrs, nil 290 } 291 292 if xdsLBPolicy.Name == rhName { 293 // If lb policy is RIHG_HASH, will build one ring_hash policy as child. 294 // The endpoints from all localities will be flattened to one addresses 295 // list, and the ring_hash policy will pick endpoints from it. 296 dubbogoLogger.Infof("xds lb policy is %q, building config with ring_hash", rhName) 297 addrs := localitiesToRingHash(localities, priorityName) 298 // Set child to ring_hash, note that the ring_hash config is from 299 // xdsLBPolicy. 300 clusterImplCfg.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: ringhash.Name, Config: xdsLBPolicy.Config} 301 return clusterImplCfg, addrs, nil 302 } 303 304 return nil, nil, fmt.Errorf("unsupported xds LB policy %q, not one of {%q,%q}", xdsLBPolicy.Name, rrName, rhName) 305 } 306 307 // localitiesToRingHash takes a list of localities (with the same priority), and 308 // generates a list of addresses. 309 // 310 // The addresses have path hierarchy set to [priority-name], so priority knows 311 // which child policy they are for. 312 func localitiesToRingHash(localities []resource.Locality, priorityName string) []resolver.Address { 313 var addrs []resolver.Address 314 for _, locality := range localities { 315 var lw uint32 = 1 316 if locality.Weight != 0 { 317 lw = locality.Weight 318 } 319 localityStr, err := locality.ID.ToString() 320 if err != nil { 321 localityStr = fmt.Sprintf("%+v", locality.ID) 322 } 323 for _, endpoint := range locality.Endpoints { 324 // Filter out all "unhealthy" endpoints (unknown and healthy are 325 // both considered to be healthy: 326 // https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus). 327 if endpoint.HealthStatus != resource.EndpointHealthStatusHealthy && endpoint.HealthStatus != resource.EndpointHealthStatusUnknown { 328 continue 329 } 330 331 var ew uint32 = 1 332 if endpoint.Weight != 0 { 333 ew = endpoint.Weight 334 } 335 336 // The weight of each endpoint is locality_weight * endpoint_weight. 337 ai := weightedroundrobin.AddrInfo{Weight: lw * ew} 338 addr := weightedroundrobin.SetAddrInfo(resolver.Address{Addr: endpoint.Address}, ai) 339 addr = hierarchy.Set(addr, []string{priorityName, localityStr}) 340 addr = resource.SetLocalityID(addr, locality.ID) 341 addrs = append(addrs, addr) 342 } 343 } 344 return addrs 345 } 346 347 // localitiesToWeightedTarget takes a list of localities (with the same 348 // priority), and generates a weighted target config, and list of addresses. 349 // 350 // The addresses have path hierarchy set to [priority-name, locality-name], so 351 // priority and weighted target know which child policy they are for. 352 func localitiesToWeightedTarget(localities []resource.Locality, priorityName string, childPolicy *internalserviceconfig.BalancerConfig) (*TargetLBConfig, []resolver.Address) { 353 weightedTargets := make(map[string]Target) 354 var addrs []resolver.Address 355 for _, locality := range localities { 356 localityStr, err := locality.ID.ToString() 357 if err != nil { 358 localityStr = fmt.Sprintf("%+v", locality.ID) 359 } 360 weightedTargets[localityStr] = Target{Weight: locality.Weight, ChildPolicy: childPolicy} 361 for _, endpoint := range locality.Endpoints { 362 // Filter out all "unhealthy" endpoints (unknown and healthy are 363 // both considered to be healthy: 364 // https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus). 365 if endpoint.HealthStatus != resource.EndpointHealthStatusHealthy && endpoint.HealthStatus != resource.EndpointHealthStatusUnknown { 366 continue 367 } 368 369 addr := resolver.Address{Addr: endpoint.Address} 370 if childPolicy.Name == weightedroundrobin.Name && endpoint.Weight != 0 { 371 ai := weightedroundrobin.AddrInfo{Weight: endpoint.Weight} 372 addr = weightedroundrobin.SetAddrInfo(addr, ai) 373 } 374 addr = hierarchy.Set(addr, []string{priorityName, localityStr}) 375 addr = resource.SetLocalityID(addr, locality.ID) 376 addrs = append(addrs, addr) 377 } 378 } 379 return &TargetLBConfig{Targets: weightedTargets}, addrs 380 }