Improve fake clientset performance

The fake clientset used a slice to store each kind of objects, it's
quite slow to init the clientset with massive objects because it checked
existence of an object by traversing all objects before adding it, which
leads to O(n^2) time complexity. Also, the Create, Update, Get, Delete
methods needs to traverse all objects, which affects the time statistic
of code that calls them.

This patch changed to use a map to store each kind of objects, reduced
the time complexity of initializing clientset to O(n) and the Create,
Update, Get, Delete to O(1).

For example:
Before this patch, it took ~29s to init a clientset with 30000 Pods,
and 2~4ms to create and get an Pod.
After this patch, it took ~50ms to init a clientset with 30000 Pods,
and tens of µs to create and get an Pod.
This commit is contained in:
Quan Tian 2020-03-27 18:39:20 +08:00
parent 119660098b
commit 7e15e31e11
3 changed files with 44 additions and 57 deletions

View File

@ -75,9 +75,9 @@ func TestList(t *testing.T) {
} }
expected := []unstructured.Unstructured{ expected := []unstructured.Unstructured{
*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), *newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), *newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
} }
if !equality.Semantic.DeepEqual(listFirst.Items, expected) { if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items)) t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))

View File

@ -79,9 +79,9 @@ func TestList(t *testing.T) {
} }
expected := []metav1.PartialObjectMetadata{ expected := []metav1.PartialObjectMetadata{
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"), *newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"),
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"), *newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"),
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
} }
if !equality.Semantic.DeepEqual(listFirst.Items, expected) { if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items)) t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))

View File

@ -19,6 +19,7 @@ package testing
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"sort"
"sync" "sync"
jsonpatch "github.com/evanphx/json-patch" jsonpatch "github.com/evanphx/json-patch"
@ -197,7 +198,7 @@ type tracker struct {
scheme ObjectScheme scheme ObjectScheme
decoder runtime.Decoder decoder runtime.Decoder
lock sync.RWMutex lock sync.RWMutex
objects map[schema.GroupVersionResource][]runtime.Object objects map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object
// The value type of watchers is a map of which the key is either a namespace or // The value type of watchers is a map of which the key is either a namespace or
// all/non namespace aka "" and its value is list of fake watchers. // all/non namespace aka "" and its value is list of fake watchers.
// Manipulations on resources will broadcast the notification events into the // Manipulations on resources will broadcast the notification events into the
@ -214,7 +215,7 @@ func NewObjectTracker(scheme ObjectScheme, decoder runtime.Decoder) ObjectTracke
return &tracker{ return &tracker{
scheme: scheme, scheme: scheme,
decoder: decoder, decoder: decoder,
objects: make(map[schema.GroupVersionResource][]runtime.Object), objects: make(map[schema.GroupVersionResource]map[types.NamespacedName]runtime.Object),
watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher), watchers: make(map[schema.GroupVersionResource]map[string][]*watch.RaceFreeFakeWatcher),
} }
} }
@ -282,31 +283,15 @@ func (t *tracker) Get(gvr schema.GroupVersionResource, ns, name string) (runtime
return nil, errNotFound return nil, errNotFound
} }
var matchingObjs []runtime.Object matchingObj, ok := objs[types.NamespacedName{Namespace: ns, Name: name}]
for _, obj := range objs { if !ok {
acc, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
if acc.GetNamespace() != ns {
continue
}
if acc.GetName() != name {
continue
}
matchingObjs = append(matchingObjs, obj)
}
if len(matchingObjs) == 0 {
return nil, errNotFound return nil, errNotFound
} }
if len(matchingObjs) > 1 {
return nil, fmt.Errorf("more than one object matched gvr %s, ns: %q name: %q", gvr, ns, name)
}
// Only one object should match in the tracker if it works // Only one object should match in the tracker if it works
// correctly, as Add/Update methods enforce kind/namespace/name // correctly, as Add/Update methods enforce kind/namespace/name
// uniqueness. // uniqueness.
obj := matchingObjs[0].DeepCopyObject() obj := matchingObj.DeepCopyObject()
if status, ok := obj.(*metav1.Status); ok { if status, ok := obj.(*metav1.Status); ok {
if status.Status != metav1.StatusSuccess { if status.Status != metav1.StatusSuccess {
return nil, &errors.StatusError{ErrStatus: *status} return nil, &errors.StatusError{ErrStatus: *status}
@ -405,29 +390,29 @@ func (t *tracker) add(gvr schema.GroupVersionResource, obj runtime.Object, ns st
return errors.NewBadRequest(msg) return errors.NewBadRequest(msg)
} }
for i, existingObj := range t.objects[gvr] { _, ok := t.objects[gvr]
oldMeta, err := meta.Accessor(existingObj) if !ok {
if err != nil { t.objects[gvr] = make(map[types.NamespacedName]runtime.Object)
return err
} }
if oldMeta.GetNamespace() == newMeta.GetNamespace() && oldMeta.GetName() == newMeta.GetName() {
namespacedName := types.NamespacedName{Namespace: newMeta.GetNamespace(), Name: newMeta.GetName()}
if _, ok = t.objects[gvr][namespacedName]; ok {
if replaceExisting { if replaceExisting {
for _, w := range t.getWatches(gvr, ns) { for _, w := range t.getWatches(gvr, ns) {
w.Modify(obj) w.Modify(obj)
} }
t.objects[gvr][i] = obj t.objects[gvr][namespacedName] = obj
return nil return nil
} }
return errors.NewAlreadyExists(gr, newMeta.GetName()) return errors.NewAlreadyExists(gr, newMeta.GetName())
} }
}
if replaceExisting { if replaceExisting {
// Tried to update but no matching object was found. // Tried to update but no matching object was found.
return errors.NewNotFound(gr, newMeta.GetName()) return errors.NewNotFound(gr, newMeta.GetName())
} }
t.objects[gvr] = append(t.objects[gvr], obj) t.objects[gvr][namespacedName] = obj
for _, w := range t.getWatches(gvr, ns) { for _, w := range t.getWatches(gvr, ns) {
w.Add(obj) w.Add(obj)
@ -457,35 +442,28 @@ func (t *tracker) Delete(gvr schema.GroupVersionResource, ns, name string) error
t.lock.Lock() t.lock.Lock()
defer t.lock.Unlock() defer t.lock.Unlock()
found := false objs, ok := t.objects[gvr]
if !ok {
for i, existingObj := range t.objects[gvr] { return errors.NewNotFound(gvr.GroupResource(), name)
objMeta, err := meta.Accessor(existingObj)
if err != nil {
return err
} }
if objMeta.GetNamespace() == ns && objMeta.GetName() == name {
obj := t.objects[gvr][i] namespacedName := types.NamespacedName{Namespace: ns, Name: name}
t.objects[gvr] = append(t.objects[gvr][:i], t.objects[gvr][i+1:]...) obj, ok := objs[namespacedName]
if !ok {
return errors.NewNotFound(gvr.GroupResource(), name)
}
delete(objs, namespacedName)
for _, w := range t.getWatches(gvr, ns) { for _, w := range t.getWatches(gvr, ns) {
w.Delete(obj) w.Delete(obj)
} }
found = true
break
}
}
if found {
return nil return nil
} }
return errors.NewNotFound(gvr.GroupResource(), name)
}
// filterByNamespace returns all objects in the collection that // filterByNamespace returns all objects in the collection that
// match provided namespace. Empty namespace matches // match provided namespace. Empty namespace matches
// non-namespaced objects. // non-namespaced objects.
func filterByNamespace(objs []runtime.Object, ns string) ([]runtime.Object, error) { func filterByNamespace(objs map[types.NamespacedName]runtime.Object, ns string) ([]runtime.Object, error) {
var res []runtime.Object var res []runtime.Object
for _, obj := range objs { for _, obj := range objs {
@ -499,6 +477,15 @@ func filterByNamespace(objs []runtime.Object, ns string) ([]runtime.Object, erro
res = append(res, obj) res = append(res, obj)
} }
// Sort res to get deterministic order.
sort.Slice(res, func(i, j int) bool {
acc1, _ := meta.Accessor(res[i])
acc2, _ := meta.Accessor(res[j])
if acc1.GetNamespace() != acc2.GetNamespace() {
return acc1.GetNamespace() < acc2.GetNamespace()
}
return acc1.GetName() < acc2.GetName()
})
return res, nil return res, nil
} }