github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/cmd/services/m3coordinator/ingest/write.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 ingest 22 23 import ( 24 "context" 25 "sync" 26 27 "github.com/m3db/m3/src/cmd/services/m3coordinator/downsample" 28 "github.com/m3db/m3/src/metrics/policy" 29 "github.com/m3db/m3/src/query/models" 30 "github.com/m3db/m3/src/query/storage" 31 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 32 "github.com/m3db/m3/src/query/ts" 33 xerrors "github.com/m3db/m3/src/x/errors" 34 "github.com/m3db/m3/src/x/instrument" 35 xsync "github.com/m3db/m3/src/x/sync" 36 xtime "github.com/m3db/m3/src/x/time" 37 38 "github.com/uber-go/tally" 39 ) 40 41 var ( 42 unaggregatedStoragePolicy = policy.NewStoragePolicy(0, xtime.Unit(0), 0) 43 unaggregatedStoragePolicies = []policy.StoragePolicy{ 44 unaggregatedStoragePolicy, 45 } 46 47 sourceTags = map[ts.SourceType]string{ 48 ts.SourceTypePrometheus: "prometheus", 49 ts.SourceTypeGraphite: "graphite", 50 ts.SourceTypeOpenMetrics: "open-metrics", 51 } 52 ) 53 54 // IterValue is the value returned by the iterator. 55 type IterValue struct { 56 Tags models.Tags 57 Datapoints ts.Datapoints 58 Attributes ts.SeriesAttributes 59 Unit xtime.Unit 60 Metadata ts.Metadata 61 Annotation []byte 62 } 63 64 // DownsampleAndWriteIter is an interface that can be implemented to use 65 // the WriteBatch method. 66 type DownsampleAndWriteIter interface { 67 Next() bool 68 Current() IterValue 69 Reset() error 70 Error() error 71 SetCurrentMetadata(ts.Metadata) 72 } 73 74 // DownsamplerAndWriter is the interface for the downsamplerAndWriter which 75 // writes metrics to the downsampler as well as to storage in unaggregated form. 76 type DownsamplerAndWriter interface { 77 Write( 78 ctx context.Context, 79 tags models.Tags, 80 datapoints ts.Datapoints, 81 unit xtime.Unit, 82 annotation []byte, 83 overrides WriteOptions, 84 source ts.SourceType, 85 ) error 86 87 WriteBatch( 88 ctx context.Context, 89 iter DownsampleAndWriteIter, 90 overrides WriteOptions, 91 ) BatchError 92 93 Storage() storage.Storage 94 95 Downsampler() downsample.Downsampler 96 } 97 98 // BatchError allows for access to individual errors. 99 type BatchError interface { 100 error 101 Errors() []error 102 LastError() error 103 } 104 105 // WriteOptions contains overrides for the downsampling mapping 106 // rules and storage policies for a given write. 107 type WriteOptions struct { 108 DownsampleMappingRules []downsample.AutoMappingRule 109 WriteStoragePolicies []policy.StoragePolicy 110 111 DownsampleOverride bool 112 WriteOverride bool 113 } 114 115 type downsamplerAndWriterMetrics struct { 116 dropped metricsBySource 117 written metricsBySource 118 } 119 120 type metricsBySource struct { 121 bySource map[ts.SourceType]tally.Counter 122 byUnknown tally.Counter 123 } 124 125 func (m metricsBySource) report(source ts.SourceType) { 126 counter, ok := m.bySource[source] 127 if !ok { 128 counter = m.byUnknown 129 } 130 counter.Inc(1) 131 } 132 133 // downsamplerAndWriter encapsulates the logic for writing data to the downsampler, 134 // as well as in unaggregated form to storage. 135 type downsamplerAndWriter struct { 136 store storage.Storage 137 downsampler downsample.Downsampler 138 workerPool xsync.PooledWorkerPool 139 140 metrics downsamplerAndWriterMetrics 141 } 142 143 // NewDownsamplerAndWriter creates a new downsampler and writer. 144 func NewDownsamplerAndWriter( 145 store storage.Storage, 146 downsampler downsample.Downsampler, 147 workerPool xsync.PooledWorkerPool, 148 instrumentOpts instrument.Options, 149 ) DownsamplerAndWriter { 150 scope := instrumentOpts.MetricsScope().SubScope("downsampler") 151 152 return &downsamplerAndWriter{ 153 store: store, 154 downsampler: downsampler, 155 workerPool: workerPool, 156 metrics: downsamplerAndWriterMetrics{ 157 dropped: newMetricsBySource(scope, "metrics_dropped"), 158 written: newMetricsBySource(scope, "metrics_written"), 159 }, 160 } 161 } 162 163 func newMetricsBySource(scope tally.Scope, name string) metricsBySource { 164 metrics := metricsBySource{ 165 bySource: make(map[ts.SourceType]tally.Counter, len(sourceTags)), 166 byUnknown: scope.Tagged(map[string]string{"source": "unknown"}).Counter(name), 167 } 168 169 for source, tag := range sourceTags { 170 metrics.bySource[source] = scope.Tagged(map[string]string{"source": tag}).Counter(name) 171 } 172 173 return metrics 174 } 175 176 func (d *downsamplerAndWriter) Write( 177 ctx context.Context, 178 tags models.Tags, 179 datapoints ts.Datapoints, 180 unit xtime.Unit, 181 annotation []byte, 182 overrides WriteOptions, 183 source ts.SourceType, 184 ) error { 185 var ( 186 multiErr = xerrors.NewMultiError() 187 dropUnaggregated bool 188 ) 189 190 if d.shouldDownsample(overrides) { 191 var err error 192 dropUnaggregated, err = d.writeToDownsampler(tags, datapoints, annotation, overrides) 193 if err != nil { 194 multiErr = multiErr.Add(err) 195 } 196 } 197 198 if dropUnaggregated { 199 d.metrics.dropped.report(source) 200 } else if d.shouldWrite(overrides) { 201 err := d.writeToStorage(ctx, tags, datapoints, unit, annotation, overrides, source) 202 if err != nil { 203 multiErr = multiErr.Add(err) 204 } 205 } 206 207 return multiErr.FinalError() 208 } 209 210 func (d *downsamplerAndWriter) shouldWrite( 211 overrides WriteOptions, 212 ) bool { 213 var ( 214 // Ensure storage set. 215 storageExists = d.store != nil 216 // Ensure using default storage policies or some storage policies set. 217 useDefaultStoragePolicies = !overrides.WriteOverride 218 // If caller tried to override the storage policies, make sure there's 219 // at least one. 220 _, writeOverride = d.writeOverrideStoragePolicies(overrides) 221 ) 222 // Only write directly to storage if the store exists, and caller wants to 223 // use the default storage policies, or they're trying to override the 224 // storage policies and they've provided at least one override to do so. 225 return storageExists && (useDefaultStoragePolicies || writeOverride) 226 } 227 228 func (d *downsamplerAndWriter) writeOverrideStoragePolicies( 229 overrides WriteOptions, 230 ) ([]policy.StoragePolicy, bool) { 231 writeOverride := overrides.WriteOverride && len(overrides.WriteStoragePolicies) > 0 232 if !writeOverride { 233 return nil, false 234 } 235 return overrides.WriteStoragePolicies, true 236 } 237 238 func (d *downsamplerAndWriter) shouldDownsample( 239 overrides WriteOptions, 240 ) bool { 241 var ( 242 // If they didn't request the mapping rules to be overridden, then assume they want the default 243 // ones. 244 useDefaultMappingRules = !overrides.DownsampleOverride 245 // If they did try and override the mapping rules, make sure they've provided at least one. 246 _, downsampleOverride = d.downsampleOverrideRules(overrides) 247 ) 248 // Only downsample if the downsampler is enabled, and they either want to use the default mapping 249 // rules, or they're trying to override the mapping rules and they've provided at least one 250 // override to do so. 251 return d.downsampler.Enabled() && (useDefaultMappingRules || downsampleOverride) 252 } 253 254 func (d *downsamplerAndWriter) downsampleOverrideRules( 255 overrides WriteOptions, 256 ) ([]downsample.AutoMappingRule, bool) { 257 downsampleOverride := overrides.DownsampleOverride && len(overrides.DownsampleMappingRules) > 0 258 if !downsampleOverride { 259 return nil, false 260 } 261 return overrides.DownsampleMappingRules, true 262 } 263 264 func (d *downsamplerAndWriter) writeToDownsampler( 265 tags models.Tags, 266 datapoints ts.Datapoints, 267 annotation []byte, 268 overrides WriteOptions, 269 ) (bool, error) { 270 if err := tags.Validate(); err != nil { 271 return false, err 272 } 273 274 appender, err := d.downsampler.NewMetricsAppender() 275 if err != nil { 276 return false, err 277 } 278 279 defer appender.Finalize() 280 281 for _, tag := range tags.Tags { 282 appender.AddTag(tag.Name, tag.Value) 283 } 284 285 if tags.Opts.IDSchemeType() == models.TypeGraphite { 286 // NB(r): This is gross, but if this is a graphite metric then 287 // we are going to set a special tag that means the downsampler 288 // will write a graphite ID. This should really be plumbed 289 // through the downsampler in general, but right now the aggregator 290 // does not allow context to be attached to a metric so when it calls 291 // back the context is lost currently. 292 // TODO_FIX_GRAPHITE_TAGGING: Using this string constant to track 293 // all places worth fixing this hack. There is at least one 294 // other path where flows back to the coordinator from the aggregator 295 // and this tag is interpreted, eventually need to handle more cleanly. 296 appender.AddTag(downsample.MetricsOptionIDSchemeTagName, 297 downsample.GraphiteIDSchemeTagValue) 298 } 299 300 // NB: we don't set series attributes on the sample appender options here. 301 // In practice this isn't needed because only the carbon ingest path comes through here. 302 var appenderOpts downsample.SampleAppenderOptions 303 if downsampleMappingRuleOverrides, ok := d.downsampleOverrideRules(overrides); ok { 304 appenderOpts = downsample.SampleAppenderOptions{ 305 Override: true, 306 OverrideRules: downsample.SamplesAppenderOverrideRules{ 307 MappingRules: downsampleMappingRuleOverrides, 308 }, 309 } 310 } 311 312 result, err := appender.SamplesAppender(appenderOpts) 313 if err != nil { 314 return false, err 315 } 316 317 for _, dp := range datapoints { 318 if result.ShouldDropTimestamp { 319 err = result.SamplesAppender.AppendUntimedGaugeSample(dp.Timestamp, dp.Value, annotation) 320 } else { 321 err = result.SamplesAppender.AppendGaugeSample( 322 dp.Timestamp, dp.Value, annotation, 323 ) 324 } 325 if err != nil { 326 return result.IsDropPolicyApplied, err 327 } 328 } 329 330 return result.IsDropPolicyApplied, nil 331 } 332 333 func (d *downsamplerAndWriter) writeToStorage( 334 ctx context.Context, 335 tags models.Tags, 336 datapoints ts.Datapoints, 337 unit xtime.Unit, 338 annotation []byte, 339 overrides WriteOptions, 340 source ts.SourceType, 341 ) error { 342 d.metrics.written.report(source) 343 344 storagePolicies, ok := d.writeOverrideStoragePolicies(overrides) 345 if !ok { 346 // NB(r): Allocate the write query at the top 347 // of the pooled worker instead of need to pass 348 // the options down the stack which can cause 349 // the stack to grow (and sometimes cause stack splits). 350 writeQuery, err := storage.NewWriteQuery(storage.WriteQueryOptions{ 351 Tags: tags, 352 Datapoints: datapoints, 353 Unit: unit, 354 Annotation: annotation, 355 Attributes: storageAttributesFromPolicy(unaggregatedStoragePolicy), 356 }) 357 if err != nil { 358 return err 359 } 360 return d.store.Write(ctx, writeQuery) 361 } 362 363 var ( 364 wg sync.WaitGroup 365 multiErr xerrors.MultiError 366 errLock sync.Mutex 367 ) 368 369 for _, p := range storagePolicies { 370 p := p // Capture for goroutine. 371 372 wg.Add(1) 373 d.workerPool.Go(func() { 374 // NB(r): Allocate the write query at the top 375 // of the pooled worker instead of need to pass 376 // the options down the stack which can cause 377 // the stack to grow (and sometimes cause stack splits). 378 writeQuery, err := storage.NewWriteQuery(storage.WriteQueryOptions{ 379 Tags: tags, 380 Datapoints: datapoints, 381 Unit: unit, 382 Annotation: annotation, 383 Attributes: storageAttributesFromPolicy(p), 384 }) 385 if err == nil { 386 err = d.store.Write(ctx, writeQuery) 387 } 388 if err != nil { 389 errLock.Lock() 390 multiErr = multiErr.Add(err) 391 errLock.Unlock() 392 } 393 394 wg.Done() 395 }) 396 } 397 398 wg.Wait() 399 return multiErr.FinalError() 400 } 401 402 func (d *downsamplerAndWriter) WriteBatch( 403 ctx context.Context, 404 iter DownsampleAndWriteIter, 405 overrides WriteOptions, 406 ) BatchError { 407 var ( 408 wg sync.WaitGroup 409 multiErr xerrors.MultiError 410 errLock sync.Mutex 411 addError = func(err error) { 412 errLock.Lock() 413 multiErr = multiErr.Add(err) 414 errLock.Unlock() 415 } 416 ) 417 418 if d.shouldDownsample(overrides) { 419 if errs := d.writeAggregatedBatch(iter, overrides); !errs.Empty() { 420 // Iterate and add through all the error to the multi error. It is 421 // ok not to use the addError method here as we are running single 422 // threaded at this point. 423 for _, err := range errs.Errors() { 424 multiErr = multiErr.Add(err) 425 } 426 } 427 } 428 429 // Reset the iter to write the unaggregated data. 430 resetErr := iter.Reset() 431 if resetErr != nil { 432 addError(resetErr) 433 } 434 435 if d.shouldWrite(overrides) && resetErr == nil { 436 // Write unaggregated. Spin up all the background goroutines that make 437 // network requests before we do the synchronous work of writing to the 438 // downsampler. 439 storagePolicies, ok := d.writeOverrideStoragePolicies(overrides) 440 if !ok { 441 storagePolicies = unaggregatedStoragePolicies 442 } 443 444 for iter.Next() { 445 value := iter.Current() 446 if value.Metadata.DropUnaggregated { 447 d.metrics.dropped.report(value.Attributes.Source) 448 continue 449 } 450 451 d.metrics.written.report(value.Attributes.Source) 452 453 for _, p := range storagePolicies { 454 p := p // Capture for lambda. 455 wg.Add(1) 456 d.workerPool.Go(func() { 457 // NB(r): Allocate the write query at the top 458 // of the pooled worker instead of need to pass 459 // the options down the stack which can cause 460 // the stack to grow (and sometimes cause stack splits). 461 writeQuery, err := storage.NewWriteQuery(storage.WriteQueryOptions{ 462 Tags: value.Tags, 463 Datapoints: value.Datapoints, 464 Unit: value.Unit, 465 Annotation: value.Annotation, 466 Attributes: storageAttributesFromPolicy(p), 467 }) 468 if err == nil { 469 err = d.store.Write(ctx, writeQuery) 470 } 471 if err != nil { 472 addError(err) 473 } 474 wg.Done() 475 }) 476 } 477 } 478 } 479 480 wg.Wait() 481 if multiErr.NumErrors() == 0 { 482 return nil 483 } 484 485 return multiErr 486 } 487 488 func (d *downsamplerAndWriter) writeAggregatedBatch( 489 iter DownsampleAndWriteIter, 490 overrides WriteOptions, 491 ) xerrors.MultiError { 492 var multiErr xerrors.MultiError 493 appender, err := d.downsampler.NewMetricsAppender() 494 if err != nil { 495 return multiErr.Add(err) 496 } 497 498 defer appender.Finalize() 499 500 for iter.Next() { 501 appender.NextMetric() 502 503 value := iter.Current() 504 if err := value.Tags.Validate(); err != nil { 505 multiErr = multiErr.Add(err) 506 continue 507 } 508 509 for _, tag := range value.Tags.Tags { 510 appender.AddTag(tag.Name, tag.Value) 511 } 512 513 if value.Tags.Opts.IDSchemeType() == models.TypeGraphite { 514 // NB(r): This is gross, but if this is a graphite metric then 515 // we are going to set a special tag that means the downsampler 516 // will write a graphite ID. This should really be plumbed 517 // through the downsampler in general, but right now the aggregator 518 // does not allow context to be attached to a metric so when it calls 519 // back the context is lost currently. 520 // TODO_FIX_GRAPHITE_TAGGING: Using this string constant to track 521 // all places worth fixing this hack. There is at least one 522 // other path where flows back to the coordinator from the aggregator 523 // and this tag is interpreted, eventually need to handle more cleanly. 524 appender.AddTag(downsample.MetricsOptionIDSchemeTagName, 525 downsample.GraphiteIDSchemeTagValue) 526 } 527 528 opts := downsample.SampleAppenderOptions{ 529 SeriesAttributes: value.Attributes, 530 } 531 if downsampleMappingRuleOverrides, ok := d.downsampleOverrideRules(overrides); ok { 532 opts = downsample.SampleAppenderOptions{ 533 Override: true, 534 OverrideRules: downsample.SamplesAppenderOverrideRules{ 535 MappingRules: downsampleMappingRuleOverrides, 536 }, 537 } 538 } 539 540 result, err := appender.SamplesAppender(opts) 541 if err != nil { 542 multiErr = multiErr.Add(err) 543 continue 544 } 545 546 if result.IsDropPolicyApplied { 547 iter.SetCurrentMetadata(ts.Metadata{DropUnaggregated: true}) 548 } 549 550 for _, dp := range value.Datapoints { 551 switch value.Attributes.M3Type { 552 case ts.M3MetricTypeGauge: 553 if result.ShouldDropTimestamp { 554 err = result.SamplesAppender.AppendUntimedGaugeSample(dp.Timestamp, dp.Value, value.Annotation) 555 } else { 556 err = result.SamplesAppender.AppendGaugeSample( 557 dp.Timestamp, dp.Value, value.Annotation, 558 ) 559 } 560 case ts.M3MetricTypeCounter: 561 if result.ShouldDropTimestamp { 562 err = result.SamplesAppender.AppendUntimedCounterSample( 563 dp.Timestamp, int64(dp.Value), value.Annotation) 564 } else { 565 err = result.SamplesAppender.AppendCounterSample( 566 dp.Timestamp, int64(dp.Value), value.Annotation, 567 ) 568 } 569 case ts.M3MetricTypeTimer: 570 if result.ShouldDropTimestamp { 571 err = result.SamplesAppender.AppendUntimedTimerSample(dp.Timestamp, dp.Value, value.Annotation) 572 } else { 573 err = result.SamplesAppender.AppendTimerSample( 574 dp.Timestamp, dp.Value, value.Annotation, 575 ) 576 } 577 } 578 if err != nil { 579 // If we see an error break out so we can try processing the 580 // next datapoint. 581 multiErr = multiErr.Add(err) 582 } 583 } 584 } 585 586 return multiErr.Add(iter.Error()) 587 } 588 589 func (d *downsamplerAndWriter) Downsampler() downsample.Downsampler { 590 return d.downsampler 591 } 592 593 func (d *downsamplerAndWriter) Storage() storage.Storage { 594 return d.store 595 } 596 597 func storageAttributesFromPolicy( 598 p policy.StoragePolicy, 599 ) storagemetadata.Attributes { 600 attributes := storagemetadata.Attributes{ 601 MetricsType: storagemetadata.UnaggregatedMetricsType, 602 } 603 if p != unaggregatedStoragePolicy { 604 attributes = storagemetadata.Attributes{ 605 // Assume all overridden storage policies are for aggregated namespaces. 606 MetricsType: storagemetadata.AggregatedMetricsType, 607 Resolution: p.Resolution().Window, 608 Retention: p.Retention().Duration(), 609 } 610 } 611 return attributes 612 }