Skip to content
LinkState
Go back

Why one backend stopped receiving service traffic

Introduction to EndpointSlices and ClusterIP

Overview of EndpointSlices

EndpointSlices are a scalable, partitioned replacement for the legacy Endpoints object in Kubernetes. Each slice holds a limited number of endpoints (default 100) and is identified by the label kubernetes.io/service-name. The controller manager creates and updates slices in response to changes in Pod readiness, node affinity, or topology spread constraints. kube-proxy watches these slices on each node to program IPVS or iptables rules that direct ClusterIP traffic to healthy backends.

Role of ClusterIP in Kubernetes

A Service of type ClusterIP allocates a virtual IP from the cluster’s service CIDR. Clients inside the cluster resolve the service name via CoreDNS to that IP. kube-proxy translates the virtual IP to a set of destination addresses derived from the current EndpointSlices for that service. The translation occurs per‑node, using either iptables (NAT mode) or IPVS (proxy mode). If the node’s view of the EndpointSlices lags behind the control plane, traffic may be blackholed for a subset of backends even though the slices appear updated globally.

Understanding the Failure Scenario

Initial Symptoms and Observations

Investigating EndpointSlice Updates

  1. Verify slice version numbers:
    kubectl get endpointslices -l kubernetes.io/service-name=frontend -o yaml | grep -E 'metadata:|resourceVersion:'
    Output shows all slices at resourceVersion: "158423" – identical across nodes.
  2. Check the slice’s addressType and ports:
    kubectl get endpointslices -l kubernetes.io/service-name=frontend -o jsonpath='{.items[*].addressType}{"\n"}'
    Returns IPv4 for all slices.
  3. Confirm that each slice contains the expected Pod IPs:
    for slice in $(kubectl get endpointslices -l kubernetes.io/service-name=frontend -o name); do
      echo "$slice:"; kubectl get $slice -o jsonpath='{.endpoints[*].addresses}{"\n"}'
    done
    Lists six distinct IPs, matching the three Pods (each Pod appears twice due to slice partitioning).

At this point the control‑plane view appears healthy.

Identifying the Silent Backend Failure

Thus, one backend is silently excluded from the node’s load‑balancing dataplane despite the EndpointSlice being up‑to‑date in etcd.

Troubleshooting the Issue

Checking Node‑Local Rendering Lag

kube-proxy watches EndpointSlices via the Kubernetes API and applies changes locally. The watch can be delayed by:

To verify lag:

  1. Enable verbose kube-proxy logs on the node:
    kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=100 | grep -E 'endpointSlice|sync'
    Look for lines like:
    I0925 14:03:12.345678       1 reflector.go:205] Watching *v1.EndpointSlice
    I0925 14:03:12.345679       1 proxier.go:456] EndpointSlice svc/frontend: received update, version 158423
    I0925 14:03:12.345685680       1 proxier.go:470] Scheduling sync in 20s (syncPeriod=30s)
    The gap between receipt and scheduled sync indicates the rendering lag.
  2. Check the node’s kube-proxy config:
    kubectl -n kube-system get configmap kube-proxy -o yaml | grep syncPeriod
    If syncPeriod is set to a high value (e.g., "5m"), updates are delayed.

Verifying Service State and EndpointSlices

Using CLI Tools for Debugging

Code Examples for Troubleshooting

Using kubectl to Inspect EndpointSlices

# List all slices for a service with resource versions
kubectl get endpointslices -l kubernetes.io/service-name=frontend -o wide

# Show a single slice in JSON, including endpoints
kubectl get endpointslice frontend-abcdefg -o json |
  jq '.endpoints[].addresses, .endpoints[].conditions.ready'

Checking Service State with kubectl get

# Verify ClusterIP and port
kubectl get svc frontend -o jsonpath='{.spec.clusterIP}{"\t"}{.spec.ports[0].port}{"\n"}'

# Confirm no stray selectors
kubectl get svc frontend -o jsonpath='{.spec.selector}'

Example CLI Commands for Debugging

# Watch EndpointSlice updates in real time
kubectl get endpointslices -l kubernetes.io/service-name=frontend -w

# Dump kube-proxy IPVS rules on a node
kubectl -n kube-system exec $(kubectl -n kube-system get pod -l k8s-app=kube-proxy -o jsonpath="{.items[0].metadata.name}") -- ipvsadm -Ln

# Measure sync latency from kube-proxy metrics (if exposed)
kubectl get --raw /api/v1/nodes/worker-03/proxy/metrics |
  grep kubeproxy_sync_latency_seconds |
  tail -n 1

Root Cause Analysis

Node‑Local Rendering Lag and Stale Service State

The symptom arises when kube-proxy receives an EndpointSlice update but defers applying it until the next full resync cycle. During the intervening window, the node’s IPVS (or iptables) table still reflects the previous slice version, which may lack endpoints for a Pod that has just become Ready or has changed its IP due to rescheduling. Because the EndpointSlice object in etcd already shows the new endpoints, kubectl get endpointslices appears healthy, yet the dataplane is stale.

Impact of Caching and DNS Resolution

CoreDNS returns the stable ClusterIP; it does not participate in per‑node load‑balancing. DNS caching on the client side is irrelevant to the failure. The failure is purely dataplane‑local: the node’s connection tracking or IPVS scheduler forwards packets to the missing backend, resulting in a silent drop (no TCP RST, just a timeout).

Role of Load Balancers and Networking

If the service is exposed via an external load balancer (e.g., cloud LB with type: LoadBalancer), the LB forwards traffic to any node’s NodePort. The LB health checks may pass because at least one node has the full backend set, masking the per‑node hole. Traffic that lands on the affected node experiences the blackhole.

Resolving the Issue

Updating EndpointSlices and Service State

No direct update is needed; the EndpointSlice is already correct. The fix is to force kube-proxy to reconcile immediately:

# Restart kube-proxy on the affected node to trigger an immediate full sync
kubectl -n kube-system delete pod -l k8s-app=kube-proxy --field-selector spec.nodeName=worker-03

The pod is recreated by the DaemonSet; on startup it performs a full list/watch and programs IPVS with the current slice set.

Adjusting Node‑Local Rendering and Caching

Reduce the sync period to minimize the window of staleness:

# Edit the kube-proxy ConfigMap
kubectl -n kube-system edit configmap kube-proxy

Change:

kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-proxy
  namespace: kube-system
data:
  kubeproxy.conf: |
    ...
    syncPeriod: "5s"
    ...

Apply and roll the DaemonSet:

kubectl -n kube-system rollout restart daemonset kube-proxy

Monitor kubeproxy_sync_latency_seconds to ensure it stays below the new period.

Implementing Monitoring and Alerting

Scaling Limitations and Considerations

Scaling EndpointSlices and Services

Load Balancer and Networking Limitations

Best Practices for Large‑Scale Deployments

Preventing Similar Failures

Implementing Health Checks and Monitoring

Regularly Updating and Verifying EndpointSlices

Using Automation and CI/CD Pipelines

Advanced Troubleshooting Techniques

Using kubectl and kube-proxy Logs


Share this post on:

Previous Post
Neighbor cache GC under 100k endpoint churn
Next Post
Kernel version changes in drop tracing cost