Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[processor/lsminterval] Define cardinality limits and handle overflows #235

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

lahsivjar
Copy link
Contributor

@lahsivjar lahsivjar commented Nov 30, 2024

Related to: #141

For more details checkout #141 (comment) comment.

@lahsivjar lahsivjar changed the title Lsminterval limits [processor/lsminterval] Define cardinality limits and handle overflows Nov 30, 2024
processor/lsmintervalprocessor/config/config.go Outdated Show resolved Hide resolved
processor/lsmintervalprocessor/config/config.go Outdated Show resolved Hide resolved
Comment on lines 361 to 362
// Metrics doesn't have overflows (only datapoints have)
// Clone it *without* the datapoint data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this mean we risk OOM due to high cardinality metric names? Should the limit be on time series (metric + dimensions) per scope, rather than data points per scope? Or metrics per scope and time series per metric?

Copy link
Member

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a high level I think the approach looks good. As mentioned in a comment, I think we should have limits on all parts of the hierarchy, including metrics.

I ran the benchmarks, and it seems there's quite a drop in throughput.

processor/lsmintervalprocessor/processor.go Show resolved Hide resolved
processor/lsmintervalprocessor/processor.go Outdated Show resolved Hide resolved
processor/lsmintervalprocessor/internal/merger/model.go Outdated Show resolved Hide resolved
@lahsivjar
Copy link
Contributor Author

I ran the benchmarks, and it seems there's quite a drop in throughput.

Yeah, there is a considerable overhead. This is what I am working on right now -- to improve performance and drop some of the unneeded complexity around attribute matching and filtering.

@lahsivjar lahsivjar marked this pull request as ready for review December 10, 2024 19:26
@lahsivjar lahsivjar requested a review from a team as a code owner December 10, 2024 19:26
@lahsivjar
Copy link
Contributor Author

lahsivjar commented Dec 10, 2024

@axw I have addressed all of the comments other than one and I am marking this ready for review now. The performance consideration is still there and I have a few threads to chase down for improvements but I expect them to give minor gains, so, if you have any ideas (even rough) I will be more than happy to chase them down. One of the sources of the extra overhead is creating the lookup maps on unmarshaling the binary to the go struct -- previously I was doing this when required but now I am always doing this to simplify overflow handling a bit.

I have left adding metrics limits to a follow-up PR. I will also add a few more detailed overflow tests.

@lahsivjar lahsivjar requested a review from axw December 10, 2024 20:19
Copy link
Member

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary comments, ran out of time today

Copy link
Member

@axw axw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, mostly minor comments/questions

processor/lsmintervalprocessor/internal/merger/value.go Outdated Show resolved Hide resolved
Comment on lines 106 to 110
// Encode resource tracker at the 0th resource metrics
// TODO (lahsivjar): Is this safe? We don't ever remove
// resource metrics so it should be but best to check.
// Also, the limits checker should ensure max cardinality
// is greater than zero.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach we could take that does not involve piggybacking on attributes would be to first encode the non-overflowed ResourceMetrics, and then encode a tracker for each node in the tree in traversal order. i.e. one for the Resource, then for each Scope in that resource, etc.

That may increase the storage size slightly when there are no/few overflows, but would avoid having to iterate through the attributes when unmarshalling. If we were to do this, we would probably want to add some versioning to the format, e.g. so we can add another level of limits.

Did you consider this? Not sure if it's better at all, what you've done is fairly straightforward - I mostly worry about the performance cost of iterating through attributes when unmarshalling individual batches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the code to encode tracker separately. This resulted in lesser allocation and better performance. The encoding is pretty straightforward right now and there are some scopes for further improvements but I don't think they would be a bottleneck.

Comment on lines 270 to 271
// Remove any hanging resource or scope which failed to have any entries
// due to children reaching their limits.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing as the scope limit is per resource, and the metric/dp limit is per scope, when would we add an empty resource or scope?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens due to metric overflows, I have added a comment to explain this better:

	// Remove any hanging metrics, scope, or resource which failed to have any
	// entries due to datapoints overflowing. Overflowing datapoints discards
	// that metric and creates a new overflow metric which might result in the
	// original metric and its parent to exist without any datapoints.

processor/lsmintervalprocessor/internal/merger/value.go Outdated Show resolved Hide resolved
processor/lsmintervalprocessor/internal/merger/value.go Outdated Show resolved Hide resolved
}
dp := metric.Sum().DataPoints().AppendEmpty()
s.numberLookup[streamID] = dp
return dp, true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the caller could be simplified if we called otherDP.CopyTo(dp) here? Then instead of returning true here, we would return true only when the caller should update the returned data point. WDYT? (Same for other metric types of course.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps the caller could be simplified if we called otherDP.CopyTo(dp) here?

The issue with this is due to the mergeCumulative method in which we want to copy the datapoint only if it is newer.

processor/lsmintervalprocessor/internal/merger/value.go Outdated Show resolved Hide resolved
processor/lsmintervalprocessor/internal/merger/value.go Outdated Show resolved Hide resolved
Comment on lines 717 to 726
toDP, ok := addDP(toMetricID, fromDP)
if toDP == zero {
// Overflow, discard the datapoint
continue
}
if ok {
// New data point is created so we can copy the old data directly
fromDP.CopyTo(toDP)
continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing to zero feels like an unnecessarily expensive way to do this. See my other comment about returning true only when we should update. Then this could become

if toDP, exists := addDP(toMetricID, fromDP); exists {
	switch fromDP := any(fromDP).(type) {
	case pmetric.NumberDataPoint:
		mergeDeltaSumDP(fromDP, any(toDP).(pmetric.NumberDataPoint))
	case pmetric.HistogramDataPoint:
		mergeDeltaHistogramDP(fromDP, any(toDP).(pmetric.HistogramDataPoint))
	case pmetric.ExponentialHistogramDataPoint:
		mergeDeltaExponentialHistogramDP(fromDP, any(toDP).(pmetric.ExponentialHistogramDataPoint))
	}
}

@lahsivjar
Copy link
Contributor Author

Bringing this back to draft as I have a few good ideas for optimization and combined with @axw 's above comments I have a few more things to do here.

(I have also pushed a different way to encode limit trackers independent of the pmetric ds)

@lahsivjar lahsivjar marked this pull request as draft December 18, 2024 19:10
@lahsivjar
Copy link
Contributor Author

lahsivjar commented Dec 19, 2024

(deleted the above comment on performance improvements as they were void and incorrect)

I have made a silly mistake in the Merge function implementation due to which the recent commits are in an incorrect state 🤦 .

@lahsivjar lahsivjar marked this pull request as ready for review December 24, 2024 12:48
@lahsivjar lahsivjar requested a review from axw December 24, 2024 12:49
@lahsivjar
Copy link
Contributor Author

lahsivjar commented Dec 24, 2024

After the above changes, below is the benchmark diff from main:

Benchmark diff from `main`
name                                            old time/op    new time/op    delta
Aggregation/sum_cumulative-10                     10.0µs ± 5%    12.2µs ± 5%  +22.80%  (p=0.008 n=5+5)
Aggregation/sum_delta-10                          10.0µs ± 3%    12.3µs ± 2%  +23.51%  (p=0.008 n=5+5)
Aggregation/histogram_cumulative-10               10.1µs ± 3%    12.8µs ± 4%  +26.10%  (p=0.008 n=5+5)
Aggregation/histogram_delta-10                    10.4µs ± 2%    12.9µs ± 2%  +24.41%  (p=0.008 n=5+5)
Aggregation/exphistogram_cumulative-10            11.1µs ± 2%    12.7µs ± 1%  +14.86%  (p=0.008 n=5+5)
Aggregation/exphistogram_delta-10                 11.2µs ± 2%    13.4µs ± 8%  +19.06%  (p=0.008 n=5+5)
Aggregation/summary_enabled-10                    10.3µs ± 1%    13.2µs ± 4%  +28.69%  (p=0.008 n=5+5)
Aggregation/summary_passthrough-10                1.30µs ± 2%    1.39µs ± 6%   +6.82%  (p=0.048 n=5+5)
AggregationWithOTTL/sum_cumulative-10             12.0µs ± 3%    14.4µs ± 6%  +19.39%  (p=0.008 n=5+5)
AggregationWithOTTL/sum_delta-10                  12.2µs ± 4%    14.3µs ± 4%  +16.75%  (p=0.008 n=5+5)
AggregationWithOTTL/histogram_cumulative-10       12.5µs ± 0%    14.4µs ± 7%  +15.09%  (p=0.016 n=4+5)
AggregationWithOTTL/histogram_delta-10            11.8µs ± 7%    15.2µs ± 6%  +28.66%  (p=0.008 n=5+5)
AggregationWithOTTL/exphistogram_cumulative-10    13.2µs ± 6%    14.3µs ± 5%   +8.42%  (p=0.016 n=5+5)
AggregationWithOTTL/exphistogram_delta-10         13.2µs ± 4%    14.9µs ± 1%  +13.16%  (p=0.008 n=5+5)
AggregationWithOTTL/summary_enabled-10            12.7µs ± 5%    14.1µs ± 3%  +11.16%  (p=0.008 n=5+5)
AggregationWithOTTL/summary_passthrough-10        1.40µs ± 5%    1.32µs ± 3%   -5.87%  (p=0.032 n=5+5)

name                                            old alloc/op   new alloc/op   delta
Aggregation/sum_cumulative-10                     17.6kB ± 3%    23.8kB ± 1%  +35.18%  (p=0.016 n=5+4)
Aggregation/sum_delta-10                          17.6kB ± 1%    23.9kB ± 1%  +35.23%  (p=0.008 n=5+5)
Aggregation/histogram_cumulative-10               19.4kB ± 2%    24.7kB ± 5%  +27.10%  (p=0.008 n=5+5)
Aggregation/histogram_delta-10                    19.4kB ± 1%    24.7kB ± 5%  +27.42%  (p=0.008 n=5+5)
Aggregation/exphistogram_cumulative-10            21.7kB ± 0%    24.2kB ± 1%  +11.70%  (p=0.008 n=5+5)
Aggregation/exphistogram_delta-10                 22.2kB ± 1%    24.9kB ± 1%  +12.11%  (p=0.008 n=5+5)
Aggregation/summary_enabled-10                    18.5kB ± 1%    21.9kB ± 4%  +18.36%  (p=0.016 n=4+5)
Aggregation/summary_passthrough-10                1.38kB ± 0%    1.38kB ± 0%   +0.52%  (p=0.032 n=5+5)
AggregationWithOTTL/sum_cumulative-10             20.2kB ± 1%    23.6kB ± 2%  +16.63%  (p=0.008 n=5+5)
AggregationWithOTTL/sum_delta-10                  20.2kB ± 1%    23.7kB ± 1%  +17.35%  (p=0.008 n=5+5)
AggregationWithOTTL/histogram_cumulative-10       22.1kB ± 3%    26.9kB ± 2%  +21.82%  (p=0.008 n=5+5)
AggregationWithOTTL/histogram_delta-10            21.9kB ± 2%    26.5kB ± 0%  +20.91%  (p=0.008 n=5+5)
AggregationWithOTTL/exphistogram_cumulative-10    23.0kB ± 5%    26.8kB ± 1%  +16.58%  (p=0.008 n=5+5)
AggregationWithOTTL/exphistogram_delta-10         23.6kB ± 3%    27.5kB ± 1%  +16.38%  (p=0.008 n=5+5)
AggregationWithOTTL/summary_enabled-10            20.7kB ± 4%    24.3kB ± 2%  +17.67%  (p=0.008 n=5+5)
AggregationWithOTTL/summary_passthrough-10        1.38kB ± 1%    1.38kB ± 0%     ~     (p=0.159 n=5+5)

name                                            old allocs/op  new allocs/op  delta
Aggregation/sum_cumulative-10                        199 ± 1%       235 ± 0%  +18.41%  (p=0.008 n=5+5)
Aggregation/sum_delta-10                             200 ± 0%       237 ± 0%  +18.06%  (p=0.008 n=5+5)
Aggregation/histogram_cumulative-10                  211 ± 1%       246 ± 0%  +16.67%  (p=0.008 n=5+5)
Aggregation/histogram_delta-10                       213 ± 0%       248 ± 0%  +16.34%  (p=0.008 n=5+5)
Aggregation/exphistogram_cumulative-10               228 ± 0%       264 ± 0%  +15.79%  (p=0.008 n=5+5)
Aggregation/exphistogram_delta-10                    237 ± 0%       273 ± 0%  +15.19%  (p=0.016 n=5+4)
Aggregation/summary_enabled-10                       212 ± 0%       248 ± 0%  +16.98%  (p=0.029 n=4+4)
Aggregation/summary_passthrough-10                  37.0 ± 0%      37.0 ± 0%     ~     (all equal)
AggregationWithOTTL/sum_cumulative-10                227 ± 0%       263 ± 0%  +15.66%  (p=0.008 n=5+5)
AggregationWithOTTL/sum_delta-10                     229 ± 0%       264 ± 0%  +15.28%  (p=0.016 n=4+5)
AggregationWithOTTL/histogram_cumulative-10          244 ± 1%       281 ± 2%  +15.33%  (p=0.008 n=5+5)
AggregationWithOTTL/histogram_delta-10               246 ± 1%       279 ± 2%  +13.07%  (p=0.008 n=5+5)
AggregationWithOTTL/exphistogram_cumulative-10       256 ± 0%       292 ± 0%  +14.06%  (p=0.029 n=4+4)
AggregationWithOTTL/exphistogram_delta-10            265 ± 0%       301 ± 0%  +13.58%  (p=0.016 n=4+5)
AggregationWithOTTL/summary_enabled-10               240 ± 0%       275 ± 0%  +14.58%  (p=0.008 n=5+5)
AggregationWithOTTL/summary_passthrough-10          37.0 ± 0%      37.0 ± 0%     ~     (all equal)

TL;DR, there is still ~20% performance degradation with overflows. We could eliminate the overflow path when it is not defined but that will only give us a false sense of improvement since, for our use case, we would always have overflows defined.

I have another PR open here which optimizes the pebble options for our use case and that will have improvements to this PR too but the diff between with and without overflow will still be approximately same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants