perf(source): benchmarks on EndpointTargetsFromServices (#5536)

* chore(benchmarking): added benchmarks to EndpointTargetsFromServices

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(benchmarking): added benchmarks to EndpointTargetsFromServices

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(benchmarking): added benchmarks to EndpointTargetsFromServices

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(benchmarking): added benchmarks to EndpointTargetsFromServices

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
This commit is contained in:
Ivan Ka 2025-06-19 23:31:00 +01:00 committed by GitHub
parent 0fae060dff
commit 36e3e53190
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 274 additions and 57 deletions

View File

@ -0,0 +1,233 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"context"
"encoding/binary"
"fmt"
"math/rand/v2"
"net"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes/fake"
v1alpha3 "istio.io/api/networking/v1alpha3"
istiov1a "istio.io/client-go/pkg/apis/networking/v1"
"k8s.io/client-go/tools/cache"
)
func BenchmarkEndpointTargetsFromServicesMedium(b *testing.B) {
svcInformer, err := svcInformerWithServices(36, 1000)
assert.NoError(b, err)
sel := map[string]string{"app": "nginx", "env": "prod"}
for b.Loop() {
targets, _ := EndpointTargetsFromServices(svcInformer, "default", sel)
assert.Equal(b, 36, targets.Len())
}
}
func BenchmarkEndpointTargetsFromServicesMediumIterateOverGateways(b *testing.B) {
svcInformer, err := svcInformerWithServices(36, 500)
assert.NoError(b, err)
gateways := fixturesIstioGatewaySvcWithLabels(15, 70)
for b.Loop() {
for _, gateway := range gateways {
_, _ = EndpointTargetsFromServices(svcInformer, gateway.Namespace, gateway.Spec.Selector)
}
}
}
func BenchmarkEndpointTargetsFromServicesHigh(b *testing.B) {
svcInformer, err := svcInformerWithServices(36, 40000)
assert.NoError(b, err)
sel := map[string]string{"app": "nginx", "env": "prod"}
for b.Loop() {
targets, _ := EndpointTargetsFromServices(svcInformer, "default", sel)
assert.Equal(b, 36, targets.Len())
}
}
// This benchmark tests the performance of EndpointTargetsFromServices with a high number of services and gateways.
func BenchmarkEndpointTargetsFromServicesHighIterateOverGateways(b *testing.B) {
svcInformer, err := svcInformerWithServices(36, 40000)
assert.NoError(b, err)
gateways := fixturesIstioGatewaySvcWithLabels(50, 1000)
for b.Loop() {
for _, gateway := range gateways {
_, _ = EndpointTargetsFromServices(svcInformer, gateway.Namespace, gateway.Spec.Selector)
}
}
}
// helperToPopulateFakeClientWithServices populates a fake Kubernetes client with a specified services.
func svcInformerWithServices(toLookup, underTest int) (coreinformers.ServiceInformer, error) {
client := fake.NewClientset()
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0, kubeinformers.WithNamespace("default"))
svcInformer := informerFactory.Core().V1().Services()
ctx := context.Background()
_, err := svcInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to add event handler: %w", err)
}
services := fixturesSvcWithLabels(toLookup, underTest)
for _, svc := range services {
_, err := client.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create service %s: %w", svc.Name, err)
}
}
stopCh := make(chan struct{})
defer close(stopCh)
informerFactory.Start(stopCh)
cache.WaitForCacheSync(stopCh, svcInformer.Informer().HasSynced)
return svcInformer, nil
}
// fixturesSvcWithLabels creates a list of Services for testing purposes.
// It generates a specified number of services with static labels and random labels.
// The first `toLookup` services have specific labels, while the next `underTest` services have random labels.
func fixturesSvcWithLabels(toLookup, underTest int) []*corev1.Service {
var services []*corev1.Service
var randomLabels = func(input int) map[string]string {
if input%3 == 0 {
// every third service has no labels
return map[string]string{}
}
return map[string]string{
"app": fmt.Sprintf("service-%d", rand.IntN(100)),
fmt.Sprintf("key%d", rand.IntN(100)): fmt.Sprintf("value%d", rand.IntN(100)),
}
}
var randomIPs = func() []string {
ip := rand.Uint32()
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, ip)
return []string{net.IP(buf).String()}
}
var createService = func(name string, namespace string, selector map[string]string) *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: corev1.ServiceSpec{
Selector: selector,
ExternalIPs: randomIPs(),
},
}
}
// services with specific labels
for i := 0; i < toLookup; i++ {
svc := createService("nginx-svc-"+strconv.Itoa(i), "default", map[string]string{"app": "nginx", "env": "prod"})
services = append(services, svc)
}
// services with random labels
for i := 0; i < underTest; i++ {
svc := createService("random-svc-"+strconv.Itoa(i), "default", randomLabels(i))
services = append(services, svc)
}
// Shuffle the services to ensure randomness
for i := 0; i < 3; i++ {
rand.Shuffle(len(services), func(i, j int) {
services[i], services[j] = services[j], services[i]
})
}
return services
}
// fixturesIstioGatewaySvcWithLabels creates a list of Services for testing purposes.
// It generates a specified number of gateways with static labels and random labels.
// The first `toLookup` services have specific labels, while the next `underTest` services have random labels.
func fixturesIstioGatewaySvcWithLabels(toLookup, underTest int) []*istiov1a.Gateway {
var result []*istiov1a.Gateway
var randomLabels = func(input int) map[string]string {
if input%3 == 0 {
// every third service has no labels
return map[string]string{}
}
return map[string]string{
"app": fmt.Sprintf("service-%d", rand.IntN(100)),
fmt.Sprintf("key%d", rand.IntN(100)): fmt.Sprintf("value%d", rand.IntN(100)),
}
}
var createGateway = func(name string, namespace string, selector map[string]string) *istiov1a.Gateway {
return &istiov1a.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1alpha3.Gateway{
Selector: selector,
Servers: []*v1alpha3.Server{
{
Port: &v1alpha3.Port{},
Hosts: []string{"*"},
},
},
},
}
}
// services with specific labels
for i := 0; i < toLookup; i++ {
svc := createGateway("istio-gw-"+strconv.Itoa(i), "default", map[string]string{"app": "nginx", "env": "prod"})
result = append(result, svc)
}
// services with random labels
for i := 0; i < underTest; i++ {
svc := createGateway("istio-random-svc-"+strconv.Itoa(i), "default", randomLabels(i))
result = append(result, svc)
}
// Shuffle the services to ensure randomness
for i := 0; i < 3; i++ {
rand.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
}
return result
}

