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

Improving Performance on the API Gzip Handler #12363

Merged
merged 4 commits into from May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -32,6 +32,7 @@ require (
github.com/hetznercloud/hcloud-go v1.42.0
github.com/ionos-cloud/sdk-go/v6 v6.1.6
github.com/json-iterator/go v1.1.12
github.com/klauspost/compress v1.13.6
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b
github.com/linode/linodego v1.16.1
github.com/miekg/dns v1.1.53
Expand Down
1 change: 1 addition & 0 deletions go.sum
Expand Up @@ -498,6 +498,7 @@ github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0Lh
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
Expand Down
59 changes: 27 additions & 32 deletions util/httputil/compression.go
Expand Up @@ -14,11 +14,12 @@
package httputil

import (
"compress/gzip"
"compress/zlib"
Copy link
Member

Choose a reason for hiding this comment

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

Why not change zlib to klauspost too?

Copy link
Contributor Author

@alanprot alanprot May 24, 2023

Choose a reason for hiding this comment

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

Yeah.. we could.. lets do it! :D

"io"
"net/http"
"strings"

"github.com/klauspost/compress/gzhttp"
)

const (
Expand All @@ -30,51 +31,31 @@ const (

// Wrapper around http.Handler which adds suitable response compression based
Copy link
Member

Choose a reason for hiding this comment

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

It's not around http.Handler, and doesn't look at headers.

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! thanks a lot for the comments btw!

// on the client's Accept-Encoding headers.
type compressedResponseWriter struct {
type deflatedResponseWriter struct {
bboreham marked this conversation as resolved.
Show resolved Hide resolved
http.ResponseWriter
writer io.Writer
}

// Writes HTTP response content data.
func (c *compressedResponseWriter) Write(p []byte) (int, error) {
func (c *deflatedResponseWriter) Write(p []byte) (int, error) {
return c.writer.Write(p)
}

// Closes the compressedResponseWriter and ensures to flush all data before.
func (c *compressedResponseWriter) Close() {
// Closes the deflatedResponseWriter and ensures to flush all data before.
func (c *deflatedResponseWriter) Close() {
if zlibWriter, ok := c.writer.(*zlib.Writer); ok {
zlibWriter.Flush()
}
if gzipWriter, ok := c.writer.(*gzip.Writer); ok {
gzipWriter.Flush()
}
if closer, ok := c.writer.(io.Closer); ok {
defer closer.Close()
}
}

// Constructs a new compressedResponseWriter based on client request headers.
func newCompressedResponseWriter(writer http.ResponseWriter, req *http.Request) *compressedResponseWriter {
encodings := strings.Split(req.Header.Get(acceptEncodingHeader), ",")
for _, encoding := range encodings {
switch strings.TrimSpace(encoding) {
case gzipEncoding:
writer.Header().Set(contentEncodingHeader, gzipEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: gzip.NewWriter(writer),
}
case deflateEncoding:
writer.Header().Set(contentEncodingHeader, deflateEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: zlib.NewWriter(writer),
}
}
}
return &compressedResponseWriter{
// Constructs a new deflatedResponseWriter to compress the original writer using 'deflate' compression.
func newDeflateResponseWriter(writer http.ResponseWriter) *deflatedResponseWriter {
return &deflatedResponseWriter{
ResponseWriter: writer,
writer: writer,
writer: zlib.NewWriter(writer),
}
}

Expand All @@ -86,7 +67,21 @@ type CompressionHandler struct {

// ServeHTTP adds compression to the original http.Handler's ServeHTTP() method.
func (c CompressionHandler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
compWriter := newCompressedResponseWriter(writer, req)
c.Handler.ServeHTTP(compWriter, req)
compWriter.Close()
encodings := strings.Split(req.Header.Get(acceptEncodingHeader), ",")
for _, encoding := range encodings {
switch strings.TrimSpace(encoding) {
case gzipEncoding:
gzhttp.GzipHandler(c.Handler).ServeHTTP(writer, req)
return
case deflateEncoding:
compWriter := newDeflateResponseWriter(writer)
writer.Header().Set(contentEncodingHeader, deflateEncoding)
c.Handler.ServeHTTP(compWriter, req)
compWriter.Close()
return
default:
c.Handler.ServeHTTP(writer, req)
return
}
}
}
115 changes: 106 additions & 9 deletions util/httputil/compression_test.go
Expand Up @@ -17,31 +17,38 @@ import (
"bytes"
"compress/gzip"
"compress/zlib"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/prometheus/prometheus/model/labels"
)

var (
mux *http.ServeMux
server *httptest.Server
mux *http.ServeMux
server *httptest.Server
respBody = strings.Repeat("Hello World!", 500)
)

func setup() func() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
return func() {
server.CloseClientConnections()
server.Close()
}
}

func getCompressionHandlerFunc() CompressionHandler {
hf := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello World!"))
w.Write([]byte(respBody))
}
return CompressionHandler{
Handler: http.HandlerFunc(hf),
Expand All @@ -67,9 +74,8 @@ func TestCompressionHandler_PlainText(t *testing.T) {
contents, err := io.ReadAll(resp.Body)
require.NoError(t, err, "unexpected error while creating the response body reader")

expected := "Hello World!"
actual := string(contents)
require.Equal(t, expected, actual, "expected response with content")
require.Equal(t, respBody, actual, "expected response with content")
}

func TestCompressionHandler_Gzip(t *testing.T) {
Expand Down Expand Up @@ -103,8 +109,7 @@ func TestCompressionHandler_Gzip(t *testing.T) {
require.NoError(t, err, "unexpected error while reading the response body")

actual := buf.String()
expected := "Hello World!"
require.Equal(t, expected, actual, "unexpected response content")
require.Equal(t, respBody, actual, "unexpected response content")
}

func TestCompressionHandler_Deflate(t *testing.T) {
Expand Down Expand Up @@ -138,6 +143,98 @@ func TestCompressionHandler_Deflate(t *testing.T) {
require.NoError(t, err, "unexpected error while reading the response body")

actual := buf.String()
expected := "Hello World!"
require.Equal(t, expected, actual, "expected response with content")
require.Equal(t, respBody, actual, "expected response with content")
}

func Benchmark_compression(b *testing.B) {
bboreham marked this conversation as resolved.
Show resolved Hide resolved
client := &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
}

cases := map[string]struct {
enc string
numberOfLabels int
}{
"gzip-10-labels": {
enc: gzipEncoding,
numberOfLabels: 10,
},
"gzip-100-labels": {
enc: gzipEncoding,
numberOfLabels: 100,
},
"gzip-1K-labels": {
enc: gzipEncoding,
numberOfLabels: 1000,
},
"gzip-10K-labels": {
enc: gzipEncoding,
numberOfLabels: 10000,
},
"gzip-100K-labels": {
enc: gzipEncoding,
numberOfLabels: 100000,
},
"gzip-1M-labels": {
enc: gzipEncoding,
numberOfLabels: 1000000,
},
}

for name, tc := range cases {
b.Run(name, func(b *testing.B) {
tearDown := setup()
defer tearDown()
labels := labels.ScratchBuilder{}

for i := 0; i < tc.numberOfLabels; i++ {
labels.Add(fmt.Sprintf("Name%v", i), fmt.Sprintf("Value%v", i))
}

respBody, err := json.Marshal(labels.Labels())
require.NoError(b, err)

hf := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(respBody)
}
h := CompressionHandler{
Handler: http.HandlerFunc(hf),
}

mux.Handle("/foo_endpoint", h)

req, _ := http.NewRequest("GET", server.URL+"/foo_endpoint", nil)
req.Header.Set(acceptEncodingHeader, tc.enc)

b.ReportAllocs()
b.ResetTimer()

// Reusing the array to read the body and avoid allocation on the test
encRespBody := make([]byte, len(respBody))

for i := 0; i < b.N; i++ {
resp, err := client.Do(req)

require.NoError(b, err)

require.NoError(b, err, "client get failed with unexpected error")
responseBodySize := 0
for {
n, err := resp.Body.Read(encRespBody)
responseBodySize += n
if err == io.EOF {
break
}
}

b.ReportMetric(float64(responseBodySize), "ContentLength")
resp.Body.Close()
}

client.CloseIdleConnections()
})
}
}