google.golang.org/grpc@v1.72.2/xds/internal/balancer/clusterresolver/configbuilder.go (about) 1 /* 2 * 3 * Copyright 2021 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 clusterresolver 20 21 import ( 22 "encoding/json" 23 "fmt" 24 "sort" 25 26 "google.golang.org/grpc/internal/balancer/weight" 27 "google.golang.org/grpc/internal/hierarchy" 28 internalserviceconfig "google.golang.org/grpc/internal/serviceconfig" 29 "google.golang.org/grpc/resolver" 30 "google.golang.org/grpc/resolver/ringhash" 31 "google.golang.org/grpc/xds/internal" 32 "google.golang.org/grpc/xds/internal/balancer/clusterimpl" 33 "google.golang.org/grpc/xds/internal/balancer/outlierdetection" 34 "google.golang.org/grpc/xds/internal/balancer/priority" 35 "google.golang.org/grpc/xds/internal/balancer/wrrlocality" 36 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 37 ) 38 39 const million = 1000000 40 41 // priorityConfig is config for one priority. For example, if there's an EDS and a 42 // DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}]. 43 // 44 // Each priorityConfig corresponds to one discovery mechanism from the LBConfig 45 // generated by the CDS balancer. The CDS balancer resolves the cluster name to 46 // an ordered list of discovery mechanisms (if the top cluster is an aggregated 47 // cluster), one for each underlying cluster. 48 type priorityConfig struct { 49 mechanism DiscoveryMechanism 50 // edsResp is set only if type is EDS. 51 edsResp xdsresource.EndpointsUpdate 52 // endpoints is set only if type is DNS. 53 endpoints []resolver.Endpoint 54 // Each discovery mechanism has a name generator so that the child policies 55 // can reuse names between updates (EDS updates for example). 56 childNameGen *nameGenerator 57 } 58 59 // buildPriorityConfigJSON builds balancer config for the passed in 60 // priorities. 61 // 62 // The built tree of balancers (see test for the output struct). 63 // 64 // ┌────────┐ 65 // │priority│ 66 // └┬──────┬┘ 67 // │ │ 68 // ┌──────────▼─┐ ┌─▼──────────┐ 69 // │cluster_impl│ │cluster_impl│ 70 // └──────┬─────┘ └─────┬──────┘ 71 // │ │ 72 // ┌──────▼─────┐ ┌─────▼──────┐ 73 // │xDSLBPolicy │ │xDSLBPolicy │ (Locality and Endpoint picking layer) 74 // └────────────┘ └────────────┘ 75 func buildPriorityConfigJSON(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Endpoint, error) { 76 pc, endpoints, err := buildPriorityConfig(priorities, xdsLBPolicy) 77 if err != nil { 78 return nil, nil, fmt.Errorf("failed to build priority config: %v", err) 79 } 80 ret, err := json.Marshal(pc) 81 if err != nil { 82 return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err) 83 } 84 return ret, endpoints, nil 85 } 86 87 func buildPriorityConfig(priorities []priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Endpoint, error) { 88 var ( 89 retConfig = &priority.LBConfig{Children: make(map[string]*priority.Child)} 90 retEndpoints []resolver.Endpoint 91 ) 92 for _, p := range priorities { 93 switch p.mechanism.Type { 94 case DiscoveryMechanismTypeEDS: 95 names, configs, endpoints, err := buildClusterImplConfigForEDS(p.childNameGen, p.edsResp, p.mechanism, xdsLBPolicy) 96 if err != nil { 97 return nil, nil, err 98 } 99 retConfig.Priorities = append(retConfig.Priorities, names...) 100 retEndpoints = append(retEndpoints, endpoints...) 101 odCfgs := convertClusterImplMapToOutlierDetection(configs, p.mechanism.outlierDetection) 102 for n, c := range odCfgs { 103 retConfig.Children[n] = &priority.Child{ 104 Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: c}, 105 // Ignore all re-resolution from EDS children. 106 IgnoreReresolutionRequests: true, 107 } 108 } 109 continue 110 case DiscoveryMechanismTypeLogicalDNS: 111 name, config, endpoints := buildClusterImplConfigForDNS(p.childNameGen, p.endpoints, p.mechanism) 112 retConfig.Priorities = append(retConfig.Priorities, name) 113 retEndpoints = append(retEndpoints, endpoints...) 114 odCfg := makeClusterImplOutlierDetectionChild(config, p.mechanism.outlierDetection) 115 retConfig.Children[name] = &priority.Child{ 116 Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: odCfg}, 117 // Not ignore re-resolution from DNS children, they will trigger 118 // DNS to re-resolve. 119 IgnoreReresolutionRequests: false, 120 } 121 continue 122 } 123 } 124 return retConfig, retEndpoints, nil 125 } 126 127 func convertClusterImplMapToOutlierDetection(ciCfgs map[string]*clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) map[string]*outlierdetection.LBConfig { 128 odCfgs := make(map[string]*outlierdetection.LBConfig, len(ciCfgs)) 129 for n, c := range ciCfgs { 130 odCfgs[n] = makeClusterImplOutlierDetectionChild(c, odCfg) 131 } 132 return odCfgs 133 } 134 135 func makeClusterImplOutlierDetectionChild(ciCfg *clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) *outlierdetection.LBConfig { 136 odCfgRet := odCfg 137 odCfgRet.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: ciCfg} 138 return &odCfgRet 139 } 140 141 func buildClusterImplConfigForDNS(g *nameGenerator, endpoints []resolver.Endpoint, mechanism DiscoveryMechanism) (string, *clusterimpl.LBConfig, []resolver.Endpoint) { 142 // Endpoint picking policy for DNS is hardcoded to pick_first. 143 const childPolicy = "pick_first" 144 retEndpoints := make([]resolver.Endpoint, len(endpoints)) 145 pName := fmt.Sprintf("priority-%v", g.prefix) 146 for i, e := range endpoints { 147 retEndpoints[i] = hierarchy.SetInEndpoint(e, []string{pName}) 148 // Copy the nested address field as slice fields are shared by the 149 // iteration variable and the original slice. 150 retEndpoints[i].Addresses = append([]resolver.Address{}, e.Addresses...) 151 } 152 return pName, &clusterimpl.LBConfig{ 153 Cluster: mechanism.Cluster, 154 TelemetryLabels: mechanism.TelemetryLabels, 155 ChildPolicy: &internalserviceconfig.BalancerConfig{Name: childPolicy}, 156 MaxConcurrentRequests: mechanism.MaxConcurrentRequests, 157 LoadReportingServer: mechanism.LoadReportingServer, 158 }, retEndpoints 159 } 160 161 // buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for 162 // each priority, sorted by priority, and the addresses for each priority (with 163 // hierarchy attributes set). 164 // 165 // For example, if there are two priorities, the returned values will be 166 // - ["p0", "p1"] 167 // - map{"p0":p0_config, "p1":p1_config} 168 // - [p0_address_0, p0_address_1, p1_address_0, p1_address_1] 169 // - p0 addresses' hierarchy attributes are set to p0 170 func buildClusterImplConfigForEDS(g *nameGenerator, edsResp xdsresource.EndpointsUpdate, mechanism DiscoveryMechanism, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Endpoint, error) { 171 drops := make([]clusterimpl.DropConfig, 0, len(edsResp.Drops)) 172 for _, d := range edsResp.Drops { 173 drops = append(drops, clusterimpl.DropConfig{ 174 Category: d.Category, 175 RequestsPerMillion: d.Numerator * million / d.Denominator, 176 }) 177 } 178 179 // Localities of length 0 is triggered by an NACK or resource-not-found 180 // error before update, or an empty localities list in an update. In either 181 // case want to create a priority, and send down empty address list, causing 182 // TF for that priority. "If any discovery mechanism instance experiences an 183 // error retrieving data, and it has not previously reported any results, it 184 // should report a result that is a single priority with no endpoints." - 185 // A37 186 priorities := [][]xdsresource.Locality{{}} 187 if len(edsResp.Localities) != 0 { 188 priorities = groupLocalitiesByPriority(edsResp.Localities) 189 } 190 retNames := g.generate(priorities) 191 retConfigs := make(map[string]*clusterimpl.LBConfig, len(retNames)) 192 var retEndpoints []resolver.Endpoint 193 for i, pName := range retNames { 194 priorityLocalities := priorities[i] 195 cfg, endpoints, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, mechanism, drops, xdsLBPolicy) 196 if err != nil { 197 return nil, nil, nil, err 198 } 199 retConfigs[pName] = cfg 200 retEndpoints = append(retEndpoints, endpoints...) 201 } 202 return retNames, retConfigs, retEndpoints, nil 203 } 204 205 // groupLocalitiesByPriority returns the localities grouped by priority. 206 // 207 // The returned list is sorted from higher priority to lower. Each item in the 208 // list is a group of localities. 209 // 210 // For example, for L0-p0, L1-p0, L2-p1, results will be 211 // - [[L0, L1], [L2]] 212 func groupLocalitiesByPriority(localities []xdsresource.Locality) [][]xdsresource.Locality { 213 var priorityIntSlice []int 214 priorities := make(map[int][]xdsresource.Locality) 215 for _, locality := range localities { 216 priority := int(locality.Priority) 217 priorities[priority] = append(priorities[priority], locality) 218 priorityIntSlice = append(priorityIntSlice, priority) 219 } 220 // Sort the priorities based on the int value, deduplicate, and then turn 221 // the sorted list into a string list. This will be child names, in priority 222 // order. 223 sort.Ints(priorityIntSlice) 224 priorityIntSliceDeduped := dedupSortedIntSlice(priorityIntSlice) 225 ret := make([][]xdsresource.Locality, 0, len(priorityIntSliceDeduped)) 226 for _, p := range priorityIntSliceDeduped { 227 ret = append(ret, priorities[p]) 228 } 229 return ret 230 } 231 232 func dedupSortedIntSlice(a []int) []int { 233 if len(a) == 0 { 234 return a 235 } 236 i, j := 0, 1 237 for ; j < len(a); j++ { 238 if a[i] == a[j] { 239 continue 240 } 241 i++ 242 if i != j { 243 a[i] = a[j] 244 } 245 } 246 return a[:i+1] 247 } 248 249 // priorityLocalitiesToClusterImpl takes a list of localities (with the same 250 // priority), and generates a cluster impl policy config, and a list of 251 // addresses with their path hierarchy set to [priority-name, locality-name], so 252 // priority and the xDS LB Policy know which child policy each address is for. 253 func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priorityName string, mechanism DiscoveryMechanism, drops []clusterimpl.DropConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Endpoint, error) { 254 var retEndpoints []resolver.Endpoint 255 for _, locality := range localities { 256 var lw uint32 = 1 257 if locality.Weight != 0 { 258 lw = locality.Weight 259 } 260 localityStr, err := locality.ID.ToString() 261 if err != nil { 262 localityStr = fmt.Sprintf("%+v", locality.ID) 263 } 264 for _, endpoint := range locality.Endpoints { 265 // Filter out all "unhealthy" endpoints (unknown and healthy are 266 // both considered to be healthy: 267 // https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus). 268 if endpoint.HealthStatus != xdsresource.EndpointHealthStatusHealthy && endpoint.HealthStatus != xdsresource.EndpointHealthStatusUnknown { 269 continue 270 } 271 resolverEndpoint := resolver.Endpoint{} 272 for _, as := range endpoint.Addresses { 273 resolverEndpoint.Addresses = append(resolverEndpoint.Addresses, resolver.Address{Addr: as}) 274 } 275 resolverEndpoint = hierarchy.SetInEndpoint(resolverEndpoint, []string{priorityName, localityStr}) 276 resolverEndpoint = internal.SetLocalityIDInEndpoint(resolverEndpoint, locality.ID) 277 // "To provide the xds_wrr_locality load balancer information about 278 // locality weights received from EDS, the cluster resolver will 279 // populate a new locality weight attribute for each address The 280 // attribute will have the weight (as an integer) of the locality 281 // the address is part of." - A52 282 resolverEndpoint = wrrlocality.SetAddrInfoInEndpoint(resolverEndpoint, wrrlocality.AddrInfo{LocalityWeight: lw}) 283 var ew uint32 = 1 284 if endpoint.Weight != 0 { 285 ew = endpoint.Weight 286 } 287 resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: lw * ew}) 288 resolverEndpoint = ringhash.SetHashKey(resolverEndpoint, endpoint.HashKey) 289 retEndpoints = append(retEndpoints, resolverEndpoint) 290 } 291 } 292 return &clusterimpl.LBConfig{ 293 Cluster: mechanism.Cluster, 294 EDSServiceName: mechanism.EDSServiceName, 295 LoadReportingServer: mechanism.LoadReportingServer, 296 MaxConcurrentRequests: mechanism.MaxConcurrentRequests, 297 TelemetryLabels: mechanism.TelemetryLabels, 298 DropCategories: drops, 299 ChildPolicy: xdsLBPolicy, 300 }, retEndpoints, nil 301 }