View File

@ -85,6 +85,7 @@ func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, name
targets := endpoint.Targets{} targets := endpoint.Targets{}
services, err := svcInformer.Lister().Services(namespace).List(labels.Everything()) services, err := svcInformer.Lister().Services(namespace).List(labels.Everything())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list labels for services in namespace %q: %w", namespace, err) return nil, fmt.Errorf("failed to list labels for services in namespace %q: %w", namespace, err)
} }

View File

@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
) )
@ -137,7 +138,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
namespace: "default", namespace: "default",
selector: map[string]string{"app": "nginx"}, selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{}, expected: endpoint.Targets{},
wantErr: false,
}, },
{ {
name: "matching service with external IPs", name: "matching service with external IPs",
@ -156,7 +156,23 @@ func TestEndpointTargetsFromServices(t *testing.T) {
namespace: "default", namespace: "default",
selector: map[string]string{"app": "nginx"}, selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"192.0.2.1", "158.123.32.23"}, expected: endpoint.Targets{"192.0.2.1", "158.123.32.23"},
wantErr: false, },
{
name: "no matching service as service without selector",
services: []*corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "svc1",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
ExternalIPs: []string{"192.0.2.1"},
},
},
},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{},
}, },
{ {
name: "matching service with load balancer IP", name: "matching service with load balancer IP",
@ -181,7 +197,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
namespace: "default", namespace: "default",
selector: map[string]string{"app": "nginx"}, selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"192.0.2.2"}, expected: endpoint.Targets{"192.0.2.2"},
wantErr: false,
}, },
{ {
name: "matching service with load balancer hostname", name: "matching service with load balancer hostname",
@ -206,7 +221,6 @@ func TestEndpointTargetsFromServices(t *testing.T) {
namespace: "default", namespace: "default",
selector: map[string]string{"app": "nginx"}, selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"lb.example.com"}, expected: endpoint.Targets{"lb.example.com"},
wantErr: false,
}, },
{ {
name: "no matching services", name: "no matching services",
@ -224,13 +238,12 @@ func TestEndpointTargetsFromServices(t *testing.T) {
namespace: "default", namespace: "default",
selector: map[string]string{"app": "nginx"}, selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{}, expected: endpoint.Targets{},
wantErr: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
client := fake.NewSimpleClientset() client := fake.NewClientset()
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0, informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0,
kubeinformers.WithNamespace(tt.namespace)) kubeinformers.WithNamespace(tt.namespace))
serviceInformer := informerFactory.Core().V1().Services() serviceInformer := informerFactory.Core().V1().Services()
@ -253,3 +266,14 @@ func TestEndpointTargetsFromServices(t *testing.T) {
}) })
} }
} }
func TestEndpointTargetsFromServicesWithFixtures(t *testing.T) {
svcInformer, err := svcInformerWithServices(2, 9)
assert.NoError(t, err)
sel := map[string]string{"app": "nginx", "env": "prod"}
targets, err := EndpointTargetsFromServices(svcInformer, "default", sel)
assert.NoError(t, err)
assert.Equal(t, 2, targets.Len())
}

View File

@ -329,7 +329,7 @@ func TestF5VirtualServerEndpoints(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fakeKubernetesClient := fakeKube.NewSimpleClientset() fakeKubernetesClient := fakeKube.NewClientset()
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
scheme.AddKnownTypes(f5VirtualServerGVR.GroupVersion(), &f5.VirtualServer{}, &f5.VirtualServerList{}) scheme.AddKnownTypes(f5VirtualServerGVR.GroupVersion(), &f5.VirtualServer{}, &f5.VirtualServerList{})
fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme)

View File

@ -260,13 +260,7 @@ func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networ
return sc.targetsFromIngress(ctx, ingressStr, gateway) return sc.targetsFromIngress(ctx, ingressStr, gateway)
} }
targets, err := EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector) return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
if err != nil {
return nil, err
}
return targets, nil
} }
// endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
@ -323,12 +317,3 @@ func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gatewa
return hostnames, nil return hostnames, nil
} }
func gatewaySelectorMatchesServiceSelector(gwSelector, svcSelector map[string]string) bool {
for k, v := range gwSelector {
if lbl, ok := svcSelector[k]; !ok || lbl != v {
return false
}
}
return true
}

View File

@ -46,7 +46,7 @@ type GatewaySuite struct {
} }
func (suite *GatewaySuite) SetupTest() { func (suite *GatewaySuite) SetupTest() {
fakeKubernetesClient := fake.NewSimpleClientset() fakeKubernetesClient := fake.NewClientset()
fakeIstioClient := istiofake.NewSimpleClientset() fakeIstioClient := istiofake.NewSimpleClientset()
var err error var err error
@ -166,7 +166,7 @@ func TestNewIstioGatewaySource(t *testing.T) {
_, err := NewIstioGatewaySource( _, err := NewIstioGatewaySource(
context.TODO(), context.TODO(),
fake.NewSimpleClientset(), fake.NewClientset(),
istiofake.NewSimpleClientset(), istiofake.NewSimpleClientset(),
"", "",
ti.annotationFilter, ti.annotationFilter,

View File

@ -453,42 +453,16 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS
return return
} }
func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) {
targets = annotations.TargetsFromTargetAnnotation(gateway.Annotations) targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 { if len(targets) > 0 {
return return targets, nil
} }
ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource] ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
if ok && ingressStr != "" { if ok && ingressStr != "" {
targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway) return sc.targetsFromIngress(ctx, ingressStr, gateway)
return
} }
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything()) return EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
if err != nil {
log.Error(err)
return
}
for _, service := range services {
if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
continue
}
if len(service.Spec.ExternalIPs) > 0 {
targets = append(targets, service.Spec.ExternalIPs...)
continue
}
for _, lb := range service.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
} else if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
return
} }

View File

@ -2035,7 +2035,7 @@ func testGatewaySelectorMatchesService(t *testing.T) {
}, },
} { } {
t.Run(ti.title, func(t *testing.T) { t.Run(ti.title, func(t *testing.T) {
require.Equal(t, ti.expected, gatewaySelectorMatchesServiceSelector(ti.gwSelector, ti.lbSelector)) require.Equal(t, ti.expected, MatchesServiceSelector(ti.gwSelector, ti.lbSelector))
}) })
} }
} }