kindnetd: WaitForCacheSync before reconcile loop to avoid 10s NodeReady lag - #4216
kindnetd: WaitForCacheSync before reconcile loop to avoid 10s NodeReady lag#4216k-simons wants to merge 5 commits into
Conversation
|
Welcome @k-simons! |
|
Hi @k-simons. Thanks for your PR. I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
|
/cc @aojea @BenTheElder |
|
/ok-to-test |
|
@stmcginnis Thanks for the tag! Please let me know if there is any more information or details that you need |
|
This does make sense to wait up front rather than getting into the potentially longer retry loop right away. Thanks for looking into this. /lgtm |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: k-simons The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@stmcginnis I went ahead and added the timeout per the recommendation. Let me know if there are any other comments/changes you'd like to see! |
Thanks @k-simons, that looks good to me. @BenTheElder any concerns with this? /lgtm |
|
@stmcginnis / @aojea Anything I can do here? Or is this pr just in the review process? |
|
@stmcginnis / @aojea Just wanted to check in here :) |
| syncCtx, syncCancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer syncCancel() | ||
| if !cache.WaitForCacheSync(syncCtx.Done(), nodeInformer.Informer().HasSynced) { | ||
| klog.Fatalf("failed to sync node informer cache") | ||
| } |
There was a problem hiding this comment.
what erroring and crashing gives us here? It seems we should follow same convention as all controllers and wait forever or until the context is cancelled
| syncCtx, syncCancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer syncCancel() |
There was a problem hiding this comment.
just remove this context and pass ctx.Done() and we are good to go
There was a problem hiding this comment.
On vacation atm but can land it when I get back; or if for some reason you'd like to add it feel free!
Problem
On fresh kind clusters, node
NodeReady=trueflips ~10 seconds later than necessary.During those 10 seconds, kubelet's runtime status stays at
KubeletNotReadywith reasonNetworkPluginNotReadyeven though CNI configuration and kindnet's control loop are readyto run.
Root cause
images/kindnetd/cmd/kindnetd/main.gostarts informers but never waits for their cache tosync before entering the reconcile ticker loop:
The informer's initial LIST typically completes within a few hundred milliseconds of
Start, but the first loop iteration runs synchronously right afterStartreturns —before the LIST response has landed.
nodeLister.List(...)returns an empty slice,reconcileNodesfinds nothing to do, and the loop blocks on the 10-second ticker beforetrying again. The first meaningful reconcile — the one that writes
/etc/cni/net.d/10-kindnet.conflistand logsHandling node with IPs— is thus gatedbehind the ticker rather than behind the (fast) cache-sync.
Fix
Wait for the node informer cache to sync before entering the reconcile loop.
import ( ... "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" )// main control loop informersFactory.Start(ctx.Done()) + // Wait for the node cache to populate before entering the reconcile + // loop. Without this, the first iteration lists an empty cache and + // blocks on the 10s ticker before doing any real work — delaying + // NodeReady by a full tick. + if !cache.WaitForCacheSync(ctx.Done(), nodeInformer.Informer().HasSynced) { + klog.Fatalf("failed to sync node informer cache") + } ticker := time.NewTicker(10 * time.Second) defer ticker.Stop()Evidence
Kindnetd container logs on a fresh single-node kind cluster (v1.33.7, arm64). Same host,
same node image, same cluster — only the kindnetd binary differed between runs.
Before:
After (patched binary):
Reduction in kindnetd's own timeline in the observed environment: ~9.5 seconds. The
theoretical maximum savings on any bring-up is one full reconcile ticker period (10s).
Downstream impact (motivation)
Consumers of kind that fan out CNI-dependent workloads during cluster bring-up
(cert-manager, service meshes, trust-bundle mounts) frequently observe pods stuck in
ContainerCreatingwithFailedMountevents, retrying against kubelet'snestedpendingoperationsexponential backoff (500ms → 1s → 2s → 4s → 8s → 16s → ...). The10s NodeReady lag pushes these pods into the 8-16s backoff tier before the resources they
depend on become available; removing the lag lets those pods succeed on their first mount
attempt.
Risk
WaitForCacheSyncreturns false only on ctx cancellation.klog.Fatalffor the sync-fail branch matches the idiom used acrosskubelet/controller-manager and flushes klog buffers before exiting; the existing
paniconline 286 could be aligned separately if desired.
only affects the first iteration after startup.
continue to sync in the background.
kube-network-policiesalready does its ownWaitForCacheSyncfor what it needs. Happy to extend the sync-wait to all four informersif reviewers prefer uniformity.
Testing
docker.io/kindest/kindnetd:v20251212-v0.29.0-alpha-105-g20ccfc88, retagging inside thekind node's containerd, restarting the DaemonSet pod, and comparing pre-/post-patch log
timestamps for
Handling node with IPs(see logs above).WaitForCacheSyncfollowing
informersFactory.Start); regression risk is bounded to the first-iterationordering.
This PR was drafted with AI assistance; code, logs, and testing are my own.