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

Limit the number of samples remote read can return. #4532

Merged
merged 2 commits into from
Sep 5, 2018

Conversation

tomwilkie
Copy link
Member

Tracking down Prometheus OOMs for a client, initially thought it was PromQL (see #4414), but looks like it is remote read requests.

Quick and dirty limit; probably want this plumbed through as a command line argument?

Signed-off-by: Tom Wilkie [email protected]

@gouthamve
Copy link
Member

This might break some Thanos users. @fabxc @Bplotka

@tomwilkie
Copy link
Member Author

Yes, sorry should have said; OOMs seemed to be caused by queries submitted via Thanos.

We'll see if we can remove this limit through a streaming API as discussed at the dev summit, this is just a quick fix.

@tomwilkie tomwilkie force-pushed the limit-remote-read branch 2 times, most recently from d2afd39 to eec2302 Compare August 23, 2018 17:35
@@ -164,6 +164,9 @@ func main() {
a.Flag("storage.remote.flush-deadline", "How long to wait flushing sample on shutdown or config reload.").
Default("1m").PlaceHolder("<duration>").SetValue(&cfg.RemoteFlushDeadline)

a.Flag("storage.remote.read-sample-limit", "Maxium number of samples to return via the remote read interface, in a single query.").
Copy link
Contributor

Choose a reason for hiding this comment

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

Maximum

@@ -164,6 +164,9 @@ func main() {
a.Flag("storage.remote.flush-deadline", "How long to wait flushing sample on shutdown or config reload.").
Default("1m").PlaceHolder("<duration>").SetValue(&cfg.RemoteFlushDeadline)

a.Flag("storage.remote.read-sample-limit", "Maxium number of samples to return via the remote read interface, in a single query.").
Default("20m").IntVar(&cfg.web.RemoteReadLimit)
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually 0 would mean no limit.

Does m mean milli here?

@@ -164,6 +164,9 @@ func main() {
a.Flag("storage.remote.flush-deadline", "How long to wait flushing sample on shutdown or config reload.").
Default("1m").PlaceHolder("<duration>").SetValue(&cfg.RemoteFlushDeadline)

a.Flag("storage.remote.read-sample-limit", "Maxium number of samples to return via the remote read interface, in a single query.").
Default("20m").IntVar(&cfg.web.RemoteReadLimit)

Choose a reason for hiding this comment

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

Default is "20m", but the flag is an IntVar - running this results in Error parsing commandline arguments: strconv.ParseFloat: parsing "20m": invalid syntax

Copy link
Member

Choose a reason for hiding this comment

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

So.. how to assess what max number of samples Prometheus can handle from remote read API?

@tomwilkie
Copy link
Member Author

tomwilkie commented Aug 23, 2018 via email

Copy link
Member

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

As long as this has reasonable default and configurable flag, Thanos should be fine.

Incidentally is there any spike for streaming API for remote Read @tomwilkie? Can I help with this?

@@ -164,6 +164,9 @@ func main() {
a.Flag("storage.remote.flush-deadline", "How long to wait flushing sample on shutdown or config reload.").
Default("1m").PlaceHolder("<duration>").SetValue(&cfg.RemoteFlushDeadline)

a.Flag("storage.remote.read-sample-limit", "Maxium number of samples to return via the remote read interface, in a single query.").
Default("20m").IntVar(&cfg.web.RemoteReadLimit)
Copy link
Member

Choose a reason for hiding this comment

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

So.. how to assess what max number of samples Prometheus can handle from remote read API?

resp := &prompb.QueryResult{}
for ss.Next() {
series := ss.At()
iter := series.Iterator()
samples := []*prompb.Sample{}

for iter.Next() {
numSamples += 1
if numSamples > sampleLimit {
return nil, fmt.Errorf("too many samples")
Copy link
Member

Choose a reason for hiding this comment

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

Can we also wrap the error with what is the limit?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably have some metrics around this too.

Copy link
Member Author

Choose a reason for hiding this comment

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

And make it return a 400, not a 500...

@tomwilkie
Copy link
Member Author

So.. how to assess what max number of samples Prometheus can handle from remote read API?

I've set the default to 20 million based on discussions on the other PR (#4513), but will happily consider making it higher.

@tomwilkie
Copy link
Member Author

Incidentally is there any spike for streaming API for remote Read @tomwilkie? Can I help with this?

Yes we discussed at the dev summit making a streaming API to help thanos & other systems. I've just done similar in cortex: cortexproject/cortex#933

Its beyond the scope of this PR - I suggest you start a thread on the -dev mailing list. We discussed streaming the compressed chunks out, instead of samples; previously I think you + I discussed trying to make a common API that can be used in both Cortex and Thanos.

@brian-brazil
Copy link
Contributor

We're in a different context here, so a higher value could be fine. I arbitrarily say that we aim to keep it under 1GB by default.

@tomwilkie
Copy link
Member Author

Ignoring labels, 60M * 16 bytes = 1GB, so I'll set it to that.

@brian-brazil
Copy link
Contributor

Call it 50M to allow for the labels, and requests for many series with few points. It's also more round.

- Return 413 entity too large.
- Limit can be set be a flag.  Allow 0 to mean no limit.
- Include limit in error message.
- Set default limit to 50M (* 16 bytes = 800MB).

Signed-off-by: Tom Wilkie <[email protected]>
@tomwilkie tomwilkie changed the title [WIP] Limit the number of samples remote read can return. Limit the number of samples remote read can return. Aug 28, 2018
@tomwilkie
Copy link
Member Author

Rebased against master; ready for final review I hope!

if sampleLimit > 0 && numSamples > sampleLimit {
return nil, HTTPError{
msg: fmt.Sprintf("exceeded sample limit (%d)", sampleLimit),
status: http.StatusRequestEntityTooLarge,
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a 413, as it's the server response that's too big. I think this should be a 400.

Copy link
Member

Choose a reason for hiding this comment

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

Agree, 413 indicates the request size too big, 400 would be less confusing

Copy link
Contributor

@brian-brazil brian-brazil left a comment

Choose a reason for hiding this comment

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

We might want to add some metrics around this

Copy link
Member

@bwplotka bwplotka left a comment

Choose a reason for hiding this comment

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

Typo + some suggestions

@@ -177,6 +177,9 @@ func main() {
a.Flag("rules.alert.resend-delay", "Minimum amount of time to wait before resending an alert to Alertmanager. Must be lower than resolve_timeout in Alertmanager").
Default("1m").SetValue(&cfg.resendDelay)

a.Flag("storage.remote.read-sample-limit", "Maximum number of samples to return via the remote read interface, in a single query. 0 means no limit.").
Copy link
Member

Choose a reason for hiding this comment

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

s/query. 0 means no limit/ query. 0 means no limit/ (double space)

Copy link
Member

Choose a reason for hiding this comment

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

Also worth to mention that this is "overall" number of samples. not per series (this could be a follow up question if there is no clarification)

Copy link
Member

@bwplotka bwplotka Sep 5, 2018

Choose a reason for hiding this comment

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

Also, is there anyway we can make it more convenient for user and already map this flag into memory size used? It is easier to limit the memory here and we can calculate/estimate mem into number of samples... The cost is that the calculation is on our part.. but it's still better, than thousands question on IRC how the number of samples here maps into memory (: Any thoughts?

Ignore me, we can only roughly estimate the mem usage with the simple logic, so let's stick to just samples limit.

if sampleLimit > 0 && numSamples > sampleLimit {
return nil, HTTPError{
msg: fmt.Sprintf("exceeded sample limit (%d)", sampleLimit),
status: http.StatusRequestEntityTooLarge,
Copy link
Member

Choose a reason for hiding this comment

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

Agree, 413 indicates the request size too big, 400 would be less confusing

resp := &prompb.QueryResult{}
for ss.Next() {
series := ss.At()
iter := series.Iterator()
samples := []*prompb.Sample{}

for iter.Next() {
numSamples += 1
Copy link
Member

Choose a reason for hiding this comment

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

minor nit: ++ instead?

http.Error(w, err.Error(), http.StatusInternalServerError)
if httpErr, ok := err.(remote.HTTPError); ok {
http.Error(w, httpErr.Error(), httpErr.Status())
} else {
Copy link
Member

Choose a reason for hiding this comment

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

can we just return in each case? no need for else here and one indent less below

@brancz
Copy link
Member

brancz commented Sep 5, 2018

Can you actually try a large size message? As far as I know the default maximum size for a proto message is limited to 64Mb (last time I checked they warned of larger sizes as it loosens some security constraints, but not sure of the details).

@bwplotka
Copy link
Member

bwplotka commented Sep 5, 2018

To clarify @brancz do you mean "if it's even useful to have this flag because anything more than 50m samples will make protobuf message larger than default proto message (64MB)"?

Good point, not sure of the implications. Also, how you get 64MB? Just for comparison: The grpc grpc/grpc#7927 by default allows max 4MB (: But it might be because protocol limitations itself. Would love to know more about implications.

BTW we are working on streaming as well: https://docs.google.com/document/d/1JqrU3NjM9HoGLSTPYOvR217f5HBKBiJTqikEB9UiJL0/edit?ts=5b8f135a#

  • @tomwilkie how the current flag would fit into streamed response? That limit would be an overall limit? or per frame?

@gouthamve
Copy link
Member

We don't use gRPC, we marshal into proto and send it over HTTP.

// EncodeReadResponse writes a remote.Response to a http.ResponseWriter.
func EncodeReadResponse(resp *prompb.ReadResponse, w http.ResponseWriter) error {
data, err := proto.Marshal(resp)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/x-protobuf")
w.Header().Set("Content-Encoding", "snappy")
compressed := snappy.Encode(nil, data)
_, err = w.Write(compressed)
return err
}

Not sure where the 64MB limit is documented though.

@bwplotka
Copy link
Member

bwplotka commented Sep 5, 2018

I know @gouthamve, just putting grpc as an example

Signed-off-by: Tom Wilkie <[email protected]>
@tomwilkie
Copy link
Member Author

tomwilkie commented Sep 5, 2018

As far as I know the default maximum size for a proto message is limited to 64Mb

I'm not aware, nor could I find, any limit in the golang proto bindings at https://github.com/golang/protobuf.

To clarify @brancz do you mean "if it's even useful to have this flag because anything more than 50m samples will make protobuf message larger than default proto message (64MB)"?

Yes it is still useful; we build the datastructure before encoding it, and its the building of the datastructure to which this limits applies. Even if there was a 64MB limit, we still see OOMs for large queries to the remote read endpoint, imply its the building of the datastructure causing the OOM.

@tomwilkie how the current flag would fit into streamed response? That limit would be an overall limit? or per frame?

Until an agreed upon approach exists for the streaming, no. I can only speculate that this could apply to a single "frame", but thats just a guess. Lets take this one offline, we can always deprecate the flag if needs be.

@brancz
Copy link
Member

brancz commented Sep 5, 2018

The thing I was thinking of is this: https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.io.coded_stream#CodedInputStream.SetTotalBytesLimit.details

The golang implementations shouldn't actually be affected by this, but it feels like we should follow the best practice especially for an API that is exposed to be consumed by arbitrary implementations.

@gouthamve
Copy link
Member

👍

@tomwilkie
Copy link
Member Author

tomwilkie commented Sep 5, 2018

The golang implementations shouldn't actually be affected by this, but it feels like we should follow the best practice especially for an API that is exposed to be consumed by arbitrary implementations.

By setting the limit to 64MB by default, you'd be limiting Thanos to fetching ~8k timeseries per query (64MB / 16bytes * 15s scrape interval / 2hr head block). I think that limit would be too low, unless my math is wrong.

@brancz
Copy link
Member

brancz commented Sep 5, 2018

That's fair. Maybe we should improve this step by step instead and only impose smaller limits if we actually manage to get streaming for example.

@tomwilkie tomwilkie merged commit 457e4bb into master Sep 5, 2018
@tomwilkie tomwilkie deleted the limit-remote-read branch September 5, 2018 13:50
Copy link
Member

@brancz brancz left a comment

Choose a reason for hiding this comment

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

We should just have a test for the too large case, otherwise lgtm.

@@ -861,6 +862,10 @@ func TestReadEndpoint(t *testing.T) {
recorder := httptest.NewRecorder()
api.remoteRead(recorder, request)

if recorder.Code/100 != 2 {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add a test for the too large case as well?

@sksingh20
Copy link

Team, I have major issue on grafana dashboard which is increasing step size by itself resulting in substantial loss of data.
grafana/grafana#34775

As per them this is limitation of Prometheus where prometheus is making limit of 11K records.

Could you please confirm if there is any limit enforced for query to return data points and what is that value.

@sksingh20
Copy link

@brancz Any thoughts on claim by Grafana teams?

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.

7 participants