Commit 6d1da164 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #47731 from jsravn/use-endpoints-cache-for-endpoint-controller

Automatic merge from submit-queue Use endpoints informer for the endpoint controller This substantially reduces the number of API calls made by the endpoint controller. Currently the controller makes an API call per endpoint for each service that is synced. When the 30s resync is triggered, this results in an API call for every single endpoint in the cluster. This quickly exceeds the default qps/burst limit of 20/30 even in small clusters, leading to delays in endpoint updates. This change modifies the controller to use the endpoint informer cache for all endpoint GETs. This means we only make API calls for changes in endpoints. As a result, qps only depends on the pod activity in the cluster, rather than the number of services. **What this PR does / why we need it**: Address endpoint update delays as described in https://github.com/kubernetes/kubernetes/issues/47597. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes # https://github.com/kubernetes/kubernetes/issues/47597 **Special notes for your reviewer**: **Release note**: ```release-note ```
parents 53ac0284 9fc5a547
...@@ -178,6 +178,7 @@ func startEndpointController(ctx ControllerContext) (bool, error) { ...@@ -178,6 +178,7 @@ func startEndpointController(ctx ControllerContext) (bool, error) {
go endpointcontroller.NewEndpointController( go endpointcontroller.NewEndpointController(
ctx.InformerFactory.Core().V1().Pods(), ctx.InformerFactory.Core().V1().Pods(),
ctx.InformerFactory.Core().V1().Services(), ctx.InformerFactory.Core().V1().Services(),
ctx.InformerFactory.Core().V1().Endpoints(),
ctx.ClientBuilder.ClientOrDie("endpoint-controller"), ctx.ClientBuilder.ClientOrDie("endpoint-controller"),
).Run(int(ctx.Options.ConcurrentEndpointSyncs), ctx.Stop) ).Run(int(ctx.Options.ConcurrentEndpointSyncs), ctx.Stop)
return true, nil return true, nil
......
...@@ -16,6 +16,7 @@ go_library( ...@@ -16,6 +16,7 @@ go_library(
], ],
tags = ["automanaged"], tags = ["automanaged"],
deps = [ deps = [
"//pkg/api:go_default_library",
"//pkg/api/v1/endpoints:go_default_library", "//pkg/api/v1/endpoints:go_default_library",
"//pkg/api/v1/pod:go_default_library", "//pkg/api/v1/pod:go_default_library",
"//pkg/client/clientset_generated/clientset:go_default_library", "//pkg/client/clientset_generated/clientset:go_default_library",
...@@ -53,6 +54,7 @@ go_test( ...@@ -53,6 +54,7 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library", "//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/util/testing:go_default_library", "//vendor/k8s.io/client-go/util/testing:go_default_library",
......
...@@ -31,6 +31,7 @@ import ( ...@@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1/endpoints" "k8s.io/kubernetes/pkg/api/v1/endpoints"
podutil "k8s.io/kubernetes/pkg/api/v1/pod" podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset" "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
...@@ -69,13 +70,15 @@ var ( ...@@ -69,13 +70,15 @@ var (
) )
// NewEndpointController returns a new *EndpointController. // NewEndpointController returns a new *EndpointController.
func NewEndpointController(podInformer coreinformers.PodInformer, serviceInformer coreinformers.ServiceInformer, client clientset.Interface) *EndpointController { func NewEndpointController(podInformer coreinformers.PodInformer, serviceInformer coreinformers.ServiceInformer,
endpointsInformer coreinformers.EndpointsInformer, client clientset.Interface) *EndpointController {
if client != nil && client.Core().RESTClient().GetRateLimiter() != nil { if client != nil && client.Core().RESTClient().GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("endpoint_controller", client.Core().RESTClient().GetRateLimiter()) metrics.RegisterMetricAndTrackRateLimiterUsage("endpoint_controller", client.Core().RESTClient().GetRateLimiter())
} }
e := &EndpointController{ e := &EndpointController{
client: client, client: client,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "endpoint"), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "endpoint"),
workerLoopPeriod: time.Second,
} }
serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
...@@ -96,6 +99,9 @@ func NewEndpointController(podInformer coreinformers.PodInformer, serviceInforme ...@@ -96,6 +99,9 @@ func NewEndpointController(podInformer coreinformers.PodInformer, serviceInforme
e.podLister = podInformer.Lister() e.podLister = podInformer.Lister()
e.podsSynced = podInformer.Informer().HasSynced e.podsSynced = podInformer.Informer().HasSynced
e.endpointsLister = endpointsInformer.Lister()
e.endpointsSynced = endpointsInformer.Informer().HasSynced
return e return e
} }
...@@ -117,12 +123,22 @@ type EndpointController struct { ...@@ -117,12 +123,22 @@ type EndpointController struct {
// Added as a member to the struct to allow injection for testing. // Added as a member to the struct to allow injection for testing.
podsSynced cache.InformerSynced podsSynced cache.InformerSynced
// endpointsLister is able to list/get endpoints and is populated by the shared informer passed to
// NewEndpointController.
endpointsLister corelisters.EndpointsLister
// endpointsSynced returns true if the endpoints shared informer has been synced at least once.
// Added as a member to the struct to allow injection for testing.
endpointsSynced cache.InformerSynced
// Services that need to be updated. A channel is inappropriate here, // Services that need to be updated. A channel is inappropriate here,
// because it allows services with lots of pods to be serviced much // because it allows services with lots of pods to be serviced much
// more often than services with few pods; it also would cause a // more often than services with few pods; it also would cause a
// service that's inserted multiple times to be processed more than // service that's inserted multiple times to be processed more than
// necessary. // necessary.
queue workqueue.RateLimitingInterface queue workqueue.RateLimitingInterface
// workerLoopPeriod is the time between worker runs. The workers process the queue of service and pod changes.
workerLoopPeriod time.Duration
} }
// Runs e; will not return until stopCh is closed. workers determines how many // Runs e; will not return until stopCh is closed. workers determines how many
...@@ -134,12 +150,12 @@ func (e *EndpointController) Run(workers int, stopCh <-chan struct{}) { ...@@ -134,12 +150,12 @@ func (e *EndpointController) Run(workers int, stopCh <-chan struct{}) {
glog.Infof("Starting endpoint controller") glog.Infof("Starting endpoint controller")
defer glog.Infof("Shutting down endpoint controller") defer glog.Infof("Shutting down endpoint controller")
if !controller.WaitForCacheSync("endpoint", stopCh, e.podsSynced, e.servicesSynced) { if !controller.WaitForCacheSync("endpoint", stopCh, e.podsSynced, e.servicesSynced, e.endpointsSynced) {
return return
} }
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
go wait.Until(e.worker, time.Second, stopCh) go wait.Until(e.worker, e.workerLoopPeriod, stopCh)
} }
go func() { go func() {
...@@ -413,7 +429,7 @@ func (e *EndpointController) syncService(key string) error { ...@@ -413,7 +429,7 @@ func (e *EndpointController) syncService(key string) error {
subsets = endpoints.RepackSubsets(subsets) subsets = endpoints.RepackSubsets(subsets)
// See if there's actually an update here. // See if there's actually an update here.
currentEndpoints, err := e.client.Core().Endpoints(service.Namespace).Get(service.Name, metav1.GetOptions{}) currentEndpoints, err := e.endpointsLister.Endpoints(service.Namespace).Get(service.Name)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
currentEndpoints = &v1.Endpoints{ currentEndpoints = &v1.Endpoints{
...@@ -432,7 +448,11 @@ func (e *EndpointController) syncService(key string) error { ...@@ -432,7 +448,11 @@ func (e *EndpointController) syncService(key string) error {
glog.V(5).Infof("endpoints are equal for %s/%s, skipping update", service.Namespace, service.Name) glog.V(5).Infof("endpoints are equal for %s/%s, skipping update", service.Namespace, service.Name)
return nil return nil
} }
newEndpoints := currentEndpoints copy, err := api.Scheme.DeepCopy(currentEndpoints)
if err != nil {
return err
}
newEndpoints := copy.(*v1.Endpoints)
newEndpoints.Subsets = subsets newEndpoints.Subsets = subsets
newEndpoints.Labels = service.Labels newEndpoints.Labels = service.Labels
if newEndpoints.Annotations == nil { if newEndpoints.Annotations == nil {
...@@ -468,13 +488,12 @@ func (e *EndpointController) syncService(key string) error { ...@@ -468,13 +488,12 @@ func (e *EndpointController) syncService(key string) error {
// some stragglers could have been left behind if the endpoint controller // some stragglers could have been left behind if the endpoint controller
// reboots). // reboots).
func (e *EndpointController) checkLeftoverEndpoints() { func (e *EndpointController) checkLeftoverEndpoints() {
list, err := e.client.Core().Endpoints(metav1.NamespaceAll).List(metav1.ListOptions{}) list, err := e.endpointsLister.List(labels.Everything())
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to list endpoints (%v); orphaned endpoints will not be cleaned up. (They're pretty harmless, but you can restart this component if you want another attempt made.)", err)) utilruntime.HandleError(fmt.Errorf("Unable to list endpoints (%v); orphaned endpoints will not be cleaned up. (They're pretty harmless, but you can restart this component if you want another attempt made.)", err))
return return
} }
for i := range list.Items { for _, ep := range list {
ep := &list.Items[i]
if _, ok := ep.Annotations[resourcelock.LeaderElectionRecordAnnotationKey]; ok { if _, ok := ep.Annotations[resourcelock.LeaderElectionRecordAnnotationKey]; ok {
// when there are multiple controller-manager instances, // when there are multiple controller-manager instances,
// we observe that it will delete leader-election endpoints after 5min // we observe that it will delete leader-election endpoints after 5min
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment