github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/storage/m3/cluster_resolver.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package m3 22 23 import ( 24 "fmt" 25 "sort" 26 27 "github.com/m3db/m3/src/query/storage" 28 "github.com/m3db/m3/src/query/storage/m3/consolidators" 29 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 30 xerrors "github.com/m3db/m3/src/x/errors" 31 xtime "github.com/m3db/m3/src/x/time" 32 ) 33 34 type unaggregatedNamespaceType uint8 35 36 const ( 37 partiallySatisfiesRange unaggregatedNamespaceType = iota 38 fullySatisfiesRange 39 disabled 40 ) 41 42 type unaggregatedNamespaceDetails struct { 43 satisfies unaggregatedNamespaceType 44 clusterNamespace ClusterNamespace 45 } 46 47 type resolvedNamespaces []resolvedNamespace 48 49 type resolvedNamespace struct { 50 ClusterNamespace 51 narrowing narrowing 52 } 53 54 func resolved(ns ClusterNamespace) resolvedNamespace { 55 return resolvedNamespace{ClusterNamespace: ns} 56 } 57 58 // resolveUnaggregatedNamespaceForQuery determines if the unaggregated namespace 59 // should be used, and if so, determines if it fully satisfies the query range. 60 func resolveUnaggregatedNamespaceForQuery( 61 now, start xtime.UnixNano, 62 unaggregated ClusterNamespace, 63 opts *storage.FanoutOptions, 64 ) unaggregatedNamespaceDetails { 65 if opts.FanoutUnaggregated == storage.FanoutForceDisable { 66 return unaggregatedNamespaceDetails{satisfies: disabled} 67 } 68 69 var ( 70 retention = unaggregated.Options().Attributes().Retention 71 unaggregatedStart = now.Add(-1 * retention) 72 ) 73 74 satisfies := fullySatisfiesRange 75 if unaggregatedStart.After(start) { 76 satisfies = partiallySatisfiesRange 77 } 78 79 return unaggregatedNamespaceDetails{ 80 clusterNamespace: unaggregated, 81 satisfies: satisfies, 82 } 83 } 84 85 // resolveClusterNamespacesForQuery returns the namespaces that need to be 86 // fanned out to depending on the query time and the namespaces configured. 87 func resolveClusterNamespacesForQuery( 88 now, 89 start, 90 end xtime.UnixNano, 91 clusters Clusters, 92 opts *storage.FanoutOptions, 93 restrict *storage.RestrictQueryOptions, 94 relatedQueryOpts *storage.RelatedQueryOptions, 95 ) (consolidators.QueryFanoutType, resolvedNamespaces, error) { 96 // Calculate a new start time if related query opts are present. 97 // NB: We do not calculate a new end time because it does not factor 98 // into namespace selection. 99 namespaceSelectionStart := start 100 if relatedQueryOpts != nil { 101 for _, timeRange := range relatedQueryOpts.Timespans { 102 if timeRange.Start < namespaceSelectionStart { 103 namespaceSelectionStart = timeRange.Start 104 } 105 } 106 } 107 108 // 1. First resolve the logical plan. 109 fanout, namespaces, err := resolveClusterNamespacesForQueryLogicalPlan(now, 110 namespaceSelectionStart, end, clusters, opts, restrict) 111 if err != nil { 112 return fanout, namespaces, err 113 } 114 115 // 2. Create physical plan. 116 // Now de-duplicate any namespaces that might be fetched twice due to 117 // the fact some of the same namespaces are reused once for unaggregated 118 // and another for aggregated rollups (which don't collide with timeseries). 119 filtered := namespaces[:0] 120 for _, ns := range namespaces { 121 keep := true 122 // Small enough that we can do n^2 here instead of creating a map, 123 // usually less than 4 namespaces resolved. 124 for _, existing := range filtered { 125 if ns.NamespaceID().Equal(existing.NamespaceID()) { 126 keep = false 127 break 128 } 129 } 130 if !keep { 131 continue 132 } 133 filtered = append(filtered, ns) 134 } 135 136 return fanout, filtered, nil 137 } 138 139 // resolveClusterNamespacesForQueryLogicalPlan resolves the logical plan 140 // for namespaces to query. 141 // nolint: unparam 142 func resolveClusterNamespacesForQueryLogicalPlan( 143 now, start, end xtime.UnixNano, 144 clusters Clusters, 145 opts *storage.FanoutOptions, 146 restrict *storage.RestrictQueryOptions, 147 ) (consolidators.QueryFanoutType, resolvedNamespaces, error) { 148 if typeRestrict := restrict.GetRestrictByType(); typeRestrict != nil { 149 // If a specific restriction is set, then attempt to satisfy. 150 return resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions(now, 151 start, clusters, *typeRestrict) 152 } 153 154 if typesRestrict := restrict.GetRestrictByTypes(); typesRestrict != nil { 155 // If a specific restriction is set, then attempt to satisfy. 156 return resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions(now, 157 start, clusters, typesRestrict) 158 } 159 160 // First check if the unaggregated cluster can fully satisfy the query range. 161 // If so, return it and shortcircuit, as unaggregated will necessarily have 162 // every metric. 163 ns, initialized := clusters.UnaggregatedClusterNamespace() 164 if !initialized { 165 return consolidators.NamespaceInvalid, nil, errUnaggregatedNamespaceUninitialized 166 } 167 168 unaggregated := resolveUnaggregatedNamespaceForQuery(now, start, ns, opts) 169 if unaggregated.satisfies == fullySatisfiesRange { 170 return consolidators.NamespaceCoversAllQueryRange, 171 resolvedNamespaces{resolved(unaggregated.clusterNamespace)}, 172 nil 173 } 174 175 if opts.FanoutAggregated == storage.FanoutForceDisable { 176 if unaggregated.satisfies == partiallySatisfiesRange { 177 return consolidators.NamespaceCoversPartialQueryRange, 178 resolvedNamespaces{resolved(unaggregated.clusterNamespace)}, nil 179 } 180 181 return consolidators.NamespaceInvalid, nil, errUnaggregatedAndAggregatedDisabled 182 } 183 184 // The filter function will drop namespaces which do not cover the entire 185 // query range from contention. 186 // 187 // NB: if fanout aggregation is forced on, the filter instead forces clusters 188 // that do not cover the range to be set as partially aggregated. 189 coversRangeFilter := newCoversRangeFilter(coversRangeFilterOptions{ 190 now: now, 191 queryStart: start, 192 }) 193 194 // Filter aggregated namespaces by filter function and options. 195 var r reusedAggregatedNamespaceSlices 196 r = aggregatedNamespaces(clusters.ClusterNamespaces(), r, coversRangeFilter, now, end, opts) 197 198 // If any of the aggregated clusters have a complete set of metrics, use 199 // those that have the smallest resolutions, supplemented by lower resolution 200 // partially aggregated metrics. 201 if len(r.completeAggregated) > 0 { 202 sort.Stable(resolvedNamespacesByResolutionAsc(r.completeAggregated)) 203 // Take most granular complete aggregated namespace. 204 result := r.completeAggregated[:1] 205 completedAttrs := result[0].Options().Attributes() 206 // Also include any finer grain partially aggregated namespaces that 207 // may contain a matching metric. 208 for _, n := range r.partialAggregated { 209 if n.Options().Attributes().Resolution < completedAttrs.Resolution { 210 // More granular resolution. 211 result = append(result, n) 212 } 213 } 214 215 if unaggregatedNarrowed, ok := mustStitchWithUnaggregated(result[0].narrowing, unaggregated); ok { 216 result = append(result, unaggregatedNarrowed) 217 } 218 219 return consolidators.NamespaceCoversAllQueryRange, result, nil 220 } 221 222 // No complete aggregated namespaces can definitely fulfill the query, 223 // so take the longest retention completed aggregated namespace to return 224 // as much data as possible, along with any partially aggregated namespaces 225 // that have either same retention and lower resolution or longer retention 226 // than the complete aggregated namespace. 227 r = aggregatedNamespaces(clusters.ClusterNamespaces(), r, nil, now, end, opts) 228 if len(r.completeAggregated) == 0 { 229 // Absolutely no complete aggregated namespaces, need to fanout to all 230 // partial aggregated namespaces as well as the unaggregated cluster 231 // as we have no idea which has the longest retention. 232 result := r.partialAggregated 233 // If unaggregated namespace can partially satisfy this range, add it as a 234 // fanout contender. 235 if unaggregated.satisfies == partiallySatisfiesRange { 236 result = append(result, resolved(unaggregated.clusterNamespace)) 237 } 238 239 // If any namespace currently in contention does not cover the entire query 240 // range, set query fanout type to namespaceCoversPartialQueryRange. 241 for _, n := range result { 242 if !coversRangeFilter(n) { 243 return consolidators.NamespaceCoversPartialQueryRange, result, nil 244 } 245 } 246 247 // Otherwise, all namespaces cover the query range. 248 return consolidators.NamespaceCoversAllQueryRange, result, nil 249 } 250 251 // Return the longest retention aggregated namespace and 252 // any potentially more granular or longer retention partial 253 // aggregated namespaces. 254 sort.Stable(sort.Reverse(resolvedNamespacesByRetentionAsc(r.completeAggregated))) 255 256 // Take longest retention complete aggregated namespace or the unaggregated 257 // cluster if that is longer than the longest aggregated namespace. 258 result := r.completeAggregated[:1] 259 completedAttrs := result[0].Options().Attributes() 260 if unaggregated.satisfies == partiallySatisfiesRange { 261 unaggregatedAttrs := unaggregated.clusterNamespace.Options().Attributes() 262 if completedAttrs.Retention <= unaggregatedAttrs.Retention { 263 // If the longest aggregated cluster for some reason has lower retention 264 // than the unaggregated cluster then we prefer the unaggregated cluster 265 // as it has a complete data set and is always the most granular. 266 result[0] = resolved(unaggregated.clusterNamespace) 267 completedAttrs = unaggregated.clusterNamespace.Options().Attributes() 268 } 269 } 270 271 if unaggregatedNarrowed, ok := mustStitchWithUnaggregated(result[0].narrowing, unaggregated); ok { 272 result = append(result, unaggregatedNarrowed) 273 } 274 275 // Take any partially aggregated namespaces with longer retention or 276 // same retention with more granular resolution that may contain 277 // a matching metric. 278 for _, n := range r.partialAggregated { 279 attrs := n.Options().Attributes() 280 if attrs.Retention > completedAttrs.Retention { 281 // Higher retention. 282 result = append(result, n) 283 } else if attrs.Retention == completedAttrs.Retention && 284 attrs.Resolution < completedAttrs.Resolution { 285 // Same retention but more granular resolution. 286 result = append(result, n) 287 } 288 } 289 290 return consolidators.NamespaceCoversPartialQueryRange, result, nil 291 } 292 293 type reusedAggregatedNamespaceSlices struct { 294 completeAggregated resolvedNamespaces 295 partialAggregated resolvedNamespaces 296 } 297 298 func (slices reusedAggregatedNamespaceSlices) reset( 299 size int, 300 ) reusedAggregatedNamespaceSlices { 301 // Initialize arrays if yet uninitialized. 302 if slices.completeAggregated == nil { 303 slices.completeAggregated = make(resolvedNamespaces, 0, size) 304 } else { 305 slices.completeAggregated = slices.completeAggregated[:0] 306 } 307 308 if slices.partialAggregated == nil { 309 slices.partialAggregated = make(resolvedNamespaces, 0, size) 310 } else { 311 slices.partialAggregated = slices.partialAggregated[:0] 312 } 313 314 return slices 315 } 316 317 // aggregatedNamespaces filters out clusters that do not meet the filter 318 // condition, and organizes remaining clusters in two lists if possible. 319 // 320 // NB: If fanout aggregation is disabled, no clusters will be returned as either 321 // partial or complete candidates. If fanout aggregation is forced to enabled 322 // then no filter is applied, and all namespaces are considered viable. In this 323 // case, the filter is used to determine if returned namespaces have the 324 // complete set of metrics. 325 // 326 // NB: If fanout optimization is enabled, add any aggregated namespaces that 327 // have a complete set of metrics to the completeAggregated slice list. If this 328 // optimization is disabled, or if none of the aggregated namespaces are 329 // guaranteed to have a complete set of all metrics, they are added to the 330 // partialAggregated list. 331 func aggregatedNamespaces( 332 all ClusterNamespaces, 333 slices reusedAggregatedNamespaceSlices, 334 filter func(ClusterNamespace) bool, 335 now, end xtime.UnixNano, 336 opts *storage.FanoutOptions, 337 ) reusedAggregatedNamespaceSlices { 338 // Reset reused slices. 339 slices = slices.reset(len(all)) 340 341 // Otherwise the default and force enable is to fanout and treat 342 // the aggregated namespaces differently (depending on whether they 343 // have all the data). 344 for _, namespace := range all { 345 nsOpts := namespace.Options() 346 if nsOpts.Attributes().MetricsType != storagemetadata.AggregatedMetricsType { 347 // Not an aggregated cluster. 348 continue 349 } 350 351 if filter != nil && !filter(namespace) { 352 // Fails to satisfy filter. 353 continue 354 } 355 356 resolvedNs := resolved(namespace) 357 358 var ( 359 dataLatency = nsOpts.DataLatency() 360 resolution = nsOpts.Attributes().Resolution 361 dataAvailableUntil = now.Add(-dataLatency).Truncate(resolution) 362 ) 363 if dataLatency > 0 && end.After(dataAvailableUntil) { 364 resolvedNs.narrowing.end = dataAvailableUntil 365 } 366 367 // If not optimizing fanout to aggregated namespaces, set all aggregated 368 // namespaces satisfying the filter as partially aggregated, as all metrics 369 // do not necessarily appear in all namespaces, depending on configuration. 370 if opts.FanoutAggregatedOptimized == storage.FanoutForceDisable { 371 slices.partialAggregated = append(slices.partialAggregated, resolvedNs) 372 continue 373 } 374 375 // Otherwise, check downsample options for the namespace and determine if 376 // this namespace is set as containing all metrics. 377 downsampleOpts, err := nsOpts.DownsampleOptions() 378 if err != nil { 379 continue 380 } 381 382 if downsampleOpts.All { 383 // This namespace has a complete set of metrics. Ensure that it passes 384 // the filter if it was a forced addition, otherwise it may be too short 385 // to cover the entire range and should be considered a partial result. 386 slices.completeAggregated = append(slices.completeAggregated, resolvedNs) 387 continue 388 } 389 390 // This namespace does not necessarily have a complete set of metrics. 391 slices.partialAggregated = append(slices.partialAggregated, resolvedNs) 392 } 393 394 return slices 395 } 396 397 // resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions returns the cluster 398 // namespace referred to by the restrict fetch options or an error if it 399 // cannot be found. 400 func resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions( 401 now, start xtime.UnixNano, 402 clusters Clusters, 403 restrict storage.RestrictByType, 404 ) (consolidators.QueryFanoutType, resolvedNamespaces, error) { 405 coversRangeFilter := newCoversRangeFilter(coversRangeFilterOptions{ 406 now: now, 407 queryStart: start, 408 }) 409 410 result := func( 411 namespace ClusterNamespace, 412 err error, 413 ) (consolidators.QueryFanoutType, resolvedNamespaces, error) { 414 if err != nil { 415 return 0, nil, err 416 } 417 418 if coversRangeFilter(namespace) { 419 return consolidators.NamespaceCoversAllQueryRange, 420 resolvedNamespaces{resolved(namespace)}, nil 421 } 422 423 return consolidators.NamespaceCoversPartialQueryRange, 424 resolvedNamespaces{resolved(namespace)}, nil 425 } 426 427 switch restrict.MetricsType { 428 case storagemetadata.UnaggregatedMetricsType: 429 ns, ok := clusters.UnaggregatedClusterNamespace() 430 if !ok { 431 return result(nil, 432 fmt.Errorf("could not find unaggregated namespace for storage policy: %v", 433 restrict.StoragePolicy.String())) 434 } 435 return result(ns, nil) 436 case storagemetadata.AggregatedMetricsType: 437 ns, ok := clusters.AggregatedClusterNamespace(RetentionResolution{ 438 Retention: restrict.StoragePolicy.Retention().Duration(), 439 Resolution: restrict.StoragePolicy.Resolution().Window, 440 }) 441 if !ok { 442 err := xerrors.NewInvalidParamsError( 443 fmt.Errorf("could not find namespace for storage policy: %v", 444 restrict.StoragePolicy.String())) 445 return result(nil, err) 446 } 447 448 return result(ns, nil) 449 default: 450 err := xerrors.NewInvalidParamsError( 451 fmt.Errorf("unrecognized metrics type: %v", restrict.MetricsType)) 452 return result(nil, err) 453 } 454 } 455 456 // resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions returns the cluster 457 // namespace referred to by the array of restrict fetch options or an error if it 458 // cannot be found. 459 func resolveClusterNamespacesForQueryWithTypesRestrictQueryOptions( 460 now, start xtime.UnixNano, 461 clusters Clusters, 462 restricts []*storage.RestrictByType, 463 ) (consolidators.QueryFanoutType, resolvedNamespaces, error) { 464 var ( 465 namespaces resolvedNamespaces 466 fanoutType consolidators.QueryFanoutType 467 ) 468 for _, restrict := range restricts { 469 t, ns, err := resolveClusterNamespacesForQueryWithTypeRestrictQueryOptions(now, start, clusters, *restrict) 470 if err != nil { 471 return consolidators.NamespaceInvalid, nil, err 472 } 473 namespaces = append(namespaces, ns...) 474 if t == consolidators.NamespaceCoversPartialQueryRange || 475 fanoutType == consolidators.NamespaceCoversPartialQueryRange { 476 fanoutType = consolidators.NamespaceCoversPartialQueryRange 477 } else { 478 fanoutType = consolidators.NamespaceCoversAllQueryRange 479 } 480 } 481 return fanoutType, namespaces, nil 482 } 483 484 func mustStitchWithUnaggregated( 485 narrowing narrowing, 486 unaggregated unaggregatedNamespaceDetails, 487 ) (resolvedNamespace, bool) { 488 if !narrowing.end.IsZero() { 489 // completeAggregated namespace will not have the most recent data available, will 490 // have to query unaggregated namespace for it and then stitch the responses together. 491 unaggregatedNarrowed := resolved(unaggregated.clusterNamespace) 492 unaggregatedNarrowed.narrowing.start = narrowing.end 493 494 return unaggregatedNarrowed, true 495 } 496 497 return resolvedNamespace{}, false 498 } 499 500 type coversRangeFilterOptions struct { 501 now xtime.UnixNano 502 queryStart xtime.UnixNano 503 } 504 505 func newCoversRangeFilter(opts coversRangeFilterOptions) func(namespace ClusterNamespace) bool { 506 return func(namespace ClusterNamespace) bool { 507 // Include only if can fulfill the entire time range of the query 508 clusterStart := opts.now.Add(-1 * namespace.Options().Attributes().Retention) 509 return !clusterStart.After(opts.queryStart) 510 } 511 } 512 513 type resolvedNamespacesByResolutionAsc resolvedNamespaces 514 515 func (a resolvedNamespacesByResolutionAsc) Len() int { return len(a) } 516 func (a resolvedNamespacesByResolutionAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 517 func (a resolvedNamespacesByResolutionAsc) Less(i, j int) bool { 518 return a[i].Options().Attributes().Resolution < a[j].Options().Attributes().Resolution 519 } 520 521 type resolvedNamespacesByRetentionAsc resolvedNamespaces 522 523 func (a resolvedNamespacesByRetentionAsc) Len() int { return len(a) } 524 func (a resolvedNamespacesByRetentionAsc) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 525 func (a resolvedNamespacesByRetentionAsc) Less(i, j int) bool { 526 return a[i].Options().Attributes().Retention < a[j].Options().Attributes().Retention 527 }