-
Notifications
You must be signed in to change notification settings - Fork 28
Filter headless Services from the informer cache via spec.clusterIP field selector #258
Description
What
Add a server-side field selector to the Service informer so headless Services are never listed, watched, or cached:
&corev1.Service{}: { Transform: k8s.StripDownServiceTransformFunc, Field: fields.OneTermNotEqualSelector("spec.clusterIP", corev1.ClusterIPNone), },
This is the same pattern the controller already uses for Pods (podCacheFieldSelector in pkg/config/runtime_config.go), and the same optimization kubelet adopted in Kubernetes 1.31 to stop watching headless Services (kubernetes/kubernetes#122541, motivated by kubernetes/kubernetes#122394).
Why
Every consumer of the Service cache already ignores headless Services, so caching them is pure overhead:
pkg/resolvers/endpoints.go—getMatchingServiceClusterIPsskips headless Services (no ClusterIP to resolve).pkg/resolvers/policies_for_service.go— NetworkPolicy and ApplicationNetworkPolicy service-reference resolution early-returns on headless Services.internal/eventhandlers/service.go— headless Service create/update/delete events still fire the handler, andGetReferredClusterPoliciesForServiceenqueues all ClusterNetworkPolicies for reconcile even though headless Services can never contribute endpoints.
Filtering server-side eliminates:
- Cache memory for headless Services (can be a large fraction of total Services in StatefulSet-heavy clusters).
- List/watch bandwidth on the apiserver connection.
- Iteration overhead in
getMatchingServiceClusterIPs, which lists Services per egress-peer per namespace. - Spurious full-CNP reconciles triggered by headless Service churn.
spec.clusterIP is immutable, so Services do not transition in/out of the filtered set. For the ExternalName↔ClusterIP type-change edge case, the apiserver synthesizes Added/Deleted watch events, which the existing handlers already handle.
Constraint
The spec.clusterIP field selector for Services requires Kubernetes 1.31+. On older apiservers the list/watch fails with field label not supported and the informer never syncs. The README currently states a 1.25+ prerequisite, so this needs one of:
- a minimum-version bump to 1.31+, or
- server-version discovery at startup with conditional application of the selector (kubelet-style fallback).
Note: existing IsServiceHeadless checks should remain regardless — they cover the unfiltered fallback path and ExternalName Services (clusterIP == ""), which the != "None" selector does not exclude.