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

[query] Increase perf for temporal functions #2049

Merged
merged 19 commits into from Dec 10, 2019
Merged

Conversation

arnikola
Copy link
Collaborator

@arnikola arnikola commented Nov 25, 2019

This PR contains improvements across the board for queries, largely focussed on temporal queries:

  • All endpoints support gziped responses for smaller payloads.
  • Prometheus remote read endpoint now uses a specialized path, allowing some memory usage optimizations; queries using this path get parsed directly into Prometheus proto format rather than a transitional format.
  • UnconsolidatedSeries now doesn't do consolidation. Shocker! (Before it was aligning points to steps, then instantly unrolling them within temporal functions, allocing 2 unused data structures).
  • Temporal functions greatly simplified by removing the logic that allowed split blocks to be worked on. This is likely to be re-added in future after some of the surrounding logic is simplified somewhat; it was currently unoptimized and quite confusing.
  • Temporal functions now compare timestamps by int64s vs Time.Times
  • Temporal functions now batch to groups sized by CPU count and are processed concurrently.

@arnikola
Copy link
Collaborator Author

arnikola commented Nov 27, 2019

FWIW, benchmark results for just the encoded series iterator improvements (top is existing, bottom is new):

BenchmarkNextIteration/10_series-12         	    1375	    773605 ns/op	  358427 B/op	    1400 allocs/op
BenchmarkNextIteration/100_series-12        	     398	   3082631 ns/op	 3483303 B/op	   10402 allocs/op
BenchmarkNextIteration/200_series-12        	     240	   4988956 ns/op	 6900974 B/op	   20404 allocs/op
BenchmarkNextIteration/500_series-12        	     100	  10027566 ns/op	17243642 B/op	   50409 allocs/op
BenchmarkNextIteration/1000_series-12       	      62	  17959736 ns/op	34472746 B/op	  100415 allocs/op
BenchmarkNextIteration/2000_series-12       	      36	  32262755 ns/op	68930819 B/op	  200426 allocs/op

BenchmarkNextIteration/10_series-12         	    1764	    689199 ns/op	   14422 B/op	     300 allocs/op
BenchmarkNextIteration/100_series-12        	     723	   1640192 ns/op	   14448 B/op	     301 allocs/op
BenchmarkNextIteration/200_series-12        	     534	   2217000 ns/op	   14464 B/op	     301 allocs/op
BenchmarkNextIteration/500_series-12        	     352	   3378426 ns/op	   14502 B/op	     302 allocs/op
BenchmarkNextIteration/1000_series-12       	     261	   4601541 ns/op	   14547 B/op	     303 allocs/op
BenchmarkNextIteration/2000_series-12       	     183	   6483285 ns/op	   14610 B/op	     304 allocs/op

@@ -174,6 +175,14 @@ func (s *mockStorage) Fetch(
return s.fetchResult.results[idx], s.fetchResult.err
}

func (s *mockStorage) FetchProm(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file should be replaced by mocks when possible

@codecov
Copy link

codecov bot commented Nov 27, 2019

Codecov Report

Merging #2049 into master will increase coverage by 3.8%.
The diff coverage is 37.7%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master   #2049     +/-   ##
========================================
+ Coverage    62.6%   66.4%   +3.8%     
========================================
  Files         996    1009     +13     
  Lines       86851   86931     +80     
========================================
+ Hits        54386   57742   +3356     
+ Misses      28192   25326   -2866     
+ Partials     4273    3863    -410
Flag Coverage Δ
#aggregator 61.1% <ø> (-2.2%) ⬇️
#cluster 71.2% <ø> (-14.4%) ⬇️
#collector 43.4% <ø> (-5.4%) ⬇️
#dbnode 69.2% <ø> (+2.6%) ⬆️
#m3em 73.2% <ø> (+17.4%) ⬆️
#m3ninx 65.5% <ø> (+18.6%) ⬆️
#m3nsch 63.6% <ø> (-36.4%) ⬇️
#metrics 51.9% <ø> (-48.1%) ⬇️
#msg 74.9% <ø> (-25.1%) ⬇️
#query 47.1% <37.7%> (-22.5%) ⬇️
#x 76.1% <ø> (-0.3%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7e54ccf...e62c84b. Read the comment docs.

batch.Iter,
m,
c.processor,
seriesMeta,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This needs to be resultSeriesMeta

@arnikola arnikola marked this pull request as ready for review November 27, 2019 22:06
// PopulateColumns sets all columns to the given row size.
func (cb ColumnBlockBuilder) PopulateColumns(size int) {
for i := range cb.block.columns {
cb.block.columns[i] = column{Values: make([]float64, size)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Worth working out the full size and doing a block alloc upfront?

i.e. all := make([]float64, size*len(cb.block.columns)) and then subdividing for each one? That would reduce num of individual allocations, we've seen good speed ups from bulk allocs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Neat trick!

@@ -181,7 +182,9 @@ func (h *Handler) RegisterRoutes() error {
// Wrap requests with response time logging as well as panic recovery.
var (
wrapped = func(n http.Handler) http.Handler {
return logging.WithResponseTimeAndPanicErrorLogging(n, h.instrumentOpts)
return httputil.CompressionHandler{
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Want to add a comment that this does compression under the hood? for all wrapped routes?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually also.. do we want want to do this in applyMiddleware(...)? That means that every single route will get it rather than only those using wrapped(...) here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will do. It actually doesn't force compression on for all requests; rather it only returns compressed responses provided the request has an Accept-Encoding:gzip (or Accept-Encoding:deflate) header

Copy link
Contributor

@notbdu notbdu left a comment

Choose a reason for hiding this comment

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

Left a few comments but otherwise LGTM. Although I don't have a ton of context here haha.

len(values), len(cols))
}

rows := len(cols[0].Values)
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Are the number of rows always the same for every column?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah this thing is a bit awkward (been meaning to update it), but basically it has a row per series; since the series are expected to be in steps by this point, it's safe to say they're the same size here.

Been meaning to refactor this file (and a bunch of other workflows)

@@ -357,13 +357,13 @@ func buildUnconsolidatedSeriesBlock(ctrl *gomock.Controller,
it.EXPECT().Err().Return(nil).AnyTimes()
it.EXPECT().Next().Return(true)
it.EXPECT().Next().Return(false)
vals := make([]ts.Datapoints, numSteps)
vals := make(ts.Datapoints, numSteps)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ts.Datapoints a slice? Do we need to initialize as vals := make(ts.Datapoints, 0, numSteps)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, ts.Datapoints is just an alias for []ts.Datapoint; updated to use append properly

) (storage.PromResult, error) {
stores := filterStores(s.stores, s.fetchFilter, query)
// Optimization for the single store case
if len(stores) == 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Does this save alot of perf when we don't enter the loop below?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Honestly, probably not a huge amount; was more following existing functions

wg.Add(len(stores))
resultMeta := block.NewResultMetadata()
for _, store := range stores {
store := store
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Do we normally use this pattern or pass the variable into the fn as a param?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think we generally use this approach, haven't seen many places where we've passed it by param

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah pass by param we usually avoid, event though may seem "safer" we use worker pools in a lot of places where you can't pass the variable as a param to the fn (as it takes a func() {} only to pass to worker pool) so it's better to just get used to doing it one way all the times and not forgetting to take ref for the inner lambda we've found.

return nil, err
}

samples := make([]prompb.Sample, 0, initRawFetchAllocSize)
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Would pooling stuff like samples and labels make a noticeable diff to perf?

Copy link
Collaborator Author

@arnikola arnikola Dec 3, 2019

Choose a reason for hiding this comment

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

Because we can't really predict the sizes of the slice we need here, we can run into problems where a certain query needs to create huge slices, which get released to the pool but never shrunk back. We could potentially use a pool that has a chance to release and re-alloc smaller slices periodically, but not super sure how big an impact that would have

if it.datapoints == nil {
it.datapoints = make(ts.Datapoints, 0, initBlockReplicaLength)
} else {
it.datapoints = it.datapoints[:0]
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Should this be done in some sort of iterator Reset() function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think Reset() is usually used when we want to change the underlying data in an Iterator; here, we're filling up only the data that's returned in Current and the underlying data stays the same

// metas := make([]SeriesMeta, count)
// for i := range metas {
// metas[i] = SeriesMeta{Name: []byte(fmt.Sprint(i))}
// }
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Remove these commented out?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice, may do it in followup

continue
}

filtered = append(filtered, l)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: We could make this a little more performant by:

  1. start for loop compare each until we hit a label we need to skip
  2. if we find label we need to skip, copy all into "filtered" up to this element, then keep copying as you go
  3. if get to end and none were found that need to be skipped, then just take a reference to the original slice

This would avoid memcpying the results back into the original for any results that don't have the label.

But maybe an overoptimization... so only do it if you feel like it's worthwhile. I also realize this only happens if the filter is specified at all.. so maybe not worth doing for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll keep that pattern in mind, but this path is likely cold enough for this to not really matter that much for the extra complexity? If we see this showing up in traces, happy to revisit though

batch, err := bl.MultiSeriesIter(concurrency)
if err != nil {
// NB: do not have to set the iterator error here, since not all
// contained blocks necessarily allow mutli series iteration.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Typo "mutli" -> "multi"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch, may handle that in the cleanup PR if thats ok

vals := make([]ts.Datapoints, numSteps)
for i := range vals {
vals := make(ts.Datapoints, 0, numSteps)
// for i := range vals {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Remove?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍 removed in cleanup

func (b *ucEmptyBlock) MultiSeriesIter(
concurrency int,
) ([]UnconsolidatedSeriesIterBatch, error) {
batch := make([]UnconsolidatedSeriesIterBatch, concurrency)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Would it make sense to use append for consistency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For cases like this where we're initializing the entire slice to a default value I think this reads a little clearer than doing:

batch := make([]UnconsolidatedSeriesIterBatch, 0, concurrency)
for i := 0; i < concurrency; i++ {

What do you think?

for i, vals := range s.datapoints {
values[i] = consolidationFunc(vals)
for i, v := range s.datapoints {
values[i] = v.Value
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, are we not performing the consolidation function anymore?

Why is consolidationFunc passed in here but not used anywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah, so this particular file is not long for this repo... was intending to delete this (and have in the cleanup) and it's (now) on a dead codepath

go func() {
err := buildBlockBatch(loopIndex, batch.Iter, builder, m, p, &mu)
mu.Lock()
// NB: this no-ops if the error is nil.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could skip the mu.Lock() however if nil. Is that worth it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, may handle that in the cleanup PR if thats ok

mu.Lock()
// NB: this sets the values internally, so no need to worry about keeping
// a reference to underlying `values`.
err := builder.SetRow(idx, values, blockMeta.seriesMeta[idx])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the lock required if SetRow(...) only touches that row? (which looking at SetRow it looks like it does?)

I know could be potentially dangerous, although could avoid a lot of locking across batches when processing series after series.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe just best to keep the locks..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah think it's safer to keep the locks here; applying the actual query is a much heavier process than writing the rows, so any increase is likely negligible

@@ -341,7 +363,8 @@ func getIndices(
l = i
}

if !ts.After(rBound) {
if ts <= rBound {
// if !ts.After(rBound) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Remove commented out?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed in cleanup 👍


for i, v := range sink.Values {
fmt.Println(i, v)
fmt.Println(" ", tt.expected[i])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Still need these printlns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed in cleanup 👍

// linearRegression performs a least-square linear regression analysis on the
// provided datapoints. It returns the slope, and the intercept value at the
// provided time.
// Uses this algorithm: https://en.wikipedia.org/wiki/Simple_linear_regression.
func linearRegression(
dps ts.Datapoints,
interceptTime time.Time,
interceptTime int64,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Can we use xtime.UnixNano instead of just pure int64 here and other places? Feel like it will be safer/easier to grok/read too. There's a lot of To/From helpers from that type too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call, changed all of these in the upcoming PR

@@ -139,20 +143,18 @@ func standardRateFunc(
counterCorrection float64
firstVal, lastValue float64
firstIdx, lastIdx int
firstTS, lastTS time.Time
firstTS, lastTS int64
Copy link
Collaborator

Choose a reason for hiding this comment

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

lBound time.Time,
rBound time.Time,
lBound int64,
rBound int64,
Copy link
Collaborator

Choose a reason for hiding this comment

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

@@ -221,7 +223,8 @@ func irateFunc(
datapoints ts.Datapoints,
isRate bool,
_ bool,
timeSpec transform.TimeSpec,
_ int64,
_ int64,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call, changed all of these in the upcoming PR

}

result, _, err := accumulator.FinalResultWithAttrs()
defer accumulator.Close()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not defer accumulator.Close() before the call to FinalResultWithAttrs since it will run anyway (seems more normal to do a defer foo.Close() just after you have access to something rather than a few more lines down).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call, changed in cleanup 👍

identTag := identTags.Current()
labels = append(labels, prompb.Label{
Name: identTag.Name.Bytes(),
Value: identTag.Value.Bytes(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are these identTags guaranteed to not be closed until we've returned/used the labels? Are they not pooled at all? If they were pooled I could see just taking raw refs to the bytes causing problems (since could be returned while you hang onto the bytes by the prompb.Labels).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, will update

if err != nil {
// Return the first error that is encountered.
select {
case errorCh <- err:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, seems racey here that errorCh might be already closed?

This is since stopped() is not read or any lock is held to guarantee the errorCh is still open before enqueuing.

Unfortunately even in a switch like this writing to a closed channel will panic:
https://play.golang.org/p/XmRlQbixatB

Can use a cancellable lifetime here if you like:
https://github.com/m3db/m3/blob/master/src/x/resource/lifetime.go#L28:6

As used here:
https://github.com/m3db/m3/blob/master/src/dbnode/storage/index/block.go#L832-L840

@@ -85,8 +92,10 @@ func (it *encodedSeriesIterUnconsolidated) Next() bool {
return false
}

alignedValues := values.AlignToBoundsNoWriteForward(it.meta.Bounds, it.lookbackDuration)
it.series = block.NewUnconsolidatedSeries(alignedValues, it.seriesMeta[it.idx])
it.series = block.NewUnconsolidatedSeries(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we not need to AlignToBoundsNoWriteForward anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No; in fact I've managed to remove it in the cleanup

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For more context, this was only being used in temporal functions, which immediately unrolled it back into a flat list :P

I've since split consolidated/unconsolidated distinction to be StepIterator = consolidated, SeriesIterator = unconsolidated, as unconsolidated steps or consolidated series don't make a lot of sense

)

type encodedSeriesIterUnconsolidated struct {
idx int
lookbackDuration time.Duration
err error
meta block.Metadata
datapoints ts.Datapoints
alignedValues []ts.Datapoints
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is alignedValues being used anywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed in cleanup 👍

@arnikola arnikola merged commit 8cbe5a8 into master Dec 10, 2019
arnikola added a commit that referenced this pull request Dec 10, 2019
arnikola added a commit that referenced this pull request Dec 10, 2019
@robskillington robskillington deleted the arnikola/temporal branch January 28, 2020 16:44
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.

None yet

3 participants