-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Use sync.Pool for gzipWriter #481
Conversation
Signed-off-by: glefloch <glfloch@gmail.com>
Is there any performance (or GC) difference having the gzipHandler as an anonymous function, vs. declared normally? |
prometheus/promhttp/http.go
Outdated
defer giveBuf(buf) | ||
writer, encoding := decorateWriter(req, buf, opts.DisableCompression) | ||
enc := expfmt.NewEncoder(writer, contentType) | ||
buf := &bytes.Buffer{} |
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.
Why did you remove the sync.Pool for buf? This will create a lot of allocations in high-scrape-rate scenarios.
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.
Or to put it another way: For the same reason we want the gzip writer in a sync.Pool, we want this intermediate buffer in a sync.Pool.
prometheus/promhttp/http.go
Outdated
if lastErr != nil && buf.Len() == 0 { | ||
http.Error(w, "No metrics encoded, last error:\n\n"+lastErr.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
header := w.Header() | ||
header.Set(contentTypeHeader, string(contentType)) | ||
header.Set(contentLengthHeader, fmt.Sprint(buf.Len())) |
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 commit removes this header (which SHOULD be used as per https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 ). That's probably OK for gzip'd case because we avoid another buffering layer, but it could still be set in the cases where we don't compress. This is complicated here because of the double-wrapping, see comment below.
prometheus/promhttp/http.go
Outdated
if _, err := w.Write(buf.Bytes()); err != nil && opts.ErrorLog != nil { | ||
opts.ErrorLog.Println("error while sending encoded metrics:", err) | ||
} | ||
// TODO(beorn7): Consider streaming serving of metrics. | ||
}) | ||
|
||
gzipHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
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.
It is helpful to have two http.HandlerFunc
s here where one just wraps the other? Couldn't this just be inlined?
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.
Nested handlers is the normal way to implement gzip middleware in Go, especially if you want it to be optional / settable by some flag.
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.
But this is not really nesting of handlers. The code is all enclosed in the very large HandlerFor function. And we always go through the local gzipHandler function, even if compression is disabled. We kind of get the worst of both worlds: No access to the content length anymore, but still no modularization.
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'm not a fan of it being an anonymous function either, as I suspect this will cause a lot of allocations / GC. The NYTimes implementation is worth studying. And bear in mind that the content-length header should be the length of the gzipped output, not the original input.
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.
WRT content length: Yes, I had that in mind. That's why I suggested it's probably OK to not add a content length when gzipping. But we should (or even SHOULD :-) still add the content length when not gzipping. And that's not possible with the approach here where the decision is made within the gzipHandler local function but the uncompressed content length is only available in the h local function. In this specific case, where we don't want to provide a generically usable gzip handler, I'd probably just inline everything into one local function. At least for now. We can still refactor into something nicer once we have something that works.
Thanks for doing this. The idea to gzip the completely rendered response instead of the decorated writer approach so far looks quite neat. I left a few comments we need to clarify. |
@beorn7 Thanks for the comment, I will update the code. |
Signed-off-by: glefloch <glfloch@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.
I think this looks good in general. Just a few minor comments, which don't change anything fundamentally.
(With the exception of the content-length-header...)
} | ||
|
||
// gzipHandler return a http.HandlerFunc in charge of compressing the content | ||
// of the given http.HandlerFunc |
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.
Doc comment is outdated now.
if closer, ok := writer.(io.Closer); ok { | ||
closer.Close() | ||
} | ||
|
||
if lastErr != nil && buf.Len() == 0 { | ||
http.Error(w, "No metrics encoded, last error:\n\n"+lastErr.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
header := w.Header() | ||
header.Set(contentTypeHeader, string(contentType)) | ||
header.Set(contentLengthHeader, fmt.Sprint(buf.Len())) |
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.
Setting the content length must only happen if we do not gzip-compress. See suggestion below.
buf.Reset() | ||
enc := expfmt.NewEncoder(buf, contentType) | ||
|
||
defer bufPool.Put(buf) |
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.
Is there a reason to not do this right after the bugPool.Get()
above?
} | ||
writeResult(w, buf, opts) |
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 now that this is inlined, we can even drop the gzipResponseWriter
and the writeResult
. Suggestion for everything from L163 to here:
var writer io.Writer = w
if !opts.DisableCompression && gzipAccepted(req.Header) {
header.Set(contentEncodingHeader, "gzip")
 gz := gzipPool.Get().(*gzip.Writer)
 defer gzipPool.Put(gz)

 gz.Reset(w)
 defer gz.Close()
 writer = gz
 } else {
header.Set(contentLengthHeader, fmt.Sprint(buf.Len()))
 }
 if _, err := writer.Write(buf.Bytes()); err != nil && opts.ErrorLog != nil {
opts.ErrorLog.Println("error while sending encoded metrics:", err)
}
Would that work?
For nicer naming, perhaps then rename w
into hw
and writer
into w
.
Hmm, thinking about the need (or not-need) of content length, the changes proposed in your PR, and the current performance testing I'm doing for kube-state-metrics, I'm wondering if we should perhaps ditch that intermediate buffer and do streaming exposition, at least for the encoding part. I'll play with it and create a PR based on yours. |
See #482 |
This PR aims to close issue #366 by adding a sync.Pool for
gzipWriter
.As mentionned by @dswarbrick, the
NYTime gzipHandler
did not improve performance compared to async.Pool
.Signed-off-by: glefloch glfloch@gmail.com