Merge pull request #36207 from smarterclayton/optimize_self_link

Automatic merge from submit-queue

SetSelfLink is inefficient

Generating self links, especially for lists, is inefficient.  Replace
use of net.URL.String() call with direct encoding that reduces number of
allocations. Switch from calling meta.ExtractList|SetList to a function
that iterates over each object in the list.

In steady state for nodes performing frequently small get/list
operations, and for larger LISTs significantly reduces CPU and
allocations.

@wojtek-t this is the next big chunk of CPU use during the large N nodes simulation test (11% of master CPU). Takes a few allocations out of the critical path
This commit is contained in:
Kubernetes Submit Queue 2016-11-06 06:42:49 -08:00 committed by GitHub
commit ebc8dc85aa
6 changed files with 279 additions and 159 deletions

View File

@ -66,6 +66,57 @@ func GetItemsPtr(list runtime.Object) (interface{}, error) {
} }
} }
// EachListItem invokes fn on each runtime.Object in the list. Any error immediately terminates
// the loop.
func EachListItem(obj runtime.Object, fn func(runtime.Object) error) error {
// TODO: Change to an interface call?
itemsPtr, err := GetItemsPtr(obj)
if err != nil {
return err
}
items, err := conversion.EnforcePtr(itemsPtr)
if err != nil {
return err
}
len := items.Len()
if len == 0 {
return nil
}
takeAddr := false
if elemType := items.Type().Elem(); elemType.Kind() != reflect.Ptr && elemType.Kind() != reflect.Interface {
if !items.Index(0).CanAddr() {
return fmt.Errorf("unable to take address of items in %T for EachListItem", obj)
}
takeAddr = true
}
for i := 0; i < len; i++ {
raw := items.Index(i)
if takeAddr {
raw = raw.Addr()
}
switch item := raw.Interface().(type) {
case *runtime.RawExtension:
if err := fn(item.Object); err != nil {
return err
}
case runtime.Object:
if err := fn(item); err != nil {
return err
}
default:
obj, ok := item.(runtime.Object)
if !ok {
return fmt.Errorf("%v: item[%v]: Expected object, got %#v(%s)", obj, i, raw.Interface(), raw.Kind())
}
if err := fn(obj); err != nil {
return err
}
}
}
return nil
}
// ExtractList returns obj's Items element as an array of runtime.Objects. // ExtractList returns obj's Items element as an array of runtime.Objects.
// Returns an error if obj is not a List type (does not have an Items member). // Returns an error if obj is not a List type (does not have an Items member).
func ExtractList(obj runtime.Object) ([]runtime.Object, error) { func ExtractList(obj runtime.Object) ([]runtime.Object, error) {

View File

@ -46,94 +46,185 @@ func TestIsList(t *testing.T) {
} }
func TestExtractList(t *testing.T) { func TestExtractList(t *testing.T) {
pl := &api.PodList{ list1 := []runtime.Object{
Items: []api.Pod{ &api.Pod{ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "1"}}, &api.Service{ObjectMeta: api.ObjectMeta{Name: "2"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
{ObjectMeta: api.ObjectMeta{Name: "3"}},
},
} }
list, err := meta.ExtractList(pl) list2 := &v1.List{
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := len(list), len(pl.Items); e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
for i := range list {
if e, a := list[i].(*api.Pod).Name, pl.Items[i].Name; e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
}
}
func TestExtractListV1(t *testing.T) {
pl := &v1.PodList{
Items: []v1.Pod{
{ObjectMeta: v1.ObjectMeta{Name: "1"}},
{ObjectMeta: v1.ObjectMeta{Name: "2"}},
{ObjectMeta: v1.ObjectMeta{Name: "3"}},
},
}
list, err := meta.ExtractList(pl)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := len(list), len(pl.Items); e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
for i := range list {
if e, a := list[i].(*v1.Pod).Name, pl.Items[i].Name; e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
}
}
func TestExtractListGeneric(t *testing.T) {
pl := &api.List{
Items: []runtime.Object{
&api.Pod{ObjectMeta: api.ObjectMeta{Name: "1"}},
&api.Service{ObjectMeta: api.ObjectMeta{Name: "2"}},
},
}
list, err := meta.ExtractList(pl)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := len(list), len(pl.Items); e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
if obj, ok := list[0].(*api.Pod); !ok {
t.Fatalf("Expected list[0] to be *api.Pod, it is %#v", obj)
}
if obj, ok := list[1].(*api.Service); !ok {
t.Fatalf("Expected list[1] to be *api.Service, it is %#v", obj)
}
}
func TestExtractListGenericV1(t *testing.T) {
pl := &v1.List{
Items: []runtime.RawExtension{ Items: []runtime.RawExtension{
{Raw: []byte("foo")}, {Raw: []byte("foo")},
{Raw: []byte("bar")}, {Raw: []byte("bar")},
{Object: &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "other"}}}, {Object: &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "other"}}},
}, },
} }
list, err := meta.ExtractList(pl) list3 := &fakePtrValueList{
if err != nil { Items: []*api.Pod{
t.Fatalf("Unexpected error %v", err) {ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
},
} }
if e, a := len(list), len(pl.Items); e != a { list4 := &api.PodList{
t.Fatalf("Expected %v, got %v", e, a) Items: []api.Pod{
{ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
{ObjectMeta: api.ObjectMeta{Name: "3"}},
},
} }
if obj, ok := list[0].(*runtime.Unknown); !ok { list5 := &v1.PodList{
t.Fatalf("Expected list[0] to be *runtime.Unknown, it is %#v", obj) Items: []v1.Pod{
{ObjectMeta: v1.ObjectMeta{Name: "1"}},
{ObjectMeta: v1.ObjectMeta{Name: "2"}},
{ObjectMeta: v1.ObjectMeta{Name: "3"}},
},
} }
if obj, ok := list[1].(*runtime.Unknown); !ok {
t.Fatalf("Expected list[1] to be *runtime.Unknown, it is %#v", obj) testCases := []struct {
in runtime.Object
out []interface{}
equal bool
}{
{
in: &api.List{},
out: []interface{}{},
},
{
in: &v1.List{},
out: []interface{}{},
},
{
in: &v1.PodList{},
out: []interface{}{},
},
{
in: &api.List{Items: list1},
out: []interface{}{list1[0], list1[1]},
},
{
in: list2,
out: []interface{}{&runtime.Unknown{Raw: list2.Items[0].Raw}, &runtime.Unknown{Raw: list2.Items[1].Raw}, list2.Items[2].Object},
equal: true,
},
{
in: list3,
out: []interface{}{list3.Items[0], list3.Items[1]},
},
{
in: list4,
out: []interface{}{&list4.Items[0], &list4.Items[1], &list4.Items[2]},
},
{
in: list5,
out: []interface{}{&list5.Items[0], &list5.Items[1], &list5.Items[2]},
},
} }
if obj, ok := list[2].(*v1.Pod); !ok { for i, test := range testCases {
t.Fatalf("Expected list[2] to be *runtime.Unknown, it is %#v", obj) list, err := meta.ExtractList(test.in)
if err != nil {
t.Fatalf("%d: extract: Unexpected error %v", i, err)
}
if e, a := len(test.out), len(list); e != a {
t.Fatalf("%d: extract: Expected %v, got %v", i, e, a)
}
for j, e := range test.out {
if e != list[j] {
if !test.equal {
t.Fatalf("%d: extract: Expected list[%d] to be %#v, but found %#v", i, j, e, list[j])
}
if !reflect.DeepEqual(e, list[j]) {
t.Fatalf("%d: extract: Expected list[%d] to be %#v, but found %#v", i, j, e, list[j])
}
}
}
}
}
func TestEachListItem(t *testing.T) {
list1 := []runtime.Object{
&api.Pod{ObjectMeta: api.ObjectMeta{Name: "1"}},
&api.Service{ObjectMeta: api.ObjectMeta{Name: "2"}},
}
list2 := &v1.List{
Items: []runtime.RawExtension{
{Raw: []byte("foo")},
{Raw: []byte("bar")},
{Object: &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "other"}}},
},
}
list3 := &fakePtrValueList{
Items: []*api.Pod{
{ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
},
}
list4 := &api.PodList{
Items: []api.Pod{
{ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
{ObjectMeta: api.ObjectMeta{Name: "3"}},
},
}
list5 := &v1.PodList{
Items: []v1.Pod{
{ObjectMeta: v1.ObjectMeta{Name: "1"}},
{ObjectMeta: v1.ObjectMeta{Name: "2"}},
{ObjectMeta: v1.ObjectMeta{Name: "3"}},
},
}
testCases := []struct {
in runtime.Object
out []interface{}
}{
{
in: &api.List{},
out: []interface{}{},
},
{
in: &v1.List{},
out: []interface{}{},
},
{
in: &v1.PodList{},
out: []interface{}{},
},
{
in: &api.List{Items: list1},
out: []interface{}{list1[0], list1[1]},
},
{
in: list2,
out: []interface{}{nil, nil, list2.Items[2].Object},
},
{
in: list3,
out: []interface{}{list3.Items[0], list3.Items[1]},
},
{
in: list4,
out: []interface{}{&list4.Items[0], &list4.Items[1], &list4.Items[2]},
},
{
in: list5,
out: []interface{}{&list5.Items[0], &list5.Items[1], &list5.Items[2]},
},
}
for i, test := range testCases {
list := []runtime.Object{}
err := meta.EachListItem(test.in, func(obj runtime.Object) error {
list = append(list, obj)
return nil
})
if err != nil {
t.Fatalf("%d: each: Unexpected error %v", i, err)
}
if e, a := len(test.out), len(list); e != a {
t.Fatalf("%d: each: Expected %v, got %v", i, e, a)
}
for j, e := range test.out {
if e != list[j] {
t.Fatalf("%d: each: Expected list[%d] to be %#v, but found %#v", i, j, e, list[j])
}
}
} }
} }
@ -166,27 +257,6 @@ func (obj fakePtrValueList) GetObjectKind() unversioned.ObjectKind {
return unversioned.EmptyObjectKind return unversioned.EmptyObjectKind
} }
func TestExtractListOfValuePtrs(t *testing.T) {
pl := &fakePtrValueList{
Items: []*api.Pod{
{ObjectMeta: api.ObjectMeta{Name: "1"}},
{ObjectMeta: api.ObjectMeta{Name: "2"}},
},
}
list, err := meta.ExtractList(pl)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := len(list), len(pl.Items); e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
for i := range list {
if obj, ok := list[i].(*api.Pod); !ok {
t.Fatalf("Expected list[%d] to be *api.Pod, it is %#v", i, obj)
}
}
}
func TestSetList(t *testing.T) { func TestSetList(t *testing.T) {
pl := &api.PodList{} pl := &api.PodList{}
list := []runtime.Object{ list := []runtime.Object{

View File

@ -17,8 +17,10 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
gpath "path" gpath "path"
"reflect" "reflect"
"sort" "sort"
@ -357,15 +359,17 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
itemPath := resourcePath + "/{name}" itemPath := resourcePath + "/{name}"
nameParams := append(params, nameParam) nameParams := append(params, nameParam)
proxyParams := append(nameParams, pathParam) proxyParams := append(nameParams, pathParam)
suffix := ""
if hasSubresource { if hasSubresource {
itemPath = itemPath + "/" + subresource suffix = "/" + subresource
itemPath = itemPath + suffix
resourcePath = itemPath resourcePath = itemPath
resourceParams = nameParams resourceParams = nameParams
} }
apiResource.Name = path apiResource.Name = path
apiResource.Namespaced = false apiResource.Namespaced = false
apiResource.Kind = resourceKind apiResource.Kind = resourceKind
namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, resourcePath, "/"), suffix}
// Handler for standard REST verbs (GET, PUT, POST and DELETE). // Handler for standard REST verbs (GET, PUT, POST and DELETE).
// Add actions at the resource path: /api/apiVersion/resource // Add actions at the resource path: /api/apiVersion/resource
@ -417,8 +421,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
apiResource.Namespaced = true apiResource.Namespaced = true
apiResource.Kind = resourceKind apiResource.Kind = resourceKind
itemPathFn := func(name, namespace string) string { itemPathFn := func(name, namespace string) bytes.Buffer {
return itemPathPrefix + namespace + itemPathMiddle + name + itemPathSuffix var buf bytes.Buffer
buf.WriteString(itemPathPrefix)
buf.WriteString(url.QueryEscape(namespace))
buf.WriteString(itemPathMiddle)
buf.WriteString(url.QueryEscape(name))
buf.WriteString(itemPathSuffix)
return buf
} }
namer := scopeNaming{scope, a.group.Linker, itemPathFn, false} namer := scopeNaming{scope, a.group.Linker, itemPathFn, false}
@ -751,7 +761,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
type rootScopeNaming struct { type rootScopeNaming struct {
scope meta.RESTScope scope meta.RESTScope
runtime.SelfLinker runtime.SelfLinker
itemPath string pathPrefix string
pathSuffix string
} }
// rootScopeNaming implements ScopeNamer // rootScopeNaming implements ScopeNamer
@ -773,25 +784,26 @@ func (n rootScopeNaming) Name(req *restful.Request) (namespace, name string, err
} }
// GenerateLink returns the appropriate path and query to locate an object by its canonical path. // GenerateLink returns the appropriate path and query to locate an object by its canonical path.
func (n rootScopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { func (n rootScopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error) {
_, name, err := n.ObjectName(obj) _, name, err := n.ObjectName(obj)
if err != nil { if err != nil {
return "", "", err return "", err
} }
if len(name) == 0 { if len(name) == 0 {
_, name, err = n.Name(req) _, name, err = n.Name(req)
if err != nil { if err != nil {
return "", "", err return "", err
} }
} }
path = strings.Replace(n.itemPath, "{name}", name, 1) return n.pathPrefix + url.QueryEscape(name) + n.pathSuffix, nil
return path, "", nil
} }
// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. // GenerateListLink returns the appropriate path and query to locate a list by its canonical path.
func (n rootScopeNaming) GenerateListLink(req *restful.Request) (path, query string, err error) { func (n rootScopeNaming) GenerateListLink(req *restful.Request) (uri string, err error) {
path = req.Request.URL.Path if len(req.Request.URL.RawPath) > 0 {
return path, "", nil return req.Request.URL.RawPath, nil
}
return req.Request.URL.EscapedPath(), nil
} }
// ObjectName returns the name set on the object, or an error if the // ObjectName returns the name set on the object, or an error if the
@ -813,7 +825,7 @@ func (n rootScopeNaming) ObjectName(obj runtime.Object) (namespace, name string,
type scopeNaming struct { type scopeNaming struct {
scope meta.RESTScope scope meta.RESTScope
runtime.SelfLinker runtime.SelfLinker
itemPathFn func(name, namespace string) string itemPathFn func(name, namespace string) bytes.Buffer
allNamespaces bool allNamespaces bool
} }
@ -846,28 +858,30 @@ func (n scopeNaming) Name(req *restful.Request) (namespace, name string, err err
} }
// GenerateLink returns the appropriate path and query to locate an object by its canonical path. // GenerateLink returns the appropriate path and query to locate an object by its canonical path.
func (n scopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { func (n scopeNaming) GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error) {
namespace, name, err := n.ObjectName(obj) namespace, name, err := n.ObjectName(obj)
if err != nil { if err != nil {
return "", "", err return "", err
} }
if len(namespace) == 0 && len(name) == 0 { if len(namespace) == 0 && len(name) == 0 {
namespace, name, err = n.Name(req) namespace, name, err = n.Name(req)
if err != nil { if err != nil {
return "", "", err return "", err
} }
} }
if len(name) == 0 { if len(name) == 0 {
return "", "", errEmptyName return "", errEmptyName
} }
result := n.itemPathFn(name, namespace)
return n.itemPathFn(name, namespace), "", nil return result.String(), nil
} }
// GenerateListLink returns the appropriate path and query to locate a list by its canonical path. // GenerateListLink returns the appropriate path and query to locate a list by its canonical path.
func (n scopeNaming) GenerateListLink(req *restful.Request) (path, query string, err error) { func (n scopeNaming) GenerateListLink(req *restful.Request) (uri string, err error) {
path = req.Request.URL.Path if len(req.Request.URL.RawPath) > 0 {
return path, "", nil return req.Request.URL.RawPath, nil
}
return req.Request.URL.EscapedPath(), nil
} }
// ObjectName returns the name and namespace set on the object, or an error if the // ObjectName returns the name and namespace set on the object, or an error if the

View File

@ -17,6 +17,7 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"bytes"
"testing" "testing"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
@ -36,8 +37,8 @@ func TestScopeNamingGenerateLink(t *testing.T) {
s := scopeNaming{ s := scopeNaming{
meta.RESTScopeNamespace, meta.RESTScopeNamespace,
selfLinker, selfLinker,
func(name, namespace string) string { func(name, namespace string) bytes.Buffer {
return "/api/v1/namespaces/" + namespace + "/services/" + name return *bytes.NewBufferString("/api/v1/namespaces/" + namespace + "/services/" + name)
}, },
true, true,
} }
@ -50,7 +51,7 @@ func TestScopeNamingGenerateLink(t *testing.T) {
Kind: "Service", Kind: "Service",
}, },
} }
_, _, err := s.GenerateLink(&restful.Request{}, service) _, err := s.GenerateLink(&restful.Request{}, service)
if err != nil { if err != nil {
t.Errorf("Unexpected error %v", err) t.Errorf("Unexpected error %v", err)
} }

View File

@ -60,10 +60,11 @@ type ScopeNamer interface {
// SetSelfLink sets the provided URL onto the object. The method should return nil if the object // SetSelfLink sets the provided URL onto the object. The method should return nil if the object
// does not support selfLinks. // does not support selfLinks.
SetSelfLink(obj runtime.Object, url string) error SetSelfLink(obj runtime.Object, url string) error
// GenerateLink creates a path and query for a given runtime object that represents the canonical path. // GenerateLink creates an encoded URI for a given runtime object that represents the canonical path
GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) // and query.
// GenerateLink creates a path and query for a list that represents the canonical path. GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error)
GenerateListLink(req *restful.Request) (path, query string, err error) // GenerateLink creates an encoded URI for a list that represents the canonical path and query.
GenerateListLink(req *restful.Request) (uri string, err error)
} }
// RequestScope encapsulates common fields across all RESTful handler methods. // RequestScope encapsulates common fields across all RESTful handler methods.
@ -997,18 +998,12 @@ func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime
// plus the path and query generated by the provided linkFunc // plus the path and query generated by the provided linkFunc
func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error {
// TODO: SelfLink generation should return a full URL? // TODO: SelfLink generation should return a full URL?
path, query, err := namer.GenerateLink(req, obj) uri, err := namer.GenerateLink(req, obj)
if err != nil { if err != nil {
return nil return nil
} }
newURL := *req.Request.URL return namer.SetSelfLink(obj, uri)
// use only canonical paths
newURL.Path = path
newURL.RawQuery = query
newURL.Fragment = ""
return namer.SetSelfLink(obj, newURL.String())
} }
func hasUID(obj runtime.Object) (bool, error) { func hasUID(obj runtime.Object) (bool, error) {
@ -1052,31 +1047,20 @@ func setListSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer)
return 0, nil return 0, nil
} }
// TODO: List SelfLink generation should return a full URL? uri, err := namer.GenerateListLink(req)
path, query, err := namer.GenerateListLink(req)
if err != nil { if err != nil {
return 0, err return 0, err
} }
newURL := *req.Request.URL if err := namer.SetSelfLink(obj, uri); err != nil {
newURL.Path = path
newURL.RawQuery = query
// use the path that got us here
newURL.Fragment = ""
if err := namer.SetSelfLink(obj, newURL.String()); err != nil {
glog.V(4).Infof("Unable to set self link on object: %v", err) glog.V(4).Infof("Unable to set self link on object: %v", err)
} }
// Set self-link of objects in the list. count := 0
items, err := meta.ExtractList(obj) err = meta.EachListItem(obj, func(obj runtime.Object) error {
if err != nil { count++
return 0, err return setSelfLink(obj, req, namer)
} })
for i := range items { return count, err
if err := setSelfLink(items[i], req, namer); err != nil {
return len(items), err
}
}
return len(items), meta.SetList(obj, items)
} }
func getPatchedJS(patchType api.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) { func getPatchedJS(patchType api.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) {

View File

@ -134,13 +134,13 @@ func (p *testNamer) SetSelfLink(obj runtime.Object, url string) error {
} }
// GenerateLink creates a path and query for a given runtime object that represents the canonical path. // GenerateLink creates a path and query for a given runtime object that represents the canonical path.
func (p *testNamer) GenerateLink(req *restful.Request, obj runtime.Object) (path, query string, err error) { func (p *testNamer) GenerateLink(req *restful.Request, obj runtime.Object) (uri string, err error) {
return "", "", errors.New("not implemented") return "", errors.New("not implemented")
} }
// GenerateLink creates a path and query for a list that represents the canonical path. // GenerateLink creates a path and query for a list that represents the canonical path.
func (p *testNamer) GenerateListLink(req *restful.Request) (path, query string, err error) { func (p *testNamer) GenerateListLink(req *restful.Request) (uri string, err error) {
return "", "", errors.New("not implemented") return "", errors.New("not implemented")
} }
type patchTestCase struct { type patchTestCase struct {