Skip to content

Commit b3824cd

Browse files
authored
Merge pull request kubernetes#75570 from smarterclayton/fix_performance
Fix a regression in watch performance and reduce allocations in the GET path
2 parents e739b55 + 0489d0b commit b3824cd

File tree

13 files changed

+211
-71
lines changed

13 files changed

+211
-71
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,8 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
644644
return []runtime.SerializerInfo{
645645
{
646646
MediaType: "application/json",
647+
MediaTypeType: "application",
648+
MediaTypeSubType: "json",
647649
EncodesAsText: true,
648650
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
649651
PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, true),
@@ -654,9 +656,11 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
654656
},
655657
},
656658
{
657-
MediaType: "application/yaml",
658-
EncodesAsText: true,
659-
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
659+
MediaType: "application/yaml",
660+
MediaTypeType: "application",
661+
MediaTypeSubType: "yaml",
662+
EncodesAsText: true,
663+
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
660664
},
661665
}
662666
}

staging/src/k8s.io/apimachinery/pkg/api/meta/help.go

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,50 +17,96 @@ limitations under the License.
1717
package meta
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"reflect"
23+
"sync"
2224

2325
"k8s.io/apimachinery/pkg/conversion"
2426
"k8s.io/apimachinery/pkg/runtime"
2527
)
2628

27-
// IsListType returns true if the provided Object has a slice called Items
29+
var (
30+
// isListCache maintains a cache of types that are checked for lists
31+
// which is used by IsListType.
32+
// TODO: remove and replace with an interface check
33+
isListCache = struct {
34+
lock sync.RWMutex
35+
byType map[reflect.Type]bool
36+
}{
37+
byType: make(map[reflect.Type]bool, 1024),
38+
}
39+
)
40+
41+
// IsListType returns true if the provided Object has a slice called Items.
42+
// TODO: Replace the code in this check with an interface comparison by
43+
// creating and enforcing that lists implement a list accessor.
2844
func IsListType(obj runtime.Object) bool {
29-
// if we're a runtime.Unstructured, check whether this is a list.
30-
// TODO: refactor GetItemsPtr to use an interface that returns []runtime.Object
31-
if unstructured, ok := obj.(runtime.Unstructured); ok {
32-
return unstructured.IsList()
45+
switch t := obj.(type) {
46+
case runtime.Unstructured:
47+
return t.IsList()
3348
}
49+
t := reflect.TypeOf(obj)
3450

35-
_, err := GetItemsPtr(obj)
36-
return err == nil
51+
isListCache.lock.RLock()
52+
ok, exists := isListCache.byType[t]
53+
isListCache.lock.RUnlock()
54+
55+
if !exists {
56+
_, err := getItemsPtr(obj)
57+
ok = err == nil
58+
59+
// cache only the first 1024 types
60+
isListCache.lock.Lock()
61+
if len(isListCache.byType) < 1024 {
62+
isListCache.byType[t] = ok
63+
}
64+
isListCache.lock.Unlock()
65+
}
66+
67+
return ok
3768
}
3869

70+
var (
71+
errExpectFieldItems = errors.New("no Items field in this object")
72+
errExpectSliceItems = errors.New("Items field must be a slice of objects")
73+
)
74+
3975
// GetItemsPtr returns a pointer to the list object's Items member.
4076
// If 'list' doesn't have an Items member, it's not really a list type
4177
// and an error will be returned.
4278
// This function will either return a pointer to a slice, or an error, but not both.
79+
// TODO: this will be replaced with an interface in the future
4380
func GetItemsPtr(list runtime.Object) (interface{}, error) {
81+
obj, err := getItemsPtr(list)
82+
if err != nil {
83+
return nil, fmt.Errorf("%T is not a list: %v", err)
84+
}
85+
return obj, nil
86+
}
87+
88+
// getItemsPtr returns a pointer to the list object's Items member or an error.
89+
func getItemsPtr(list runtime.Object) (interface{}, error) {
4490
v, err := conversion.EnforcePtr(list)
4591
if err != nil {
4692
return nil, err
4793
}
4894

4995
items := v.FieldByName("Items")
5096
if !items.IsValid() {
51-
return nil, fmt.Errorf("no Items field in %#v", list)
97+
return nil, errExpectFieldItems
5298
}
5399
switch items.Kind() {
54100
case reflect.Interface, reflect.Ptr:
55101
target := reflect.TypeOf(items.Interface()).Elem()
56102
if target.Kind() != reflect.Slice {
57-
return nil, fmt.Errorf("items: Expected slice, got %s", target.Kind())
103+
return nil, errExpectSliceItems
58104
}
59105
return items.Interface(), nil
60106
case reflect.Slice:
61107
return items.Addr().Interface(), nil
62108
default:
63-
return nil, fmt.Errorf("items: Expected slice, got %s", items.Kind())
109+
return nil, errExpectSliceItems
64110
}
65111
}
66112

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme/scheme.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
5151
return []runtime.SerializerInfo{
5252
{
5353
MediaType: "application/json",
54+
MediaTypeType: "application",
55+
MediaTypeSubType: "json",
5456
EncodesAsText: true,
5557
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
5658
PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, true),
@@ -61,9 +63,11 @@ func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.Serial
6163
},
6264
},
6365
{
64-
MediaType: "application/yaml",
65-
EncodesAsText: true,
66-
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
66+
MediaType: "application/yaml",
67+
MediaTypeType: "application",
68+
MediaTypeSubType: "yaml",
69+
EncodesAsText: true,
70+
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
6771
},
6872
}
6973
}

staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ type Framer interface {
9191
type SerializerInfo struct {
9292
// MediaType is the value that represents this serializer over the wire.
9393
MediaType string
94+
// MediaTypeType is the first part of the MediaType ("application" in "application/json").
95+
MediaTypeType string
96+
// MediaTypeSubType is the second part of the MediaType ("json" in "application/json").
97+
MediaTypeSubType string
9498
// EncodesAsText indicates this serializer can be encoded to UTF-8 safely.
9599
EncodesAsText bool
96100
// Serializer is the individual object serializer for this media type.

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ limitations under the License.
1717
package serializer
1818

1919
import (
20+
"mime"
21+
"strings"
22+
2023
"k8s.io/apimachinery/pkg/runtime"
2124
"k8s.io/apimachinery/pkg/runtime/schema"
2225
"k8s.io/apimachinery/pkg/runtime/serializer/json"
@@ -120,6 +123,15 @@ func newCodecFactory(scheme *runtime.Scheme, serializers []serializerType) Codec
120123
Serializer: d.Serializer,
121124
PrettySerializer: d.PrettySerializer,
122125
}
126+
127+
mediaType, _, err := mime.ParseMediaType(info.MediaType)
128+
if err != nil {
129+
panic(err)
130+
}
131+
parts := strings.SplitN(mediaType, "/", 2)
132+
info.MediaTypeType = parts[0]
133+
info.MediaTypeSubType = parts[1]
134+
123135
if d.StreamSerializer != nil {
124136
info.StreamSerializer = &runtime.StreamSerializerInfo{
125137
Serializer: d.StreamSerializer,

staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,41 @@ func TestGet(t *testing.T) {
16161616
}
16171617
}
16181618

1619+
func BenchmarkGet(b *testing.B) {
1620+
storage := map[string]rest.Storage{}
1621+
simpleStorage := SimpleRESTStorage{
1622+
item: genericapitesting.Simple{
1623+
Other: "foo",
1624+
},
1625+
}
1626+
selfLinker := &setTestSelfLinker{
1627+
expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id",
1628+
name: "id",
1629+
namespace: "default",
1630+
}
1631+
storage["simple"] = &simpleStorage
1632+
handler := handleLinker(storage, selfLinker)
1633+
server := httptest.NewServer(handler)
1634+
defer server.Close()
1635+
1636+
u := server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id"
1637+
1638+
b.ResetTimer()
1639+
for i := 0; i < b.N; i++ {
1640+
resp, err := http.Get(u)
1641+
if err != nil {
1642+
b.Fatalf("unexpected error: %v", err)
1643+
}
1644+
if resp.StatusCode != http.StatusOK {
1645+
b.Fatalf("unexpected response: %#v", resp)
1646+
}
1647+
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
1648+
b.Fatalf("unable to read body")
1649+
}
1650+
}
1651+
b.StopTimer()
1652+
}
1653+
16191654
func TestGetCompression(t *testing.T) {
16201655
storage := map[string]rest.Storage{}
16211656
simpleStorage := SimpleRESTStorage{

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go

Lines changed: 22 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, strea
4343
// NegotiateOutputMediaType negotiates the output structured media type and a serializer, or
4444
// returns an error.
4545
func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) {
46-
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions)
46+
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), ns.SupportedMediaTypes(), restrictions)
4747
if !ok {
4848
supported, _ := MediaTypesForSerializer(ns)
4949
return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported)
5050
}
5151
// TODO: move into resthandler
52-
info := mediaType.Accepted.Serializer
52+
info := mediaType.Accepted
5353
if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil {
5454
info.Serializer = info.PrettySerializer
5555
}
@@ -58,12 +58,12 @@ func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer
5858

5959
// NegotiateOutputMediaTypeStream returns a stream serializer for the given request.
6060
func NegotiateOutputMediaTypeStream(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (runtime.SerializerInfo, error) {
61-
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions)
62-
if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil {
61+
mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), ns.SupportedMediaTypes(), restrictions)
62+
if !ok || mediaType.Accepted.StreamSerializer == nil {
6363
_, supported := MediaTypesForSerializer(ns)
6464
return runtime.SerializerInfo{}, NewNotAcceptableError(supported)
6565
}
66-
return mediaType.Accepted.Serializer, nil
66+
return mediaType.Accepted, nil
6767
}
6868

6969
// NegotiateInputSerializer returns the input serializer for the provided request.
@@ -99,10 +99,13 @@ func NegotiateInputSerializerForMediaType(mediaType string, streaming bool, ns r
9999
func isPrettyPrint(req *http.Request) bool {
100100
// DEPRECATED: should be part of the content type
101101
if req.URL != nil {
102-
pp := req.URL.Query().Get("pretty")
103-
if len(pp) > 0 {
104-
pretty, _ := strconv.ParseBool(pp)
105-
return pretty
102+
// avoid an allocation caused by parsing the URL query
103+
if strings.Contains(req.URL.RawQuery, "pretty") {
104+
pp := req.URL.Query().Get("pretty")
105+
if len(pp) > 0 {
106+
pretty, _ := strconv.ParseBool(pp)
107+
return pretty
108+
}
106109
}
107110
}
108111
userAgent := req.UserAgent()
@@ -139,17 +142,6 @@ func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind, strin
139142
func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false }
140143
func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
141144

142-
// AcceptedMediaType contains information about a valid media type that the
143-
// server can serialize.
144-
type AcceptedMediaType struct {
145-
// Type is the first part of the media type ("application")
146-
Type string
147-
// SubType is the second part of the media type ("json")
148-
SubType string
149-
// Serializer is the serialization info this object accepts
150-
Serializer runtime.SerializerInfo
151-
}
152-
153145
// MediaTypeOptions describes information for a given media type that may alter
154146
// the server response
155147
type MediaTypeOptions struct {
@@ -176,13 +168,13 @@ type MediaTypeOptions struct {
176168
Unrecognized []string
177169

178170
// the accepted media type from the client
179-
Accepted *AcceptedMediaType
171+
Accepted runtime.SerializerInfo
180172
}
181173

182174
// acceptMediaTypeOptions returns an options object that matches the provided media type params. If
183175
// it returns false, the provided options are not allowed and the media type must be skipped. These
184176
// parameters are unversioned and may not be changed.
185-
func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
177+
func acceptMediaTypeOptions(params map[string]string, accepts *runtime.SerializerInfo, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
186178
var options MediaTypeOptions
187179

188180
// extract all known parameters
@@ -208,7 +200,7 @@ func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType
208200

209201
// controls the streaming schema
210202
case "stream":
211-
if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) {
203+
if len(v) > 0 && (accepts.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) {
212204
return MediaTypeOptions{}, false
213205
}
214206
options.Stream = v
@@ -236,27 +228,27 @@ func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType
236228
}
237229
}
238230

239-
if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert, accepts.Type, accepts.SubType) {
231+
if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert, accepts.MediaTypeType, accepts.MediaTypeSubType) {
240232
return MediaTypeOptions{}, false
241233
}
242234

243-
options.Accepted = accepts
235+
options.Accepted = *accepts
244236
return options, true
245237
}
246238

247239
type candidateMediaType struct {
248-
accepted *AcceptedMediaType
240+
accepted *runtime.SerializerInfo
249241
clauses goautoneg.Accept
250242
}
251243

252244
type candidateMediaTypeSlice []candidateMediaType
253245

254246
// NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and
255247
// a list of alternatives along with the accepted media type parameters.
256-
func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
248+
func NegotiateMediaTypeOptions(header string, accepted []runtime.SerializerInfo, endpoint EndpointRestrictions) (MediaTypeOptions, bool) {
257249
if len(header) == 0 && len(accepted) > 0 {
258250
return MediaTypeOptions{
259-
Accepted: &accepted[0],
251+
Accepted: accepted[0],
260252
}, true
261253
}
262254

@@ -266,8 +258,8 @@ func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endp
266258
for i := range accepted {
267259
accepts := &accepted[i]
268260
switch {
269-
case clause.Type == accepts.Type && clause.SubType == accepts.SubType,
270-
clause.Type == accepts.Type && clause.SubType == "*",
261+
case clause.Type == accepts.MediaTypeType && clause.SubType == accepts.MediaTypeSubType,
262+
clause.Type == accepts.MediaTypeType && clause.SubType == "*",
271263
clause.Type == "*" && clause.SubType == "*":
272264
candidates = append(candidates, candidateMediaType{accepted: accepts, clauses: clause})
273265
}
@@ -282,22 +274,3 @@ func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endp
282274

283275
return MediaTypeOptions{}, false
284276
}
285-
286-
// AcceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which
287-
// allowed media types the server exposes.
288-
func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType {
289-
var acceptedMediaTypes []AcceptedMediaType
290-
for _, info := range ns.SupportedMediaTypes() {
291-
segments := strings.SplitN(info.MediaType, "/", 2)
292-
if len(segments) == 1 {
293-
segments = append(segments, "*")
294-
}
295-
t := AcceptedMediaType{
296-
Type: segments[0],
297-
SubType: segments[1],
298-
Serializer: info,
299-
}
300-
acceptedMediaTypes = append(acceptedMediaTypes, t)
301-
}
302-
return acceptedMediaTypes
303-
}

0 commit comments

Comments
 (0)