Skip to content

Commit 5976b00

Browse files
benluddyk8s-publishing-bot
authored andcommitted
Don't pool large CBOR encode buffers.
Objects in a sync.Pool are assumed to be fungible. This is not a good assumption for pools of *bytes.Buffer because a *bytes.Buffer's underlying array grows as needed to accomodate writes. In Kubernetes, apiservers tend to encode "small" objects very frequently and much larger objects (especially large lists) only occasionally. Under steady load, pooled buffers tend to be borrowed frequently enough to prevent them from being released. Over time, each buffer is used to encode a large object and its capacity increases accordingly. The result is that practically all buffers in the pool retain much more capacity than needed to encode most objects. As a basic mitigation for the worst case, buffers with more capacity than the default max request body size are never returned to the pool. Kubernetes-commit: a19d142f0da60841887bb4be4f33e7b99a2b0ea8
1 parent 56f28d1 commit 5976b00

File tree

7 files changed

+287
-92
lines changed

7 files changed

+287
-92
lines changed

pkg/runtime/serializer/cbor/cbor.go

+22-3
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,35 @@ func (s *serializer) Identifier() runtime.Identifier {
9999
return "cbor"
100100
}
101101

102+
// Encode writes a CBOR representation of the given object.
103+
//
104+
// Because the CBOR data item written by a call to Encode is always enclosed in the "self-described
105+
// CBOR" tag, its encoded form always has the prefix 0xd9d9f7. This prefix is suitable for use as a
106+
// "magic number" for distinguishing encoded CBOR from other protocols.
107+
//
108+
// The default serialization behavior for any given object replicates the behavior of the JSON
109+
// serializer as far as it is necessary to allow the CBOR serializer to be used as a drop-in
110+
// replacement for the JSON serializer, with limited exceptions. For example, the distinction
111+
// between integers and floating-point numbers is preserved in CBOR due to its distinct
112+
// representations for each type.
113+
//
114+
// Objects implementing runtime.Unstructured will have their unstructured content encoded rather
115+
// than following the default behavior for their dynamic type.
102116
func (s *serializer) Encode(obj runtime.Object, w io.Writer) error {
117+
return s.encode(modes.Encode, obj, w)
118+
}
119+
120+
func (s *serializer) encode(mode modes.EncMode, obj runtime.Object, w io.Writer) error {
103121
if _, err := w.Write(selfDescribedCBOR); err != nil {
104122
return err
105123
}
106124

107-
e := modes.Encode.NewEncoder(w)
125+
var v interface{} = obj
108126
if u, ok := obj.(runtime.Unstructured); ok {
109-
return e.Encode(u.UnstructuredContent())
127+
v = u.UnstructuredContent()
110128
}
111-
return e.Encode(obj)
129+
130+
return mode.MarshalTo(v, w)
112131
}
113132

114133
// gvkWithDefaults returns group kind and version defaulting from provided default
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package modes
18+
19+
import (
20+
"bytes"
21+
"sync"
22+
)
23+
24+
var buffers = BufferProvider{p: new(sync.Pool)}
25+
26+
type buffer struct {
27+
bytes.Buffer
28+
}
29+
30+
type pool interface {
31+
Get() interface{}
32+
Put(interface{})
33+
}
34+
35+
type BufferProvider struct {
36+
p pool
37+
}
38+
39+
func (b *BufferProvider) Get() *buffer {
40+
if buf, ok := b.p.Get().(*buffer); ok {
41+
return buf
42+
}
43+
return &buffer{}
44+
}
45+
46+
func (b *BufferProvider) Put(buf *buffer) {
47+
if buf.Cap() > 3*1024*1024 /* Default MaxRequestBodyBytes */ {
48+
// Objects in a sync.Pool are assumed to be fungible. This is not a good assumption
49+
// for pools of *bytes.Buffer because a *bytes.Buffer's underlying array grows as
50+
// needed to accommodate writes. In Kubernetes, apiservers tend to encode "small"
51+
// objects very frequently and much larger objects (especially large lists) only
52+
// occasionally. Under steady load, pooled buffers tend to be borrowed frequently
53+
// enough to prevent them from being released. Over time, each buffer is used to
54+
// encode a large object and its capacity increases accordingly. The result is that
55+
// practically all buffers in the pool retain much more capacity than needed to
56+
// encode most objects.
57+
58+
// As a basic mitigation for the worst case, buffers with more capacity than the
59+
// default max request body size are never returned to the pool.
60+
// TODO: Optimize for higher buffer utilization.
61+
return
62+
}
63+
buf.Reset()
64+
b.p.Put(buf)
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package modes
18+
19+
import (
20+
"testing"
21+
)
22+
23+
type mockPool struct {
24+
v interface{}
25+
}
26+
27+
func (*mockPool) Get() interface{} {
28+
return nil
29+
}
30+
31+
func (p *mockPool) Put(v interface{}) {
32+
p.v = v
33+
}
34+
35+
func TestBufferProviderPut(t *testing.T) {
36+
{
37+
p := new(mockPool)
38+
bp := &BufferProvider{p: p}
39+
small := new(buffer)
40+
small.Grow(3 * 1024 * 1024)
41+
small.WriteString("hello world")
42+
bp.Put(small)
43+
if p.v != small {
44+
t.Errorf("expected buf with capacity %d to be returned to pool", small.Cap())
45+
}
46+
if small.Len() != 0 {
47+
t.Errorf("expected buf to be reset before returning to pool")
48+
}
49+
}
50+
51+
{
52+
p := new(mockPool)
53+
bp := &BufferProvider{p: p}
54+
big := new(buffer)
55+
big.Grow(3*1024*1024 + 1)
56+
bp.Put(big)
57+
if p.v != nil {
58+
t.Errorf("expected buf with capacity %d not to be returned to pool", big.Cap())
59+
}
60+
}
61+
}

pkg/runtime/serializer/cbor/internal/modes/encode.go

+131-85
Original file line numberDiff line numberDiff line change
@@ -17,93 +17,139 @@ limitations under the License.
1717
package modes
1818

1919
import (
20+
"io"
21+
2022
"github.com/fxamacker/cbor/v2"
2123
)
2224

23-
var Encode cbor.EncMode = func() cbor.EncMode {
24-
encode, err := cbor.EncOptions{
25-
// Map keys need to be sorted to have deterministic output, and this is the order
26-
// defined in RFC 8949 4.2.1 "Core Deterministic Encoding Requirements".
27-
Sort: cbor.SortBytewiseLexical,
28-
29-
// CBOR supports distinct types for IEEE-754 float16, float32, and float64. Store
30-
// floats in the smallest width that preserves value so that equivalent float32 and
31-
// float64 values encode to identical bytes, as they do in a JSON
32-
// encoding. Satisfies one of the "Core Deterministic Encoding Requirements".
33-
ShortestFloat: cbor.ShortestFloat16,
34-
35-
// Error on attempt to encode NaN and infinite values. This is what the JSON
36-
// serializer does.
37-
NaNConvert: cbor.NaNConvertReject,
38-
InfConvert: cbor.InfConvertReject,
39-
40-
// Error on attempt to encode math/big.Int values, which can't be faithfully
41-
// roundtripped through Unstructured in general (the dynamic numeric types allowed
42-
// in Unstructured are limited to float64 and int64).
43-
BigIntConvert: cbor.BigIntConvertReject,
44-
45-
// MarshalJSON for time.Time writes RFC3339 with nanos.
46-
Time: cbor.TimeRFC3339Nano,
47-
48-
// The decoder must be able to accept RFC3339 strings with or without tag 0 (e.g. by
49-
// the end of time.Time -> JSON -> Unstructured -> CBOR, the CBOR encoder has no
50-
// reliable way of knowing that a particular string originated from serializing a
51-
// time.Time), so producing tag 0 has little use.
52-
TimeTag: cbor.EncTagNone,
53-
54-
// Indefinite-length items have multiple encodings and aren't being used anyway, so
55-
// disable to avoid an opportunity for nondeterminism.
56-
IndefLength: cbor.IndefLengthForbidden,
57-
58-
// Preserve distinction between nil and empty for slices and maps.
59-
NilContainers: cbor.NilContainerAsNull,
60-
61-
// OK to produce tags.
62-
TagsMd: cbor.TagsAllowed,
63-
64-
// Use the same definition of "empty" as encoding/json.
65-
OmitEmpty: cbor.OmitEmptyGoValue,
66-
67-
// The CBOR types text string and byte string are structurally equivalent, with the
68-
// semantic difference that a text string whose content is an invalid UTF-8 sequence
69-
// is itself invalid. We reject all invalid text strings at decode time and do not
70-
// validate or sanitize all Go strings at encode time. Encoding Go strings to the
71-
// byte string type is comparable to the existing Protobuf behavior and cheaply
72-
// ensures that the output is valid CBOR.
73-
String: cbor.StringToByteString,
74-
75-
// Encode struct field names to the byte string type rather than the text string
76-
// type.
77-
FieldName: cbor.FieldNameToByteString,
78-
79-
// Marshal Go byte arrays to CBOR arrays of integers (as in JSON) instead of byte
80-
// strings.
81-
ByteArray: cbor.ByteArrayToArray,
82-
83-
// Marshal []byte to CBOR byte string enclosed in tag 22 (expected later base64
84-
// encoding, https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.5.2), to
85-
// interoperate with the existing JSON behavior. This indicates to the decoder that,
86-
// when decoding into a string (or unstructured), the resulting value should be the
87-
// base64 encoding of the original bytes. No base64 encoding or decoding needs to be
88-
// performed for []byte-to-CBOR-to-[]byte roundtrips.
89-
ByteSliceLaterFormat: cbor.ByteSliceLaterFormatBase64,
90-
91-
// Disable default recognition of types implementing encoding.BinaryMarshaler, which
92-
// is not recognized for JSON encoding.
93-
BinaryMarshaler: cbor.BinaryMarshalerNone,
94-
}.EncMode()
95-
if err != nil {
96-
panic(err)
25+
var Encode = EncMode{
26+
delegate: func() cbor.UserBufferEncMode {
27+
encode, err := cbor.EncOptions{
28+
// Map keys need to be sorted to have deterministic output, and this is the order
29+
// defined in RFC 8949 4.2.1 "Core Deterministic Encoding Requirements".
30+
Sort: cbor.SortBytewiseLexical,
31+
32+
// CBOR supports distinct types for IEEE-754 float16, float32, and float64. Store
33+
// floats in the smallest width that preserves value so that equivalent float32 and
34+
// float64 values encode to identical bytes, as they do in a JSON
35+
// encoding. Satisfies one of the "Core Deterministic Encoding Requirements".
36+
ShortestFloat: cbor.ShortestFloat16,
37+
38+
// Error on attempt to encode NaN and infinite values. This is what the JSON
39+
// serializer does.
40+
NaNConvert: cbor.NaNConvertReject,
41+
InfConvert: cbor.InfConvertReject,
42+
43+
// Error on attempt to encode math/big.Int values, which can't be faithfully
44+
// roundtripped through Unstructured in general (the dynamic numeric types allowed
45+
// in Unstructured are limited to float64 and int64).
46+
BigIntConvert: cbor.BigIntConvertReject,
47+
48+
// MarshalJSON for time.Time writes RFC3339 with nanos.
49+
Time: cbor.TimeRFC3339Nano,
50+
51+
// The decoder must be able to accept RFC3339 strings with or without tag 0 (e.g. by
52+
// the end of time.Time -> JSON -> Unstructured -> CBOR, the CBOR encoder has no
53+
// reliable way of knowing that a particular string originated from serializing a
54+
// time.Time), so producing tag 0 has little use.
55+
TimeTag: cbor.EncTagNone,
56+
57+
// Indefinite-length items have multiple encodings and aren't being used anyway, so
58+
// disable to avoid an opportunity for nondeterminism.
59+
IndefLength: cbor.IndefLengthForbidden,
60+
61+
// Preserve distinction between nil and empty for slices and maps.
62+
NilContainers: cbor.NilContainerAsNull,
63+
64+
// OK to produce tags.
65+
TagsMd: cbor.TagsAllowed,
66+
67+
// Use the same definition of "empty" as encoding/json.
68+
OmitEmpty: cbor.OmitEmptyGoValue,
69+
70+
// The CBOR types text string and byte string are structurally equivalent, with the
71+
// semantic difference that a text string whose content is an invalid UTF-8 sequence
72+
// is itself invalid. We reject all invalid text strings at decode time and do not
73+
// validate or sanitize all Go strings at encode time. Encoding Go strings to the
74+
// byte string type is comparable to the existing Protobuf behavior and cheaply
75+
// ensures that the output is valid CBOR.
76+
String: cbor.StringToByteString,
77+
78+
// Encode struct field names to the byte string type rather than the text string
79+
// type.
80+
FieldName: cbor.FieldNameToByteString,
81+
82+
// Marshal Go byte arrays to CBOR arrays of integers (as in JSON) instead of byte
83+
// strings.
84+
ByteArray: cbor.ByteArrayToArray,
85+
86+
// Marshal []byte to CBOR byte string enclosed in tag 22 (expected later base64
87+
// encoding, https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.5.2), to
88+
// interoperate with the existing JSON behavior. This indicates to the decoder that,
89+
// when decoding into a string (or unstructured), the resulting value should be the
90+
// base64 encoding of the original bytes. No base64 encoding or decoding needs to be
91+
// performed for []byte-to-CBOR-to-[]byte roundtrips.
92+
ByteSliceLaterFormat: cbor.ByteSliceLaterFormatBase64,
93+
94+
// Disable default recognition of types implementing encoding.BinaryMarshaler, which
95+
// is not recognized for JSON encoding.
96+
BinaryMarshaler: cbor.BinaryMarshalerNone,
97+
}.UserBufferEncMode()
98+
if err != nil {
99+
panic(err)
100+
}
101+
return encode
102+
}(),
103+
}
104+
105+
var EncodeNondeterministic = EncMode{
106+
delegate: func() cbor.UserBufferEncMode {
107+
opts := Encode.options()
108+
opts.Sort = cbor.SortNone // TODO: Use cbor.SortFastShuffle after bump to v2.7.0.
109+
em, err := opts.UserBufferEncMode()
110+
if err != nil {
111+
panic(err)
112+
}
113+
return em
114+
}(),
115+
}
116+
117+
type EncMode struct {
118+
delegate cbor.UserBufferEncMode
119+
}
120+
121+
func (em EncMode) options() cbor.EncOptions {
122+
return em.delegate.EncOptions()
123+
}
124+
125+
func (em EncMode) MarshalTo(v interface{}, w io.Writer) error {
126+
if buf, ok := w.(*buffer); ok {
127+
return em.delegate.MarshalToBuffer(v, &buf.Buffer)
128+
}
129+
130+
buf := buffers.Get()
131+
defer buffers.Put(buf)
132+
if err := em.delegate.MarshalToBuffer(v, &buf.Buffer); err != nil {
133+
return err
134+
}
135+
136+
if _, err := io.Copy(w, buf); err != nil {
137+
return err
97138
}
98-
return encode
99-
}()
100-
101-
var EncodeNondeterministic cbor.EncMode = func() cbor.EncMode {
102-
opts := Encode.EncOptions()
103-
opts.Sort = cbor.SortNone
104-
em, err := opts.EncMode()
105-
if err != nil {
106-
panic(err)
139+
140+
return nil
141+
}
142+
143+
func (em EncMode) Marshal(v interface{}) ([]byte, error) {
144+
buf := buffers.Get()
145+
defer buffers.Put(buf)
146+
147+
if err := em.MarshalTo(v, &buf.Buffer); err != nil {
148+
return nil, err
107149
}
108-
return em
109-
}()
150+
151+
clone := make([]byte, buf.Len())
152+
copy(clone, buf.Bytes())
153+
154+
return clone, nil
155+
}

0 commit comments

Comments
 (0)