Merge pull request #130281 from z1cheng/issue_130264
Implement chunking for gzip encoder in deferredResponseWriter Kubernetes-commit: 25dc6c98209b50db1f0a023020003a4051b06138
This commit is contained in:
commit
205c0f56b5
|
@ -157,6 +157,9 @@ const (
|
||||||
// (usually the entire object), and if the size is smaller no gzipping will be performed
|
// (usually the entire object), and if the size is smaller no gzipping will be performed
|
||||||
// if the client requests it.
|
// if the client requests it.
|
||||||
defaultGzipThresholdBytes = 128 * 1024
|
defaultGzipThresholdBytes = 128 * 1024
|
||||||
|
// Use the length of the first write of streaming implementations.
|
||||||
|
// TODO: Update when streaming proto is implemented
|
||||||
|
firstWriteStreamingThresholdBytes = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// negotiateContentEncoding returns a supported client-requested content encoding for the
|
// negotiateContentEncoding returns a supported client-requested content encoding for the
|
||||||
|
@ -192,14 +195,53 @@ type deferredResponseWriter struct {
|
||||||
statusCode int
|
statusCode int
|
||||||
contentEncoding string
|
contentEncoding string
|
||||||
|
|
||||||
hasWritten bool
|
hasBuffered bool
|
||||||
hw http.ResponseWriter
|
buffer []byte
|
||||||
w io.Writer
|
hasWritten bool
|
||||||
|
hw http.ResponseWriter
|
||||||
|
w io.Writer
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *deferredResponseWriter) Write(p []byte) (n int, err error) {
|
func (w *deferredResponseWriter) Write(p []byte) (n int, err error) {
|
||||||
|
switch {
|
||||||
|
case w.hasWritten:
|
||||||
|
// already written, cannot buffer
|
||||||
|
return w.unbufferedWrite(p)
|
||||||
|
|
||||||
|
case w.contentEncoding != "gzip":
|
||||||
|
// non-gzip, no need to buffer
|
||||||
|
return w.unbufferedWrite(p)
|
||||||
|
|
||||||
|
case !w.hasBuffered && len(p) > defaultGzipThresholdBytes:
|
||||||
|
// not yet buffered, first write is long enough to trigger gzip, no need to buffer
|
||||||
|
return w.unbufferedWrite(p)
|
||||||
|
|
||||||
|
case !w.hasBuffered && len(p) > firstWriteStreamingThresholdBytes:
|
||||||
|
// not yet buffered, first write is longer than expected for streaming scenarios that would require buffering, no need to buffer
|
||||||
|
return w.unbufferedWrite(p)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if !w.hasBuffered {
|
||||||
|
w.hasBuffered = true
|
||||||
|
// Start at 80 bytes to avoid rapid reallocation of the buffer.
|
||||||
|
// The minimum size of a 0-item serialized list object is 80 bytes:
|
||||||
|
// {"kind":"List","apiVersion":"v1","metadata":{"resourceVersion":"1"},"items":[]}\n
|
||||||
|
w.buffer = make([]byte, 0, max(80, len(p)))
|
||||||
|
}
|
||||||
|
w.buffer = append(w.buffer, p...)
|
||||||
|
var err error
|
||||||
|
if len(w.buffer) > defaultGzipThresholdBytes {
|
||||||
|
// we've accumulated enough to trigger gzip, write and clear buffer
|
||||||
|
_, err = w.unbufferedWrite(w.buffer)
|
||||||
|
w.buffer = nil
|
||||||
|
}
|
||||||
|
return len(p), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *deferredResponseWriter) unbufferedWrite(p []byte) (n int, err error) {
|
||||||
ctx := w.ctx
|
ctx := w.ctx
|
||||||
span := tracing.SpanFromContext(ctx)
|
span := tracing.SpanFromContext(ctx)
|
||||||
// This Step usually wraps in-memory object serialization.
|
// This Step usually wraps in-memory object serialization.
|
||||||
|
@ -245,11 +287,17 @@ func (w *deferredResponseWriter) Write(p []byte) (n int, err error) {
|
||||||
return w.w.Write(p)
|
return w.w.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *deferredResponseWriter) Close() error {
|
func (w *deferredResponseWriter) Close() (err error) {
|
||||||
if !w.hasWritten {
|
if !w.hasWritten {
|
||||||
return nil
|
if !w.hasBuffered {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// never reached defaultGzipThresholdBytes, no need to do the gzip writer cleanup
|
||||||
|
_, err := w.unbufferedWrite(w.buffer)
|
||||||
|
w.buffer = nil
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
switch t := w.w.(type) {
|
switch t := w.w.(type) {
|
||||||
case *gzip.Writer:
|
case *gzip.Writer:
|
||||||
err = t.Close()
|
err = t.Close()
|
||||||
|
|
|
@ -33,7 +33,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -42,6 +41,7 @@ import (
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
rand2 "k8s.io/apimachinery/pkg/util/rand"
|
||||||
"k8s.io/apimachinery/pkg/util/uuid"
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
@ -378,29 +378,94 @@ func TestDeferredResponseWriter_Write(t *testing.T) {
|
||||||
largeChunk := bytes.Repeat([]byte("b"), defaultGzipThresholdBytes+1)
|
largeChunk := bytes.Repeat([]byte("b"), defaultGzipThresholdBytes+1)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
chunks [][]byte
|
chunks [][]byte
|
||||||
expectGzip bool
|
expectGzip bool
|
||||||
|
expectHeaders http.Header
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "no writes",
|
||||||
|
chunks: nil,
|
||||||
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one empty write",
|
||||||
|
chunks: [][]byte{{}},
|
||||||
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one single byte write",
|
||||||
|
chunks: [][]byte{{'{'}},
|
||||||
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "one small chunk write",
|
name: "one small chunk write",
|
||||||
chunks: [][]byte{smallChunk},
|
chunks: [][]byte{smallChunk},
|
||||||
expectGzip: false,
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "two small chunk writes",
|
name: "two small chunk writes",
|
||||||
chunks: [][]byte{smallChunk, smallChunk},
|
chunks: [][]byte{smallChunk, smallChunk},
|
||||||
expectGzip: false,
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one single byte and one small chunk write",
|
||||||
|
chunks: [][]byte{{'{'}, smallChunk},
|
||||||
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two single bytes and one small chunk write",
|
||||||
|
chunks: [][]byte{{'{'}, {'{'}, smallChunk},
|
||||||
|
expectGzip: true,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
"Content-Encoding": []string{"gzip"},
|
||||||
|
"Vary": []string{"Accept-Encoding"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "one large chunk writes",
|
name: "one large chunk writes",
|
||||||
chunks: [][]byte{largeChunk},
|
chunks: [][]byte{largeChunk},
|
||||||
expectGzip: true,
|
expectGzip: true,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
"Content-Encoding": []string{"gzip"},
|
||||||
|
"Vary": []string{"Accept-Encoding"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "two large chunk writes",
|
name: "two large chunk writes",
|
||||||
chunks: [][]byte{largeChunk, largeChunk},
|
chunks: [][]byte{largeChunk, largeChunk},
|
||||||
expectGzip: true,
|
expectGzip: true,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
"Content-Encoding": []string{"gzip"},
|
||||||
|
"Vary": []string{"Accept-Encoding"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one small chunk and one large chunk write",
|
||||||
|
chunks: [][]byte{smallChunk, largeChunk},
|
||||||
|
expectGzip: false,
|
||||||
|
expectHeaders: http.Header{
|
||||||
|
"Content-Type": []string{"text/plain"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,8 +506,9 @@ func TestDeferredResponseWriter_Write(t *testing.T) {
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
t.Fatalf("status code is not writtend properly, expected: 200, got: %d", res.StatusCode)
|
t.Fatalf("status code is not writtend properly, expected: 200, got: %d", res.StatusCode)
|
||||||
}
|
}
|
||||||
contentEncoding := res.Header.Get("Content-Encoding")
|
if !reflect.DeepEqual(res.Header, tt.expectHeaders) {
|
||||||
varyHeader := res.Header.Get("Vary")
|
t.Fatal(cmp.Diff(tt.expectHeaders, res.Header))
|
||||||
|
}
|
||||||
|
|
||||||
resBytes, err := io.ReadAll(res.Body)
|
resBytes, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -450,14 +516,6 @@ func TestDeferredResponseWriter_Write(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.expectGzip {
|
if tt.expectGzip {
|
||||||
if contentEncoding != "gzip" {
|
|
||||||
t.Fatalf("content-encoding is not set properly, expected: gzip, got: %s", contentEncoding)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(varyHeader, "Accept-Encoding") {
|
|
||||||
t.Errorf("vary header doesn't have Accept-Encoding")
|
|
||||||
}
|
|
||||||
|
|
||||||
gr, err := gzip.NewReader(bytes.NewReader(resBytes))
|
gr, err := gzip.NewReader(bytes.NewReader(resBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create gzip reader: %v", err)
|
t.Fatalf("failed to create gzip reader: %v", err)
|
||||||
|
@ -471,22 +529,101 @@ func TestDeferredResponseWriter_Write(t *testing.T) {
|
||||||
if !bytes.Equal(fullPayload, decompressed) {
|
if !bytes.Equal(fullPayload, decompressed) {
|
||||||
t.Errorf("payload mismatch, expected: %s, got: %s", fullPayload, decompressed)
|
t.Errorf("payload mismatch, expected: %s, got: %s", fullPayload, decompressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if contentEncoding != "" {
|
|
||||||
t.Errorf("content-encoding is set unexpectedly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(varyHeader, "Accept-Encoding") {
|
|
||||||
t.Errorf("accept encoding is set unexpectedly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(fullPayload, resBytes) {
|
if !bytes.Equal(fullPayload, resBytes) {
|
||||||
t.Errorf("payload mismatch, expected: %s, got: %s", fullPayload, resBytes)
|
t.Errorf("payload mismatch, expected: %s, got: %s", fullPayload, resBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchmarkChunkingGzip(b *testing.B, count int, chunk []byte) {
|
||||||
|
mockResponseWriter := httptest.NewRecorder()
|
||||||
|
mockResponseWriter.Body = nil
|
||||||
|
|
||||||
|
drw := &deferredResponseWriter{
|
||||||
|
mediaType: "text/plain",
|
||||||
|
statusCode: 200,
|
||||||
|
contentEncoding: "gzip",
|
||||||
|
hw: mockResponseWriter,
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
n, err := drw.Write(chunk)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("unexpected error while writing chunk: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(chunk) {
|
||||||
|
b.Errorf("write is not complete, expected: %d bytes, written: %d bytes", len(chunk), n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := drw.Close()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("unexpected error when closing deferredResponseWriter: %v", err)
|
||||||
|
}
|
||||||
|
res := mockResponseWriter.Result()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
b.Fatalf("status code is not writtend properly, expected: 200, got: %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkChunkingGzip(b *testing.B) {
|
||||||
|
tests := []struct {
|
||||||
|
count int
|
||||||
|
size int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
count: 100,
|
||||||
|
size: 1_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 100,
|
||||||
|
size: 100_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1_000,
|
||||||
|
size: 100_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1_000,
|
||||||
|
size: 1_000_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 10_000,
|
||||||
|
size: 100_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 100_000,
|
||||||
|
size: 10_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
size: 100_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
size: 1_000_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
size: 10_000_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
size: 100_000_000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
size: 1_000_000_000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tests {
|
||||||
|
b.Run(fmt.Sprintf("Count=%d/Size=%d", t.count, t.size), func(b *testing.B) {
|
||||||
|
chunk := []byte(rand2.String(t.size))
|
||||||
|
benchmarkChunkingGzip(b, t.count, chunk)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue