-
Notifications
You must be signed in to change notification settings - Fork 8.8k
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
Exemplar resize #8974
Exemplar resize #8974
Conversation
Signed-off-by: Martin Disibio <mdisibio@gmail.com>
Signed-off-by: Martin Disibio <mdisibio@gmail.com>
Signed-off-by: Martin Disibio <mdisibio@gmail.com>
Signed-off-by: Martin Disibio <mdisibio@gmail.com>
I'll push the changes to make use of this via a config file option + |
reloadable storage config. Signed-off-by: Callum Styan <callumstyan@gmail.com>
I've added a rough implementation of having a storage config section (tsdb/exemplars) in the config file so that resize can be used. Not entirely happy with how the passing of the config works given that TSDB starts up before the full config file is read. Fixing tests is on my todo list for the morning. |
The configuration file is read once before everything. Maybe we can reuse that somehow? |
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Implemented Julien's and Ben's suggestions and tried to clean up the config reloading a bit by having This also means the code can check the opts struct values and skip sending an exemplar to the exemplar storage if the storage itself is disabled or sized to I'm still not happy with how the default storage size is set, but I don't fully understand the config file parsing yet and how we could inject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments, haven't looked at the resize function yet. Looks good in general, I have some question/concerns in the comments below.
I was wondering if resize could affect ingestion of samples with exemplars in the same commit since it takes a lock for resize, but with such low overhead of resize (as seen from benchmarks), I guess that is not an issue.
tsdb/head.go
Outdated
if err != nil { | ||
return err | ||
} | ||
h.exemplars = e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause race with the appending since there is nothing protecting this ApplyConfig
/h.exemplars
. I think we should get rid of noopExemplarStorage
and just start with a 0 size exemplar storage in NewHead
without having to lock ApplyConfig
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. We'd still be allocating some memory with a 0 length buffer and empty map, but I think that's reasonable.
I'll think about this a little before making any changes here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The race still exists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll deal with this one in the morning
tsdb/head.go
Outdated
// Head uses opts.MaxExemplars in combination with opts.EnableExemplarStorage | ||
// to decide if it should pass exemplars along to it's exemplar storage, so we | ||
// need to update opts.MaxExemplars here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should only use opts.EnableExemplarStorage
for deciding and not opts.MaxExemplars
Reasons:
opts
is a config given for starting Head and would be error prone to keep it up to date with dynamic fields likeopts.MaxExemplars
. It is best to get these dynamic config from the exemplar storage itself.- There could be a case where
opts.MaxExemplars
is made >0 from <0 while the appender has not committed, in which case we should prolly not have discarded those exemplars and have kept in appender and committed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a few additional changes that we'd have to make in order to do this.
opts is a config given for starting Head and would be error prone to keep it up to date with dynamic fields like opts.MaxExemplars. It is best to get these dynamic config from the exemplar storage itself.
All config for the exemplar storage is passed through TSDB head already, and opts was what we were using before. I was trying to make this change as small as possible. If opts has only the EnableExemplarStorage
bool and not the MaxExemplars
int then we need ValidateExemplar
to return a new error type if MaxExemplars
is < 0
.
There could be a case where opts.MaxExemplars is made >0 from <0 while the appender has not committed, in which case we should prolly not have discarded those exemplars and have kept in appender and committed.
I'm not sure what you're saying here/suggesting we change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore my above comment. I was thinking about interaction between and open appender and resize and what should be the right way to append exemplars in that case. So my new question would be, is it fine to only ingest partial exemplars from a scrape? (although rare case, but can happen during a config reload)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So my new question would be, is it fine to only ingest partial exemplars from a scrape? (although rare case, but can happen during a config reload)
I think for now this would be okay, but we could explore improvements in the future.
Signed-off-by: Callum Styan <callumstyan@gmail.com>
tsdb/head.go
Outdated
es, err := NewCircularExemplarStorage(opts.NumExemplars, r) | ||
if err != nil { | ||
return nil, err | ||
var em *ExemplarMetrics |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid this being nil
. There are cases where ApplyConfig can panic and we are not updating h.exemplarMetrics
in ApplyConfig
if we moved from <0 exemplars to >0. So we could start from this being not nil
from the beginning.
var em *ExemplarMetrics | |
em := NewExemplarMetrics(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are cases where ApplyConfig can panic
can you elaborate?
tsdb/head.go
Outdated
// Head uses opts.MaxExemplars in combination with opts.EnableExemplarStorage | ||
// to decide if it should pass exemplars along to it's exemplar storage, so we | ||
// need to update opts.MaxExemplars here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore my above comment. I was thinking about interaction between and open appender and resize and what should be the right way to append exemplars in that case. So my new question would be, is it fine to only ingest partial exemplars from a scrape? (although rare case, but can happen during a config reload)
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Thanks for the review @codesome. Still not entirely happy with the config file parsing and setting the default config but maybe I'm just hung up on only setting the default config if the feature flag is set, which we're not really set up to do nicely at the moment. |
when resizing from Head code. Signed-off-by: Callum Styan <callumstyan@gmail.com>
Signed-off-by: Callum Styan <callumstyan@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I have less context here but didn't see any logic errors - seemed like a good refactor + addition of resize logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't checked exemplar.go and exemplar_test.go yet, but all other parts LGTM where I had the most comments. Since @cstyan has reviewed these exemplar files, I will do one last sanity check and merge it after that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see some potential panics. Can you update the tests to cover these cases too? With start and end size to be negative (including resize from negative to negative sizes)
Signed-off-by: Callum Styan <callumstyan@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! 🎉
I was looking at the exemplars storage feature flag docs and noticed that the |
Signed-off-by: Callum Styan <callumstyan@gmail.com>
This PR adds
Resize
method to CircularExemplarStorage. The need for this originally stems from Cortex, to support dynamically adjusting exemplar storage at runtime, but it is submitted here as it aligns with Prometheus direction of hot reload for some configuration. A future PR is expected to complete this functionality and utilize thisResize
method. It seemed better to submit separately, but this work can be combined if preferred.Storage is resized by allocating a new circular buffer and index, and replaying data into it which rebuilds index and entry linking. There is some optimization to reduce allocs and replay as little as needed (i.e. when shrinking buffer). Other approaches were considered, but lacked correctness or were less straightforward. For example, it would be very efficient when growing the ring to simply leave entries in place which preserves all indexes, but loses guarantee that oldest exemplars are overwritten first. Instead of the current implementation of absolute indexing between exemplars, we could use relative indexing or direct pointers, but this increases complexity for adding or selecting.
Benchmark: