Skip to content

Commit a79f6f1

Browse files
authored
⚠️ Change client.Patch to take client.Object for performance (kubernetes-sigs#1395)
* ✨ Add a fast path for client.Object in merge-patch Most uses of merge-patch will be for built-in types or kubebuilder- generated custom resources which have accessors for ResourceVersion. Using these with DeepCopyObject() is simple and fast. name old time/op new time/op delta MergeFrom/NoOptions 91.6µs ± 4% 107.3µs ±26% ~ (p=0.417 n=7+10) MergeFrom/NoOptions-2 112µs ±18% 88µs ±11% -21.15% (p=0.000 n=10+9) MergeFrom/WithOptimisticLock 163µs ± 3% 121µs ± 3% -25.66% (p=0.000 n=10+8) MergeFrom/WithOptimisticLock-2 137µs ± 4% 101µs ± 4% -26.28% (p=0.000 n=10+10) name old alloc/op new alloc/op delta MergeFrom/NoOptions 20.3kB ± 0% 20.3kB ± 0% ~ (all equal) MergeFrom/NoOptions-2 20.3kB ± 0% 20.3kB ± 0% ~ (all equal) MergeFrom/WithOptimisticLock 34.1kB ± 0% 26.7kB ± 0% -21.89% (p=0.000 n=10+10) MergeFrom/WithOptimisticLock-2 34.2kB ± 0% 26.7kB ± 0% -21.89% (p=0.000 n=10+8) name old allocs/op new allocs/op delta MergeFrom/NoOptions 359 ± 0% 359 ± 0% ~ (all equal) MergeFrom/NoOptions-2 359 ± 0% 359 ± 0% ~ (all equal) MergeFrom/WithOptimisticLock 579 ± 0% 390 ± 0% -32.64% (p=0.000 n=10+10) MergeFrom/WithOptimisticLock-2 579 ± 0% 390 ± 0% -32.64% (p=0.000 n=10+10) * ⚠ Change client.Patch to take client.Object This allows us to use metav1 accessors to speed up the merge-patch implementation.
1 parent eaf06ce commit a79f6f1

File tree

4 files changed

+121
-33
lines changed

4 files changed

+121
-33
lines changed

pkg/client/interfaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type Patch interface {
3939
// Type is the PatchType of the patch.
4040
Type() types.PatchType
4141
// Data is the raw data representing the patch.
42-
Data(obj runtime.Object) ([]byte, error)
42+
Data(obj Object) ([]byte, error)
4343
}
4444

4545
// TODO(directxman12): is there a sane way to deal with get/delete options?

pkg/client/patch.go

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"fmt"
2121

2222
jsonpatch "github.com/evanphx/json-patch"
23-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2523
"k8s.io/apimachinery/pkg/runtime"
2624
"k8s.io/apimachinery/pkg/types"
2725
"k8s.io/apimachinery/pkg/util/json"
@@ -47,7 +45,7 @@ func (s *patch) Type() types.PatchType {
4745
}
4846

4947
// Data implements Patch.
50-
func (s *patch) Data(obj runtime.Object) ([]byte, error) {
48+
func (s *patch) Data(obj Object) ([]byte, error) {
5149
return s.data, nil
5250
}
5351

@@ -87,7 +85,7 @@ type MergeFromOptions struct {
8785
}
8886

8987
type mergeFromPatch struct {
90-
from runtime.Object
88+
from Object
9189
opts MergeFromOptions
9290
}
9391

@@ -97,13 +95,29 @@ func (s *mergeFromPatch) Type() types.PatchType {
9795
}
9896

9997
// Data implements Patch.
100-
func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
101-
originalJSON, err := json.Marshal(s.from)
98+
func (s *mergeFromPatch) Data(obj Object) ([]byte, error) {
99+
original := s.from
100+
modified := obj
101+
102+
if s.opts.OptimisticLock {
103+
version := original.GetResourceVersion()
104+
if len(version) == 0 {
105+
return nil, fmt.Errorf("cannot use OptimisticLock, object %q does not have any resource version we can use", original)
106+
}
107+
108+
original = original.DeepCopyObject().(Object)
109+
original.SetResourceVersion("")
110+
111+
modified = modified.DeepCopyObject().(Object)
112+
modified.SetResourceVersion(version)
113+
}
114+
115+
originalJSON, err := json.Marshal(original)
102116
if err != nil {
103117
return nil, err
104118
}
105119

106-
modifiedJSON, err := json.Marshal(obj)
120+
modifiedJSON, err := json.Marshal(modified)
107121
if err != nil {
108122
return nil, err
109123
}
@@ -113,37 +127,16 @@ func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
113127
return nil, err
114128
}
115129

116-
if s.opts.OptimisticLock {
117-
dataMap := map[string]interface{}{}
118-
if err := json.Unmarshal(data, &dataMap); err != nil {
119-
return nil, err
120-
}
121-
fromMeta, ok := s.from.(metav1.Object)
122-
if !ok {
123-
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q is not a valid metav1.Object", s.from)
124-
}
125-
resourceVersion := fromMeta.GetResourceVersion()
126-
if len(resourceVersion) == 0 {
127-
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q does not have any resource version we can use", s.from)
128-
}
129-
u := &unstructured.Unstructured{Object: dataMap}
130-
u.SetResourceVersion(resourceVersion)
131-
data, err = json.Marshal(u)
132-
if err != nil {
133-
return nil, err
134-
}
135-
}
136-
137130
return data, nil
138131
}
139132

140133
// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
141-
func MergeFrom(obj runtime.Object) Patch {
134+
func MergeFrom(obj Object) Patch {
142135
return &mergeFromPatch{from: obj}
143136
}
144137

145138
// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
146-
func MergeFromWithOptions(obj runtime.Object, opts ...MergeFromOption) Patch {
139+
func MergeFromWithOptions(obj Object, opts ...MergeFromOption) Patch {
147140
options := &MergeFromOptions{}
148141
for _, opt := range opts {
149142
opt.ApplyToMergeFrom(options)

pkg/client/patch_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright 2021 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 client
18+
19+
import (
20+
"testing"
21+
22+
appsv1 "k8s.io/api/apps/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/resource"
25+
)
26+
27+
func BenchmarkMergeFrom(b *testing.B) {
28+
cm1 := &corev1.ConfigMap{}
29+
cm1.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
30+
cm1.ResourceVersion = "anything"
31+
32+
cm2 := cm1.DeepCopy()
33+
cm2.Data = map[string]string{"key": "value"}
34+
35+
sts1 := &appsv1.StatefulSet{}
36+
sts1.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("StatefulSet"))
37+
sts1.ResourceVersion = "somesuch"
38+
39+
sts2 := sts1.DeepCopy()
40+
sts2.Spec.Template.Spec.Containers = []corev1.Container{{
41+
Resources: corev1.ResourceRequirements{
42+
Requests: map[corev1.ResourceName]resource.Quantity{
43+
corev1.ResourceCPU: resource.MustParse("1m"),
44+
corev1.ResourceMemory: resource.MustParse("1M"),
45+
},
46+
},
47+
ReadinessProbe: &corev1.Probe{
48+
Handler: corev1.Handler{
49+
HTTPGet: &corev1.HTTPGetAction{},
50+
},
51+
},
52+
Lifecycle: &corev1.Lifecycle{
53+
PreStop: &corev1.Handler{
54+
HTTPGet: &corev1.HTTPGetAction{},
55+
},
56+
},
57+
SecurityContext: &corev1.SecurityContext{},
58+
}}
59+
60+
b.Run("NoOptions", func(b *testing.B) {
61+
cmPatch := MergeFrom(cm1)
62+
if _, err := cmPatch.Data(cm2); err != nil {
63+
b.Fatalf("expected no error, got %v", err)
64+
}
65+
66+
stsPatch := MergeFrom(sts1)
67+
if _, err := stsPatch.Data(sts2); err != nil {
68+
b.Fatalf("expected no error, got %v", err)
69+
}
70+
71+
b.ResetTimer()
72+
for i := 0; i < b.N; i++ {
73+
_, _ = cmPatch.Data(cm2)
74+
_, _ = stsPatch.Data(sts2)
75+
}
76+
})
77+
78+
b.Run("WithOptimisticLock", func(b *testing.B) {
79+
cmPatch := MergeFromWithOptions(cm1, MergeFromWithOptimisticLock{})
80+
if _, err := cmPatch.Data(cm2); err != nil {
81+
b.Fatalf("expected no error, got %v", err)
82+
}
83+
84+
stsPatch := MergeFromWithOptions(sts1, MergeFromWithOptimisticLock{})
85+
if _, err := stsPatch.Data(sts2); err != nil {
86+
b.Fatalf("expected no error, got %v", err)
87+
}
88+
89+
b.ResetTimer()
90+
for i := 0; i < b.N; i++ {
91+
_, _ = cmPatch.Data(cm2)
92+
_, _ = stsPatch.Data(sts2)
93+
}
94+
})
95+
}

pkg/controller/controllerutil/controllerutil.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ func CreateOrPatch(ctx context.Context, c client.Client, obj client.Object, f Mu
249249
}
250250

251251
// Create patches for the object and its possible status.
252-
objPatch := client.MergeFrom(obj.DeepCopyObject())
253-
statusPatch := client.MergeFrom(obj.DeepCopyObject())
252+
objPatch := client.MergeFrom(obj.DeepCopyObject().(client.Object))
253+
statusPatch := client.MergeFrom(obj.DeepCopyObject().(client.Object))
254254

255255
// Create a copy of the original object as well as converting that copy to
256256
// unstructured data.

0 commit comments

Comments
 (0)