Skip to content

Commit 36e3e53

Browse files
perf(source): benchmarks on EndpointTargetsFromServices (kubernetes-sigs#5536)
* chore(benchmarking): added benchmarks to EndpointTargetsFromServices Signed-off-by: ivan katliarchuk <[email protected]> * chore(benchmarking): added benchmarks to EndpointTargetsFromServices Signed-off-by: ivan katliarchuk <[email protected]> * chore(benchmarking): added benchmarks to EndpointTargetsFromServices Signed-off-by: ivan katliarchuk <[email protected]> * chore(benchmarking): added benchmarks to EndpointTargetsFromServices Signed-off-by: ivan katliarchuk <[email protected]> --------- Signed-off-by: ivan katliarchuk <[email protected]>
1 parent 0fae060 commit 36e3e53

File tree

8 files changed

+274
-57
lines changed

8 files changed

+274
-57
lines changed

source/endpoint_benchmark_test.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package source
15+
16+
import (
17+
"context"
18+
"encoding/binary"
19+
"fmt"
20+
"math/rand/v2"
21+
"net"
22+
"strconv"
23+
"testing"
24+
25+
"github.com/stretchr/testify/assert"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
kubeinformers "k8s.io/client-go/informers"
29+
coreinformers "k8s.io/client-go/informers/core/v1"
30+
"k8s.io/client-go/kubernetes/fake"
31+
32+
v1alpha3 "istio.io/api/networking/v1alpha3"
33+
istiov1a "istio.io/client-go/pkg/apis/networking/v1"
34+
35+
"k8s.io/client-go/tools/cache"
36+
)
37+
38+
func BenchmarkEndpointTargetsFromServicesMedium(b *testing.B) {
39+
svcInformer, err := svcInformerWithServices(36, 1000)
40+
assert.NoError(b, err)
41+
42+
sel := map[string]string{"app": "nginx", "env": "prod"}
43+
44+
for b.Loop() {
45+
targets, _ := EndpointTargetsFromServices(svcInformer, "default", sel)
46+
assert.Equal(b, 36, targets.Len())
47+
}
48+
}
49+
50+
func BenchmarkEndpointTargetsFromServicesMediumIterateOverGateways(b *testing.B) {
51+
svcInformer, err := svcInformerWithServices(36, 500)
52+
assert.NoError(b, err)
53+
54+
gateways := fixturesIstioGatewaySvcWithLabels(15, 70)
55+
56+
for b.Loop() {
57+
for _, gateway := range gateways {
58+
_, _ = EndpointTargetsFromServices(svcInformer, gateway.Namespace, gateway.Spec.Selector)
59+
}
60+
}
61+
}
62+
63+
func BenchmarkEndpointTargetsFromServicesHigh(b *testing.B) {
64+
svcInformer, err := svcInformerWithServices(36, 40000)
65+
assert.NoError(b, err)
66+
sel := map[string]string{"app": "nginx", "env": "prod"}
67+
68+
for b.Loop() {
69+
targets, _ := EndpointTargetsFromServices(svcInformer, "default", sel)
70+
assert.Equal(b, 36, targets.Len())
71+
}
72+
}
73+
74+
// This benchmark tests the performance of EndpointTargetsFromServices with a high number of services and gateways.
75+
func BenchmarkEndpointTargetsFromServicesHighIterateOverGateways(b *testing.B) {
76+
svcInformer, err := svcInformerWithServices(36, 40000)
77+
assert.NoError(b, err)
78+
79+
gateways := fixturesIstioGatewaySvcWithLabels(50, 1000)
80+
81+
for b.Loop() {
82+
for _, gateway := range gateways {
83+
_, _ = EndpointTargetsFromServices(svcInformer, gateway.Namespace, gateway.Spec.Selector)
84+
}
85+
}
86+
}
87+
88+
// helperToPopulateFakeClientWithServices populates a fake Kubernetes client with a specified services.
89+
func svcInformerWithServices(toLookup, underTest int) (coreinformers.ServiceInformer, error) {
90+
client := fake.NewClientset()
91+
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0, kubeinformers.WithNamespace("default"))
92+
svcInformer := informerFactory.Core().V1().Services()
93+
ctx := context.Background()
94+
95+
_, err := svcInformer.Informer().AddEventHandler(
96+
cache.ResourceEventHandlerFuncs{
97+
AddFunc: func(obj interface{}) {
98+
},
99+
},
100+
)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to add event handler: %w", err)
103+
}
104+
105+
services := fixturesSvcWithLabels(toLookup, underTest)
106+
for _, svc := range services {
107+
_, err := client.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{})
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to create service %s: %w", svc.Name, err)
110+
}
111+
}
112+
113+
stopCh := make(chan struct{})
114+
defer close(stopCh)
115+
informerFactory.Start(stopCh)
116+
cache.WaitForCacheSync(stopCh, svcInformer.Informer().HasSynced)
117+
return svcInformer, nil
118+
}
119+
120+
// fixturesSvcWithLabels creates a list of Services for testing purposes.
121+
// It generates a specified number of services with static labels and random labels.
122+
// The first `toLookup` services have specific labels, while the next `underTest` services have random labels.
123+
func fixturesSvcWithLabels(toLookup, underTest int) []*corev1.Service {
124+
var services []*corev1.Service
125+
126+
var randomLabels = func(input int) map[string]string {
127+
if input%3 == 0 {
128+
// every third service has no labels
129+
return map[string]string{}
130+
}
131+
return map[string]string{
132+
"app": fmt.Sprintf("service-%d", rand.IntN(100)),
133+
fmt.Sprintf("key%d", rand.IntN(100)): fmt.Sprintf("value%d", rand.IntN(100)),
134+
}
135+
}
136+
137+
var randomIPs = func() []string {
138+
ip := rand.Uint32()
139+
buf := make([]byte, 4)
140+
binary.LittleEndian.PutUint32(buf, ip)
141+
return []string{net.IP(buf).String()}
142+
}
143+
144+
var createService = func(name string, namespace string, selector map[string]string) *corev1.Service {
145+
return &corev1.Service{
146+
ObjectMeta: metav1.ObjectMeta{
147+
Name: name,
148+
Namespace: namespace,
149+
},
150+
Spec: corev1.ServiceSpec{
151+
Selector: selector,
152+
ExternalIPs: randomIPs(),
153+
},
154+
}
155+
}
156+
157+
// services with specific labels
158+
for i := 0; i < toLookup; i++ {
159+
svc := createService("nginx-svc-"+strconv.Itoa(i), "default", map[string]string{"app": "nginx", "env": "prod"})
160+
services = append(services, svc)
161+
}
162+
163+
// services with random labels
164+
for i := 0; i < underTest; i++ {
165+
svc := createService("random-svc-"+strconv.Itoa(i), "default", randomLabels(i))
166+
services = append(services, svc)
167+
}
168+
169+
// Shuffle the services to ensure randomness
170+
for i := 0; i < 3; i++ {
171+
rand.Shuffle(len(services), func(i, j int) {
172+
services[i], services[j] = services[j], services[i]
173+
})
174+
}
175+
176+
return services
177+
}
178+
179+
// fixturesIstioGatewaySvcWithLabels creates a list of Services for testing purposes.
180+
// It generates a specified number of gateways with static labels and random labels.
181+
// The first `toLookup` services have specific labels, while the next `underTest` services have random labels.
182+
func fixturesIstioGatewaySvcWithLabels(toLookup, underTest int) []*istiov1a.Gateway {
183+
var result []*istiov1a.Gateway
184+
185+
var randomLabels = func(input int) map[string]string {
186+
if input%3 == 0 {
187+
// every third service has no labels
188+
return map[string]string{}
189+
}
190+
return map[string]string{
191+
"app": fmt.Sprintf("service-%d", rand.IntN(100)),
192+
fmt.Sprintf("key%d", rand.IntN(100)): fmt.Sprintf("value%d", rand.IntN(100)),
193+
}
194+
}
195+
196+
var createGateway = func(name string, namespace string, selector map[string]string) *istiov1a.Gateway {
197+
return &istiov1a.Gateway{
198+
ObjectMeta: metav1.ObjectMeta{
199+
Name: name,
200+
Namespace: namespace,
201+
},
202+
Spec: v1alpha3.Gateway{
203+
Selector: selector,
204+
Servers: []*v1alpha3.Server{
205+
{
206+
Port: &v1alpha3.Port{},
207+
Hosts: []string{"*"},
208+
},
209+
},
210+
},
211+
}
212+
}
213+
// services with specific labels
214+
for i := 0; i < toLookup; i++ {
215+
svc := createGateway("istio-gw-"+strconv.Itoa(i), "default", map[string]string{"app": "nginx", "env": "prod"})
216+
result = append(result, svc)
217+
}
218+
219+
// services with random labels
220+
for i := 0; i < underTest; i++ {
221+
svc := createGateway("istio-random-svc-"+strconv.Itoa(i), "default", randomLabels(i))
222+
result = append(result, svc)
223+
}
224+
225+
// Shuffle the services to ensure randomness
226+
for i := 0; i < 3; i++ {
227+
rand.Shuffle(len(result), func(i, j int) {
228+
result[i], result[j] = result[j], result[i]
229+
})
230+
}
231+
232+
return result
233+
}

source/endpoints.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, name
8585
targets := endpoint.Targets{}
8686

8787
services, err := svcInformer.Lister().Services(namespace).List(labels.Everything())
88+
8889
if err != nil {
8990
return nil, fmt.Errorf("failed to list labels for services in namespace %q: %w", namespace, err)
9091
}

source/endpoints_test.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2323
kubeinformers "k8s.io/client-go/informers"
2424
"k8s.io/client-go/kubernetes/fake"
25+
2526
"sigs.k8s.io/external-dns/endpoint"
2627
)
2728

@@ -137,7 +138,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
137138
namespace: "default",
138139
selector: map[string]string{"app": "nginx"},
139140
expected: endpoint.Targets{},
140-
wantErr: false,
141141
},
142142
{
143143
name: "matching service with external IPs",
@@ -156,7 +156,23 @@ func TestEndpointTargetsFromServices(t *testing.T) {
156156
namespace: "default",
157157
selector: map[string]string{"app": "nginx"},
158158
expected: endpoint.Targets{"192.0.2.1", "158.123.32.23"},
159-
wantErr: false,
159+
},
160+
{
161+
name: "no matching service as service without selector",
162+
services: []*corev1.Service{
163+
{
164+
ObjectMeta: metav1.ObjectMeta{
165+
Name: "svc1",
166+
Namespace: "default",
167+
},
168+
Spec: corev1.ServiceSpec{
169+
ExternalIPs: []string{"192.0.2.1"},
170+
},
171+
},
172+
},
173+
namespace: "default",
174+
selector: map[string]string{"app": "nginx"},
175+
expected: endpoint.Targets{},
160176
},
161177
{
162178
name: "matching service with load balancer IP",
@@ -181,7 +197,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
181197
namespace: "default",
182198
selector: map[string]string{"app": "nginx"},
183199
expected: endpoint.Targets{"192.0.2.2"},
184-
wantErr: false,
185200
},
186201
{
187202
name: "matching service with load balancer hostname",
@@ -206,7 +221,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
206221
namespace: "default",
207222
selector: map[string]string{"app": "nginx"},
208223
expected: endpoint.Targets{"lb.example.com"},
209-
wantErr: false,
210224
},
211225
{
212226
name: "no matching services",
@@ -224,13 +238,12 @@ func TestEndpointTargetsFromServices(t *testing.T) {
224238
namespace: "default",
225239
selector: map[string]string{"app": "nginx"},
226240
expected: endpoint.Targets{},
227-
wantErr: false,
228241
},
229242
}
230243

231244
for _, tt := range tests {
232245
t.Run(tt.name, func(t *testing.T) {
233-
client := fake.NewSimpleClientset()
246+
client := fake.NewClientset()
234247
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0,
235248
kubeinformers.WithNamespace(tt.namespace))
236249
serviceInformer := informerFactory.Core().V1().Services()
@@ -253,3 +266,14 @@ func TestEndpointTargetsFromServices(t *testing.T) {
253266
})
254267
}
255268
}
269+
270+
func TestEndpointTargetsFromServicesWithFixtures(t *testing.T) {
271+
svcInformer, err := svcInformerWithServices(2, 9)
272+
assert.NoError(t, err)
273+
274+
sel := map[string]string{"app": "nginx", "env": "prod"}
275+
276+
targets, err := EndpointTargetsFromServices(svcInformer, "default", sel)
277+
assert.NoError(t, err)
278+
assert.Equal(t, 2, targets.Len())
279+
}

source/f5_virtualserver_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func TestF5VirtualServerEndpoints(t *testing.T) {
329329

330330
for _, tc := range tests {
331331
t.Run(tc.name, func(t *testing.T) {
332-
fakeKubernetesClient := fakeKube.NewSimpleClientset()
332+
fakeKubernetesClient := fakeKube.NewClientset()
333333
scheme := runtime.NewScheme()
334334
scheme.AddKnownTypes(f5VirtualServerGVR.GroupVersion(), &f5.VirtualServer{}, &f5.VirtualServerList{})
335335
fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme)

source/istio_gateway.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,7 @@ func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networ
260260
return sc.targetsFromIngress(ctx, ingressStr, gateway)
261261
}
262262

263-
targets, err := EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
264-
265-
if err != nil {
266-
return nil, err
267-
}
268-
269-
return targets, nil
263+
return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
270264
}
271265

272266
// endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
@@ -323,12 +317,3 @@ func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gatewa
323317

324318
return hostnames, nil
325319
}
326-
327-
func gatewaySelectorMatchesServiceSelector(gwSelector, svcSelector map[string]string) bool {
328-
for k, v := range gwSelector {
329-
if lbl, ok := svcSelector[k]; !ok || lbl != v {
330-
return false
331-
}
332-
}
333-
return true
334-
}

source/istio_gateway_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type GatewaySuite struct {
4646
}
4747

4848
func (suite *GatewaySuite) SetupTest() {
49-
fakeKubernetesClient := fake.NewSimpleClientset()
49+
fakeKubernetesClient := fake.NewClientset()
5050
fakeIstioClient := istiofake.NewSimpleClientset()
5151
var err error
5252

@@ -166,7 +166,7 @@ func TestNewIstioGatewaySource(t *testing.T) {
166166

167167
_, err := NewIstioGatewaySource(
168168
context.TODO(),
169-
fake.NewSimpleClientset(),
169+
fake.NewClientset(),
170170
istiofake.NewSimpleClientset(),
171171
"",
172172
ti.annotationFilter,

0 commit comments

Comments
 (0)