Skip to content
This repository has been archived by the owner on Aug 23, 2023. It is now read-only.

Limit series from clusterByFind operation #1021

Merged
merged 12 commits into from
Oct 11, 2018

Conversation

shanson7
Copy link
Collaborator

Fixes #968 and #1018

Looking through the node::Post code, it seems like we could save quite a lot by marshaling directly from the response body into the msgp generated type. My C++ brain wants to use templates to accomplish it, so I'm not sure the cleanest method to use in go without generics. For that reason, I punted on that for now.


// 0 disables the check, so only check if maxSeriesPerReq > 0
if maxSeriesPerReq > 0 && len(resp.Metrics)+len(allSeries) > maxSeries {
return nil,
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't you cancel ctx so outstanding peer queries are aborted?

Copy link
Collaborator Author

@shanson7 shanson7 Aug 28, 2018

Choose a reason for hiding this comment

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

Technically, the query will be Doned when the error propagates up and a response is returned. But canceling it here could do it a little bit earlier.

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'm also not entirely sure how to cancel this context here. I suppose we would need to make a new cancelable context here.

Copy link
Contributor

Choose a reason for hiding this comment

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

that makes sense I think. Since we only get the ctx passed in, we don't know where what the cancelfunc was.
However, if you do something like newCtx, cancel := context.WithCancel(ctx) and pass newCtx into peerQuerySpeculativeChan, then you can cancel it.

@shanson7
Copy link
Collaborator Author

I plan on following up this change with a version that decodes directly into a msgp.Decodable type. As part of this change, it might be easy to support local processing as well.

@shanson7
Copy link
Collaborator Author

See master...bloomberg:directDecode for a version of this change that includes local request processing and decoding straight from the http.Response into a msgp.Decodable type. This should reduce the temporary memory usage from reading into a []byte and then unmarshalling.

@@ -294,7 +294,7 @@ func (s *Server) peerQuery(ctx context.Context, data cluster.Traceable, name, pa
result := make(map[string]PeerResponse)
for resp := range responses {
if resp.err != nil {
return nil, err
return nil, resp.err
Copy link
Contributor

Choose a reason for hiding this comment

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

oops!

log.Debug("HTTP Render querying %s%s", peer.GetName(), path)
buf, err := peer.Post(reqCtx, name, path, data)
// peerQuerySpeculativeChan takes a request and the path to request it on, then fans it out
// across the cluster. If any peer fails requests to other peers are aborted. If enough
Copy link
Contributor

Choose a reason for hiding this comment

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

I notice peerQuerySpeculative description includes "...except to the local peer...", whereas this one does not.
i think the former is wrong

api/graphite.go Outdated
// 0 disables the check, so only check if maxSeriesPerReq > 0
if maxSeriesPerReq > 0 && len(resp.Metrics)+len(allSeries) > maxSeries {
return nil,
response.NewError(413, fmt.Sprintf("Request exceeds max-series-per-req limit (%d). Reduce the number of targets or ask your admin to increase the limit.", maxSeriesPerReq))
Copy link
Contributor

Choose a reason for hiding this comment

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

please use the http.StatusXxx constants
in this case http.StatusRequestEntityTooLarge

}
go func() {
defer close(errorChan)
defer close(resultChan)
Copy link
Contributor

Choose a reason for hiding this comment

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

minor thought: would it be cleaner/simpler to just have 1 return channel? we could return PeerResponse along with its error

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Possibly. I like the 2 channels, because you can just loop over responses until it is closed, then quickly try to read into an err var for the return value. See this usage

Copy link
Contributor

Choose a reason for hiding this comment

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

fair enough

@Dieterbe
Copy link
Contributor

Dieterbe commented Sep 17, 2018

See master...bloomberg:directDecode for a version of this change that includes local request processing and decoding straight from the http.Response into a msgp.Decodable type. This should reduce the temporary memory usage from reading into a []byte and then unmarshalling.

sounds great. but I suggest we first merge this, and then new PR for that work.

Do you have interesting observations/stats you can share with us?
I assume "decrease in OOM's" would be one, but anything else? decreased heap usage? reduced allocation rate? I guess it's hard to separate the effects from the new limit parameter (which causes MT to do less work) vs the new channeling approach (which should make it work more efficiently)
Is this, in your experience, safe to go to prod?

Copy link
Contributor

@Dieterbe Dieterbe left a comment

Choose a reason for hiding this comment

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

looks pretty good. few minor tweaks needed

@shanson7
Copy link
Collaborator Author

I definitely saw a reduction in short-term heap spikes, but overall heap usage stayed about the same. We've been running this in prod for about 2 months now, without any issues.

@Dieterbe
Copy link
Contributor

Dieterbe commented Oct 10, 2018

@shanson7 @woodsaj what do you think of the cancellation i added in the last commit?
I think this was the last item to do before we can merge this.

if looks good, can you rebase on master (I didn't want to force push into your branch, not sure if i even can). then i'll merge.


var allSeries []Series

for r := range responseChan {
resp := models.IndexFindByTagResp{}
_, err := resp.UnmarshalMsg(r.buf)
if err != nil {
cancel()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would it make sense to just defer cancel() at the top like the peerQuery functions do?

Also, how much does this canceling really buy us if the top-level ctx will be Done when the error is returned?

Copy link
Contributor

@Dieterbe Dieterbe Oct 10, 2018

Choose a reason for hiding this comment

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

not sure what you mean with a ctx "being Done".
ctx.Done() just returns a channel that signals cancellation (by getting closed), but you still need to call a corresponding cancel function for the cancellation to happen.

I see 3 callers of this function:

  • /render -> renderMetrics -> executePlan
  • /tags/findSeries (graphiteTagFindSeries)
  • querier.Select

we would need all 3 to call a cancelfunc for my change to be unnecessary.
but from what i can tell, it looks like neither of them, or at least not the first two, do this. (unless macron automatically calls cancel on our behalf?)

but regardless, it shouldn't take much code spelunking for reader to assert whether we cancel a context when we should. so i rather call cancel wherever it seems sensible, even if we call it multiple times (which is harmless).
I think @woodsaj was trying to establish this convention when he first added cancellations throughout this code base.

as for using defer vs just putting the code at the return sites, for short functions it's really debatable and depends more on taste. so unless you have a strong argument, let's just stick with 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.

Re: Done

I mean that peerQuerySpeculativelyChan checks if the context is Done() https://github.com/grafana/metrictank/pull/1021/files#diff-bc8a656be21edce0cf2a74adf23d7aeaR401

Done isn't just done on cancel, but also on a final response being returned. So, as soon as the error propagates up and a response is delivered, peerQuerySpeculativelyChan will break its loop and call cancel(). This is done specifically so callers don't need to implement cancellation themselves. Granted, doing it directly will cancel a few milliseconds earlier, so might be marginally cheaper.

Copy link
Contributor

Choose a reason for hiding this comment

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

the cancels that i added in clusterFindByTag target cases that peerQuerySpeculativeChan cannot detect itself. these are :

  • unmarshalling error
  • exceeding maxSeriesPerReq

the cancels triggered in peerQuerySpeculativeChan (upon erroring peer.Post and upon function return) would be triggered not all, or much later, respectively.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the cancels that i added in clusterFindByTag target cases that peerQuerySpeculativeChan cannot detect itself.

True, but as long as these failures cause an error response to the HTTP request, the parent context's Done channel will be closed and peerQuerySpeculativeChan will short-circuit and will call cancel on the context it created.

This is the case in the 3 examples you posted, but I guess it's too much for clusterFindByTag to assume?

Copy link
Contributor

Choose a reason for hiding this comment

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

the parent context's Done channel will be closed

where does this happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TBH, I didn't come to this conclusion by code inspection, but rather by logging when early responses happened. It definitely was how it behaved, but I couldn't tell you if it's by contract or a symptom of how I was querying

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, well. this reinforces my point that these matters should be made much more obvious. hence rather a call to cancel more rather than too few.
good to merge?

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, let me rebase

@shanson7
Copy link
Collaborator Author

LGTM. I see what looks like an unrelated failure in the tests.

@Dieterbe
Copy link
Contributor

yes, this failure is due to the flakey TimeLimiter tests. see above PR.

@Dieterbe Dieterbe merged commit 8194bd7 into grafana:master Oct 11, 2018
@Dieterbe
Copy link
Contributor

nice work sean!

@shanson7 shanson7 deleted the limitSeriesChan branch March 6, 2019 14:27
@shanson7 shanson7 restored the limitSeriesChan branch March 6, 2019 14:27
@shanson7 shanson7 deleted the limitSeriesChan branch December 24, 2019 14:19
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants