istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/krt/bench_test.go (about)

     1  // Copyright Istio Authors
     2  //
     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  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package krt_test
    16  
    17  import (
    18  	"fmt"
    19  	"net"
    20  	"reflect"
    21  	"testing"
    22  
    23  	v1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	klabels "k8s.io/apimachinery/pkg/labels"
    26  	"k8s.io/apimachinery/pkg/types"
    27  	"k8s.io/apimachinery/pkg/watch"
    28  
    29  	"istio.io/istio/pkg/config/labels"
    30  	"istio.io/istio/pkg/kube"
    31  	"istio.io/istio/pkg/kube/controllers"
    32  	"istio.io/istio/pkg/kube/kclient"
    33  	"istio.io/istio/pkg/kube/kclient/clienttest"
    34  	"istio.io/istio/pkg/kube/krt"
    35  	"istio.io/istio/pkg/log"
    36  	"istio.io/istio/pkg/slices"
    37  	"istio.io/istio/pkg/test"
    38  )
    39  
    40  type Workload struct {
    41  	krt.Named
    42  	ServiceNames []string
    43  	IP           string
    44  }
    45  
    46  // GetLabelSelector defaults to using Reflection which is slow. Provide a specialized implementation that does it more efficiently.
    47  type ServiceWrapper struct{ *v1.Service }
    48  
    49  func (s ServiceWrapper) GetLabelSelector() map[string]string {
    50  	return s.Spec.Selector
    51  }
    52  
    53  var _ krt.LabelSelectorer = ServiceWrapper{}
    54  
    55  func NewModern(c kube.Client, events chan string, _ <-chan struct{}) {
    56  	Pods := krt.NewInformer[*v1.Pod](c)
    57  	Services := krt.NewInformer[*v1.Service](c, krt.WithObjectAugmentation(func(o any) any {
    58  		return ServiceWrapper{o.(*v1.Service)}
    59  	}))
    60  	ServicesByNamespace := krt.NewNamespaceIndex(Services)
    61  
    62  	Workloads := krt.NewCollection(Pods, func(ctx krt.HandlerContext, p *v1.Pod) *Workload {
    63  		if p.Status.PodIP == "" {
    64  			return nil
    65  		}
    66  		services := krt.Fetch(ctx, Services, krt.FilterIndex(ServicesByNamespace, p.Namespace), krt.FilterSelectsNonEmpty(p.GetLabels()))
    67  		return &Workload{
    68  			Named:        krt.NewNamed(p),
    69  			IP:           p.Status.PodIP,
    70  			ServiceNames: slices.Map(services, func(e *v1.Service) string { return e.Name }),
    71  		}
    72  	})
    73  	Workloads.Register(func(e krt.Event[Workload]) {
    74  		events <- fmt.Sprintf(e.Latest().Name, e.Event)
    75  	})
    76  }
    77  
    78  type legacy struct {
    79  	pods      kclient.Client[*v1.Pod]
    80  	services  kclient.Client[*v1.Service]
    81  	queue     controllers.Queue
    82  	workloads map[types.NamespacedName]*Workload
    83  	handler   func(event krt.Event[Workload])
    84  }
    85  
    86  func getPodServices(allServices []*v1.Service, pod *v1.Pod) []*v1.Service {
    87  	var services []*v1.Service
    88  	for _, service := range allServices {
    89  		if labels.Instance(service.Spec.Selector).Match(pod.Labels) {
    90  			services = append(services, service)
    91  		}
    92  	}
    93  
    94  	return services
    95  }
    96  
    97  func (l *legacy) Reconcile(key types.NamespacedName) error {
    98  	pod := l.pods.Get(key.Name, key.Namespace)
    99  	if pod == nil || pod.Status.PodIP == "" {
   100  		old := l.workloads[key]
   101  		if old != nil {
   102  			ev := krt.Event[Workload]{
   103  				Old:   old,
   104  				Event: controllers.EventDelete,
   105  			}
   106  			l.handler(ev)
   107  			delete(l.workloads, key)
   108  		}
   109  
   110  		return nil
   111  	}
   112  	allServices := l.services.List(pod.Namespace, klabels.Everything())
   113  	services := getPodServices(allServices, pod)
   114  	wl := &Workload{
   115  		Named:        krt.NewNamed(pod),
   116  		IP:           pod.Status.PodIP,
   117  		ServiceNames: slices.Map(services, func(e *v1.Service) string { return e.Name }),
   118  	}
   119  	old := l.workloads[key]
   120  	if reflect.DeepEqual(old, wl) {
   121  		// No changes, NOP
   122  		return nil
   123  	}
   124  	// Changed. Update and call handlers
   125  	l.workloads[key] = wl
   126  	if old == nil {
   127  		l.handler(krt.Event[Workload]{
   128  			New:   wl,
   129  			Event: controllers.EventAdd,
   130  		})
   131  	} else {
   132  		l.handler(krt.Event[Workload]{
   133  			Old:   old,
   134  			New:   wl,
   135  			Event: controllers.EventUpdate,
   136  		})
   137  	}
   138  	return nil
   139  }
   140  
   141  func NewLegacy(cl kube.Client, events chan string, stop <-chan struct{}) {
   142  	c := &legacy{
   143  		workloads: map[types.NamespacedName]*Workload{},
   144  	}
   145  	c.pods = kclient.New[*v1.Pod](cl)
   146  	c.services = kclient.New[*v1.Service](cl)
   147  	c.queue = controllers.NewQueue("pods", controllers.WithReconciler(c.Reconcile))
   148  	c.pods.AddEventHandler(controllers.ObjectHandler(c.queue.AddObject))
   149  	c.services.AddEventHandler(controllers.FromEventHandler(func(e controllers.Event) {
   150  		o := e.Latest()
   151  		for _, pod := range c.pods.List(o.GetNamespace(), klabels.SelectorFromValidatedSet(o.(*v1.Service).Spec.Selector)) {
   152  			c.queue.AddObject(pod)
   153  		}
   154  	}))
   155  	c.handler = func(e krt.Event[Workload]) {
   156  		events <- fmt.Sprintf(e.Latest().Name, e.Event)
   157  	}
   158  	go c.queue.Run(stop)
   159  }
   160  
   161  var nextIP = net.ParseIP("10.0.0.10")
   162  
   163  func GetIP() string {
   164  	i := nextIP.To4()
   165  	ret := i.String()
   166  	v := uint(i[0])<<24 + uint(i[1])<<16 + uint(i[2])<<8 + uint(i[3])
   167  	v++
   168  	v3 := byte(v & 0xFF)
   169  	v2 := byte((v >> 8) & 0xFF)
   170  	v1 := byte((v >> 16) & 0xFF)
   171  	v0 := byte((v >> 24) & 0xFF)
   172  	nextIP = net.IPv4(v0, v1, v2, v3)
   173  	return ret
   174  }
   175  
   176  func drainN(c chan string, n int) {
   177  	for n > 0 {
   178  		n--
   179  		<-c
   180  	}
   181  }
   182  
   183  func BenchmarkControllers(b *testing.B) {
   184  	log.FindScope("krt").SetOutputLevel(log.InfoLevel)
   185  	watch.DefaultChanSize = 100_000
   186  	initialPods := []*v1.Pod{}
   187  	for i := 0; i < 1000; i++ {
   188  		initialPods = append(initialPods, &v1.Pod{
   189  			ObjectMeta: metav1.ObjectMeta{
   190  				Name:      fmt.Sprintf("pod-%d", i),
   191  				Namespace: fmt.Sprintf("ns-%d", i%2),
   192  				Labels: map[string]string{
   193  					"app": fmt.Sprintf("app-%d", i%25),
   194  				},
   195  			},
   196  			Spec: v1.PodSpec{
   197  				ServiceAccountName: "fake-sa",
   198  			},
   199  			Status: v1.PodStatus{
   200  				Phase: v1.PodRunning,
   201  				PodIP: GetIP(),
   202  			},
   203  		})
   204  	}
   205  	initialServices := []*v1.Service{}
   206  	for i := 0; i < 50; i++ {
   207  		initialServices = append(initialServices, &v1.Service{
   208  			ObjectMeta: metav1.ObjectMeta{
   209  				Name:      fmt.Sprintf("pod-%d", i),
   210  				Namespace: fmt.Sprintf("ns-%d", i%2),
   211  			},
   212  			Spec: v1.ServiceSpec{
   213  				Selector: map[string]string{
   214  					"app": fmt.Sprintf("app-%d", i%25),
   215  				},
   216  			},
   217  		})
   218  	}
   219  	benchmark := func(b *testing.B, fn func(client kube.Client, events chan string, stop <-chan struct{})) {
   220  		c := kube.NewFakeClient()
   221  		events := make(chan string, 1000)
   222  		stop := test.NewStop(b)
   223  		fn(c, events, stop)
   224  		pods := clienttest.NewWriter[*v1.Pod](b, c)
   225  		services := clienttest.NewWriter[*v1.Service](b, c)
   226  		for _, p := range initialPods {
   227  			pods.Create(p)
   228  		}
   229  		for _, p := range initialServices {
   230  			services.Create(p)
   231  		}
   232  		b.ResetTimer()
   233  		c.RunAndWait(test.NewStop(b))
   234  		drainN(events, 1000)
   235  		for n := 0; n < b.N; n++ {
   236  			for i := 0; i < 1000; i++ {
   237  				pods.Update(&v1.Pod{
   238  					ObjectMeta: metav1.ObjectMeta{
   239  						Name:      fmt.Sprintf("pod-%d", i),
   240  						Namespace: fmt.Sprintf("ns-%d", i%2),
   241  						Labels: map[string]string{
   242  							"app": fmt.Sprintf("app-%d", i%25),
   243  						},
   244  					},
   245  					Spec: v1.PodSpec{
   246  						ServiceAccountName: "fake-sa",
   247  					},
   248  					Status: v1.PodStatus{
   249  						Phase: v1.PodRunning,
   250  						PodIP: GetIP(),
   251  					},
   252  				})
   253  			}
   254  			drainN(events, 1000)
   255  		}
   256  	}
   257  	b.Run("krt", func(b *testing.B) {
   258  		benchmark(b, NewModern)
   259  	})
   260  	b.Run("legacy", func(b *testing.B) {
   261  		benchmark(b, NewLegacy)
   262  	})
   263  }