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

feat: Support zstd compression #1496

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

mrueg
Copy link
Contributor

@mrueg mrueg commented Apr 10, 2024

This allows endpoints to respond with zstd compressed metric data, if the requester supports it. For backwards compatibility, gzip compression will take precedence.

I added a benchmark to the http_test.go, that will test with a differently sized synthetic metric series.

Running tool: go test -benchmem -run=^$ -bench ^BenchmarkEncoding$ github.com/prometheus/client_golang/prometheus/promhttp

goos: linux
goarch: amd64
pkg: github.com/prometheus/client_golang/prometheus/promhttp
cpu: Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
BenchmarkEncoding/test_with_gzip_encoding_small-8         	   20670	     53641 ns/op	  111770 B/op	      49 allocs/op
BenchmarkEncoding/test_with_zstd_encoding_small-8         	    7838	    178033 ns/op	 1121493 B/op	      75 allocs/op
BenchmarkEncoding/test_with_no_encoding_small-8           	   56409	     20998 ns/op	   38030 B/op	      43 allocs/op
BenchmarkEncoding/test_with_gzip_encoding_medium-8        	    8493	    143460 ns/op	   78918 B/op	      49 allocs/op
BenchmarkEncoding/test_with_zstd_encoding_medium-8        	    3650	    345559 ns/op	 1155737 B/op	      76 allocs/op
BenchmarkEncoding/test_with_no_encoding_medium-8          	    8816	    122759 ns/op	   72252 B/op	      44 allocs/op
BenchmarkEncoding/test_with_gzip_encoding_large-8         	     944	   1206069 ns/op	  479829 B/op	      52 allocs/op
BenchmarkEncoding/test_with_zstd_encoding_large-8         	     817	   1482854 ns/op	 1429911 B/op	      75 allocs/op
BenchmarkEncoding/test_with_no_encoding_large-8           	     994	   1213586 ns/op	  346837 B/op	      45 allocs/op
BenchmarkEncoding/test_with_gzip_encoding_extra-large-8   	      85	  14008798 ns/op	 2972152 B/op	      56 allocs/op
BenchmarkEncoding/test_with_zstd_encoding_extra-large-8   	      90	  14638520 ns/op	 3731521 B/op	      74 allocs/op
BenchmarkEncoding/test_with_no_encoding_extra-large-8     	      93	  13897780 ns/op	 2648745 B/op	      45 allocs/op
PASS
ok  	github.com/prometheus/client_golang/prometheus/promhttp	17.534s

See also: prometheus/prometheus#13866

/kind feature

Release note:

promhttp: Support zstd compression	

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.

Nice! I am supportive. I think adding zstd to some places in Prometheus turned out to be sometimes controversial due to higher CPU, but from the client side... we are happy to support more AS LONG as the dependency is not too problematic (zstd should be fine).

Thanks! I have some suggestions to the implementation though, see comments.

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http_test.go Outdated Show resolved Hide resolved
@mrueg
Copy link
Contributor Author

mrueg commented May 13, 2024

Thanks for the feedback!

I haven't thought about enhancing the Accept-Encoding header further and have replaced it now with an internal copy of the archived https://github.com/golang/gddo/blob/master/httputil/negotiate.go#L19

I see prometheus is using an internal copy of goautoneg: https://github.com/prometheus/common/blob/main/internal/bitbucket.org/ww/goautoneg/autoneg.go this might be an option to switch it to the gddo/httputil implementation and move all the code into prometheus/common.

Users of the library can now control what compression they offer via opts.EncodingOffers as well.

Cross-linking these issues from internal TODOs:
golang/go#19307
golang/go#62513

Open question:
If the user of the client supplies an not-yet implemented compression, should this be visible via an error (and if so, how)?

@mrueg mrueg force-pushed the support-zstd-encoding branch 11 times, most recently from 0d37c09 to 03af4da Compare May 13, 2024 12:51
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.

Beautiful work, had to find time to review it

If the user of the client supplies an not-yet implemented compression, should this be visible via an error (and if so, how)?

Good question. First of all I would use enum so it's less likely it happens. Then.. I propose something in comment, should be good enough. WDYT?

Thanks! Almost there!

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http_test.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
@mrueg mrueg force-pushed the support-zstd-encoding branch 2 times, most recently from 48c16ce to ce851d0 Compare May 17, 2024 14:03
@mrueg mrueg changed the title feat: Support zstd encoding feat: Support zstd compression May 17, 2024
@mrueg mrueg force-pushed the support-zstd-encoding branch 2 times, most recently from bd79bd6 to b1dbb01 Compare May 17, 2024 14:10
@mrueg mrueg force-pushed the support-zstd-encoding branch 2 times, most recently from 1716212 to 789178e Compare May 31, 2024 12:53
@mrueg mrueg force-pushed the support-zstd-encoding branch 2 times, most recently from 175c33d to 70157ae Compare May 31, 2024 12:57
w, encodingHeader, closeWriter, err := NegotiateEncodingWriter(req, rsp, opts.DisableCompression, compressions)

if closeWriter != nil {
defer func() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bwplotka Test will fail if you comment out this defer.
Needed to do it that way instead of calling the function directly for golangci-lint's errcheck.

Copy link
Member

Choose a reason for hiding this comment

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

I think there is potentially better way (do _ := if we don't care about this err, or pack opt.Err log in helper func), but that's already good enough, Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

Still not resolved, plus we can kill this if != nil check for close Writer I assume (let's ensure negotiateEncodingWriter always sets it, even if noop).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I added the empty functions you suggested also if an error is returned.

@mrueg
Copy link
Contributor Author

mrueg commented Jun 3, 2024

@bwplotka I believe I addressed everything, so this is ready for another round of review. :)
Appreciate the feedback, helped me to ramp up my golang quite a bit.

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.

Amazing, thank you!

I think this is good enough to merge in this state. I think there are some things e.g. number of if statements to handle response from Negotiate yields more code vs if we just inline this function (: But I think there might be ways to simplify this, mostly for readability.

Happy to merge, and allow others and myself to simplify bit in another PR, or propose PR to your PR, what do you prefer? Or if you want you could try to simplify too if you know what I mean (added some suggestions), but happy to help here to save your time! 🤗

Thanks!

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
w, encodingHeader, closeWriter, err := NegotiateEncodingWriter(req, rsp, opts.DisableCompression, compressions)

if closeWriter != nil {
defer func() {
Copy link
Member

Choose a reason for hiding this comment

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

I think there is potentially better way (do _ := if we don't care about this err, or pack opt.Err log in helper func), but that's already good enough, Thanks!

}
}()
}
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Technically no defer is needed on this error, no? So we can move closeWriter to after err != nil and skip closerWriter if nil check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one isn't the deferred one, this is the one from NegotiateEncodingWriter. I reordered it for better readability.

if opts.ErrorLog != nil {
opts.ErrorLog.Println("error getting writer", err)
}
// Since the writer received from NegotiateEncodingWriter will be nil, in case there's an error, we set it here
Copy link
Member

Choose a reason for hiding this comment

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

No need for this comment TBH

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed it. :)


w = gz
if encodingHeader == "" {
Copy link
Member

Choose a reason for hiding this comment

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

After err check we expect this to be always valid, can we ensure in func?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, I moved it into the err check.

@mrueg
Copy link
Contributor Author

mrueg commented Jun 4, 2024

Amazing, thank you!

I think this is good enough to merge in this state. I think there are some things e.g. number of if statements to handle response from Negotiate yields more code vs if we just inline this function (: But I think there might be ways to simplify this, mostly for readability.

Happy to merge, and allow others and myself to simplify bit in another PR, or propose PR to your PR, what do you prefer? Or if you want you could try to simplify too if you know what I mean (added some suggestions), but happy to help here to save your time! 🤗

Thanks!

Can we keep the NegotiateEncodingWriter func separate and not inline it? I want to reuse it in kube-state-metrics to avoid code duplication there (unfortunately we cannot fully use ServeHTTP there).

If there are any other bits that could make the code simpler and easier to read, please let me know, happy to implement them as well.

Do you want me to squash the commits into a single one?

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.

Great work! Let's keep going then ;p

I think I found one more bug 🙈 And added some suggestions inline. No need to squash, we will do it while merging. Function can be separate, added suggestions, but I think we have to make it private... sorry. Exporting NegotiateEncodingWriter would be not ideal. Commented why.

w, encodingHeader, closeWriter, err := NegotiateEncodingWriter(req, rsp, opts.DisableCompression, compressions)

if closeWriter != nil {
defer func() {
Copy link
Member

Choose a reason for hiding this comment

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

Still not resolved, plus we can kill this if != nil check for close Writer I assume (let's ensure negotiateEncodingWriter always sets it, even if noop).

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
// compressions. It returns a writer implementing the compression and an the
// correct value that the caller can set in the response header.
func NegotiateEncodingWriter(r *http.Request, rw io.Writer, disableCompression bool, compressions []string) (_ io.Writer, encodingHeaderValue string, closeWriter func() error, _ error) {
w := rw
Copy link
Member

Choose a reason for hiding this comment

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

Hm.. I feel this can be removed? We can return those z and gz things?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removed it. :)

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
return w, compression, z.Close, nil
case "gzip":
gz := gzipPool.Get().(*gzip.Writer)
defer gzipPool.Put(gz)
Copy link
Member

Choose a reason for hiding this comment

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

This has to be done on Close. It's a subtle bug, but concurrent Negotiate calls might corrupt gzip writers (hard to repro on tests I guess). "Put" means "I won't use gz anymore", which will happen on return of this function. But that's not true is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I was wondering about that as well. Thanks for the suggestions on returning a function here that calls both. TIL :)

prometheus/promhttp/http.go Outdated Show resolved Hide resolved
prometheus/promhttp/http.go Outdated Show resolved Hide resolved
mrueg and others added 5 commits June 6, 2024 00:09
This allows endpoints to respond with zstd compressed metric data, if
the requester supports it.

I have imported a content-encoding parser from
https://github.com/golang/gddo which is an archived repository to
support different content-encoding headers.

Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
* String typed enum

Signed-off-by: Manuel Rüger <manuel@rueg.eu>
mrueg and others added 4 commits June 6, 2024 00:13
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants