Skip to content

Commit 396c3eb

Browse files
authored
Merge pull request kubernetes#130711 from lauralorenz/crashloopbackoff-featuregate-reducedefaultcrashloopbackoffdelay
KEP-4603: Add ReduceDefaultCrashLoopBackoffDecay featuregate, feature and unit and e2e tests
2 parents 020c4b7 + 256a576 commit 396c3eb

File tree

8 files changed

+204
-32
lines changed

8 files changed

+204
-32
lines changed

pkg/features/kube_features.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,13 @@ const (
565565
// Allows recursive read-only mounts.
566566
RecursiveReadOnlyMounts featuregate.Feature = "RecursiveReadOnlyMounts"
567567

568+
// owner: @lauralorenz
569+
// kep: https://kep.k8s.io/4603
570+
//
571+
// Enables support for a lower internal cluster-wide backoff maximum for restarting
572+
// containers (aka containers in CrashLoopBackOff)
573+
ReduceDefaultCrashLoopBackOffDecay featuregate.Feature = "ReduceDefaultCrashLoopBackOffDecay"
574+
568575
// owner: @adrianmoisey
569576
// kep: https://kep.k8s.io/4427
570577
//

pkg/features/versioned_kube_features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
625625
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.36
626626
},
627627

628+
ReduceDefaultCrashLoopBackOffDecay: {
629+
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
630+
},
631+
628632
RelaxedDNSSearchValidation: {
629633
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
630634
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta},

pkg/kubelet/kubelet.go

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,19 @@ const (
154154
// DefaultContainerLogsDir is the location of container logs.
155155
DefaultContainerLogsDir = "/var/log/containers"
156156

157-
// MaxContainerBackOff is the max backoff period for container restarts, exported for the e2e test
158-
MaxContainerBackOff = v1beta1.MaxContainerBackOff
157+
// MaxCrashLoopBackOff is the max backoff period for container restarts, exported for the e2e test
158+
MaxCrashLoopBackOff = v1beta1.MaxContainerBackOff
159+
160+
// reducedMaxCrashLoopBackOff is the default max backoff period for container restarts when the alpha feature
161+
// gate ReduceDefaultCrashLoopBackOffDecay is enabled
162+
reducedMaxCrashLoopBackOff = 60 * time.Second
163+
164+
// Initial period for the exponential backoff for container restarts.
165+
initialCrashLoopBackOff = time.Second * 10
166+
167+
// reducedInitialCrashLoopBackOff is the default initial backoff period for container restarts when the alpha feature
168+
// gate ReduceDefaultCrashLoopBackOffDecay is enabled
169+
reducedInitialCrashLoopBackOff = 1 * time.Second
159170

160171
// MaxImageBackOff is the max backoff period for image pulls, exported for the e2e test
161172
MaxImageBackOff = 300 * time.Second
@@ -205,9 +216,6 @@ const (
205216
// error.
206217
backOffPeriod = time.Second * 10
207218

208-
// Initial period for the exponential backoff for container restarts.
209-
containerBackOffPeriod = time.Second * 10
210-
211219
// Initial period for the exponential backoff for image pulls.
212220
imageBackOffPeriod = time.Second * 10
213221

@@ -322,6 +330,27 @@ type Dependencies struct {
322330
useLegacyCadvisorStats bool
323331
}
324332

333+
// newCrashLoopBackOff configures the backoff maximum to be used
334+
// by kubelet for container restarts depending on the alpha gates
335+
// and kubelet configuration set
336+
func newCrashLoopBackOff(kubeCfg *kubeletconfiginternal.KubeletConfiguration) (time.Duration, time.Duration) {
337+
boMax := MaxCrashLoopBackOff
338+
boInitial := initialCrashLoopBackOff
339+
if utilfeature.DefaultFeatureGate.Enabled(features.ReduceDefaultCrashLoopBackOffDecay) {
340+
boMax = reducedMaxCrashLoopBackOff
341+
boInitial = reducedInitialCrashLoopBackOff
342+
}
343+
344+
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletCrashLoopBackOffMax) {
345+
// operator-invoked configuration always has precedence if valid
346+
boMax = kubeCfg.CrashLoopBackOff.MaxContainerRestartPeriod.Duration
347+
if boMax < boInitial {
348+
boInitial = boMax
349+
}
350+
}
351+
return boMax, boInitial
352+
}
353+
325354
// makePodSourceConfig creates a config.PodConfig from the given
326355
// KubeletConfiguration or returns an error.
327356
func makePodSourceConfig(kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *Dependencies, nodeName types.NodeName, nodeHasSynced func() bool) (*config.PodConfig, error) {
@@ -939,16 +968,10 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
939968
kubeDeps.Recorder,
940969
volumepathhandler.NewBlockVolumePathHandler())
941970

942-
boMax := MaxContainerBackOff
943-
base := containerBackOffPeriod
944-
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletCrashLoopBackOffMax) {
945-
boMax = kubeCfg.CrashLoopBackOff.MaxContainerRestartPeriod.Duration
946-
if boMax < containerBackOffPeriod {
947-
base = boMax
948-
}
949-
}
950-
klet.backOff = flowcontrol.NewBackOff(base, boMax)
951-
klet.backOff.HasExpiredFunc = func(eventTime time.Time, lastUpdate time.Time, maxDuration time.Duration) bool {
971+
boMax, base := newCrashLoopBackOff(kubeCfg)
972+
973+
klet.crashLoopBackOff = flowcontrol.NewBackOff(base, boMax)
974+
klet.crashLoopBackOff.HasExpiredFunc = func(eventTime time.Time, lastUpdate time.Time, maxDuration time.Duration) bool {
952975
return eventTime.Sub(lastUpdate) > 600*time.Second
953976
}
954977

@@ -1349,7 +1372,7 @@ type Kubelet struct {
13491372
syncLoopMonitor atomic.Value
13501373

13511374
// Container restart Backoff
1352-
backOff *flowcontrol.Backoff
1375+
crashLoopBackOff *flowcontrol.Backoff
13531376

13541377
// Information about the ports which are opened by daemons on Node running this Kubelet server.
13551378
daemonEndpoints *v1.NodeDaemonEndpoints
@@ -2039,7 +2062,7 @@ func (kl *Kubelet) SyncPod(ctx context.Context, updateType kubetypes.SyncPodType
20392062
// Use WithoutCancel instead of a new context.TODO() to propagate trace context
20402063
// Call the container runtime's SyncPod callback
20412064
sctx := context.WithoutCancel(ctx)
2042-
result := kl.containerRuntime.SyncPod(sctx, pod, podStatus, pullSecrets, kl.backOff)
2065+
result := kl.containerRuntime.SyncPod(sctx, pod, podStatus, pullSecrets, kl.crashLoopBackOff)
20432066
kl.reasonCache.Update(pod.UID, result)
20442067
if err := result.Error(); err != nil {
20452068
// Do not return error if the only failures were pods in backoff
@@ -2928,14 +2951,14 @@ func (kl *Kubelet) handlePodResourcesResize(pod *v1.Pod, podStatus *kubecontaine
29282951
for i, container := range pod.Spec.Containers {
29292952
if !apiequality.Semantic.DeepEqual(container.Resources, allocatedPod.Spec.Containers[i].Resources) {
29302953
key := kuberuntime.GetStableKey(pod, &container)
2931-
kl.backOff.Reset(key)
2954+
kl.crashLoopBackOff.Reset(key)
29322955
}
29332956
}
29342957
for i, container := range pod.Spec.InitContainers {
29352958
if podutil.IsRestartableInitContainer(&container) {
29362959
if !apiequality.Semantic.DeepEqual(container.Resources, allocatedPod.Spec.InitContainers[i].Resources) {
29372960
key := kuberuntime.GetStableKey(pod, &container)
2938-
kl.backOff.Reset(key)
2961+
kl.crashLoopBackOff.Reset(key)
29392962
}
29402963
}
29412964
}

pkg/kubelet/kubelet_pods.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1383,7 +1383,7 @@ func (kl *Kubelet) HandlePodCleanups(ctx context.Context) error {
13831383
}
13841384

13851385
// Cleanup any backoff entries.
1386-
kl.backOff.GC()
1386+
kl.crashLoopBackOff.GC()
13871387
return nil
13881388
}
13891389

pkg/kubelet/kubelet_test.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555
"k8s.io/client-go/kubernetes/fake"
5656
"k8s.io/client-go/tools/record"
5757
"k8s.io/client-go/util/flowcontrol"
58+
"k8s.io/component-base/featuregate"
5859
featuregatetesting "k8s.io/component-base/featuregate/testing"
5960
"k8s.io/component-base/metrics/testutil"
6061
internalapi "k8s.io/cri-api/pkg/apis"
@@ -338,8 +339,8 @@ func newTestKubeletWithImageList(
338339
kubelet.containerGC = containerGC
339340

340341
fakeClock := testingclock.NewFakeClock(time.Now())
341-
kubelet.backOff = flowcontrol.NewBackOff(time.Second, time.Minute)
342-
kubelet.backOff.Clock = fakeClock
342+
kubelet.crashLoopBackOff = flowcontrol.NewBackOff(time.Second, time.Minute)
343+
kubelet.crashLoopBackOff.Clock = fakeClock
343344
kubelet.resyncInterval = 10 * time.Second
344345
kubelet.workQueue = queue.NewBasicWorkQueue(fakeClock)
345346
// Relist period does not affect the tests.
@@ -2900,7 +2901,7 @@ func TestHandlePodResourcesResize(t *testing.T) {
29002901
now := kubelet.clock.Now()
29012902
// Put the container in backoff so we can confirm backoff is reset.
29022903
backoffKey := kuberuntime.GetStableKey(originalPod, originalCtr)
2903-
kubelet.backOff.Next(backoffKey, now)
2904+
kubelet.crashLoopBackOff.Next(backoffKey, now)
29042905

29052906
updatedPod, err := kubelet.handlePodResourcesResize(newPod, podStatus)
29062907
require.NoError(t, err)
@@ -2922,7 +2923,7 @@ func TestHandlePodResourcesResize(t *testing.T) {
29222923
resizeStatus := kubelet.statusManager.GetPodResizeStatus(newPod.UID)
29232924
assert.Equal(t, tt.expectedResize, resizeStatus)
29242925

2925-
isInBackoff := kubelet.backOff.IsInBackOffSince(backoffKey, now)
2926+
isInBackoff := kubelet.crashLoopBackOff.IsInBackOffSince(backoffKey, now)
29262927
if tt.expectBackoffReset {
29272928
assert.False(t, isInBackoff, "container backoff should be reset")
29282929
} else {
@@ -3408,7 +3409,7 @@ func TestSyncPodSpans(t *testing.T) {
34083409
kubelet.os,
34093410
kubelet,
34103411
nil,
3411-
kubelet.backOff,
3412+
kubelet.crashLoopBackOff,
34123413
kubeCfg.SerializeImagePulls,
34133414
kubeCfg.MaxParallelImagePulls,
34143415
float32(kubeCfg.RegistryPullQPS),
@@ -3938,3 +3939,86 @@ func TestIsPodResizeInProgress(t *testing.T) {
39383939
})
39393940
}
39403941
}
3942+
3943+
func TestCrashLoopBackOffConfiguration(t *testing.T) {
3944+
testCases := []struct {
3945+
name string
3946+
featureGates []featuregate.Feature
3947+
nodeDecay metav1.Duration
3948+
expectedInitial time.Duration
3949+
expectedMax time.Duration
3950+
}{
3951+
{
3952+
name: "Prior behavior",
3953+
expectedMax: time.Duration(300 * time.Second),
3954+
expectedInitial: time.Duration(10 * time.Second),
3955+
},
3956+
{
3957+
name: "New default only",
3958+
featureGates: []featuregate.Feature{features.ReduceDefaultCrashLoopBackOffDecay},
3959+
expectedMax: time.Duration(60 * time.Second),
3960+
expectedInitial: time.Duration(1 * time.Second),
3961+
},
3962+
{
3963+
name: "Faster per node config; only node config configured",
3964+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax},
3965+
nodeDecay: metav1.Duration{Duration: 2 * time.Second},
3966+
expectedMax: time.Duration(2 * time.Second),
3967+
expectedInitial: time.Duration(2 * time.Second),
3968+
},
3969+
{
3970+
name: "Faster per node config; new default and node config configured",
3971+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax, features.ReduceDefaultCrashLoopBackOffDecay},
3972+
nodeDecay: metav1.Duration{Duration: 2 * time.Second},
3973+
expectedMax: time.Duration(2 * time.Second),
3974+
expectedInitial: time.Duration(1 * time.Second),
3975+
},
3976+
{
3977+
name: "Slower per node config; new default and node config configured, set A",
3978+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax, features.ReduceDefaultCrashLoopBackOffDecay},
3979+
nodeDecay: metav1.Duration{Duration: 10 * time.Second},
3980+
expectedMax: time.Duration(10 * time.Second),
3981+
expectedInitial: time.Duration(1 * time.Second),
3982+
},
3983+
{
3984+
name: "Slower per node config; new default and node config configured, set B",
3985+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax, features.ReduceDefaultCrashLoopBackOffDecay},
3986+
nodeDecay: metav1.Duration{Duration: 300 * time.Second},
3987+
expectedMax: time.Duration(300 * time.Second),
3988+
expectedInitial: time.Duration(1 * time.Second),
3989+
},
3990+
{
3991+
name: "Slower per node config; only node config configured, set A",
3992+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax},
3993+
nodeDecay: metav1.Duration{Duration: 11 * time.Second},
3994+
expectedMax: time.Duration(11 * time.Second),
3995+
expectedInitial: time.Duration(10 * time.Second),
3996+
},
3997+
{
3998+
name: "Slower per node config; only node config configured, set B",
3999+
featureGates: []featuregate.Feature{features.KubeletCrashLoopBackOffMax},
4000+
nodeDecay: metav1.Duration{Duration: 300 * time.Second},
4001+
expectedMax: time.Duration(300 * time.Second),
4002+
expectedInitial: time.Duration(10 * time.Second),
4003+
},
4004+
}
4005+
4006+
for _, tc := range testCases {
4007+
t.Run(tc.name, func(t *testing.T) {
4008+
kubeCfg := &kubeletconfiginternal.KubeletConfiguration{}
4009+
4010+
for _, f := range tc.featureGates {
4011+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)
4012+
}
4013+
if tc.nodeDecay.Duration > 0 {
4014+
kubeCfg.CrashLoopBackOff.MaxContainerRestartPeriod = &tc.nodeDecay
4015+
}
4016+
4017+
resultMax, resultInitial := newCrashLoopBackOff(kubeCfg)
4018+
4019+
assert.Equalf(t, tc.expectedMax, resultMax, "wrong max calculated, want: %v, got %v", tc.expectedMax, resultMax)
4020+
assert.Equalf(t, tc.expectedInitial, resultInitial, "wrong base calculated, want: %v, got %v", tc.expectedInitial, resultInitial)
4021+
})
4022+
}
4023+
4024+
}

test/e2e/common/node/pods.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import (
6464
const (
6565
buildBackOffDuration = time.Minute
6666
syncLoopFrequency = 10 * time.Second
67-
maxBackOffTolerance = time.Duration(1.3 * float64(kubelet.MaxContainerBackOff))
67+
maxBackOffTolerance = time.Duration(1.3 * float64(kubelet.MaxCrashLoopBackOff))
6868
podRetryPeriod = 1 * time.Second
6969
)
7070

@@ -739,7 +739,7 @@ var _ = SIGDescribe("Pods", func() {
739739
})
740740

741741
podClient.CreateSync(ctx, pod)
742-
time.Sleep(2 * kubelet.MaxContainerBackOff) // it takes slightly more than 2*x to get to a back-off of x
742+
time.Sleep(2 * kubelet.MaxCrashLoopBackOff) // it takes slightly more than 2*x to get to a back-off of x
743743

744744
// wait for a delay == capped delay of MaxContainerBackOff
745745
ginkgo.By("getting restart delay when capped")
@@ -753,13 +753,13 @@ var _ = SIGDescribe("Pods", func() {
753753
framework.Failf("timed out waiting for container restart in pod=%s/%s", podName, containerName)
754754
}
755755

756-
if delay1 < kubelet.MaxContainerBackOff {
756+
if delay1 < kubelet.MaxCrashLoopBackOff {
757757
continue
758758
}
759759
}
760760

761-
if (delay1 < kubelet.MaxContainerBackOff) || (delay1 > maxBackOffTolerance) {
762-
framework.Failf("expected %s back-off got=%s in delay1", kubelet.MaxContainerBackOff, delay1)
761+
if (delay1 < kubelet.MaxCrashLoopBackOff) || (delay1 > maxBackOffTolerance) {
762+
framework.Failf("expected %s back-off got=%s in delay1", kubelet.MaxCrashLoopBackOff, delay1)
763763
}
764764

765765
ginkgo.By("getting restart delay after a capped delay")
@@ -768,8 +768,8 @@ var _ = SIGDescribe("Pods", func() {
768768
framework.Failf("timed out waiting for container restart in pod=%s/%s", podName, containerName)
769769
}
770770

771-
if delay2 < kubelet.MaxContainerBackOff || delay2 > maxBackOffTolerance { // syncloop cumulative drift
772-
framework.Failf("expected %s back-off got=%s on delay2", kubelet.MaxContainerBackOff, delay2)
771+
if delay2 < kubelet.MaxCrashLoopBackOff || delay2 > maxBackOffTolerance { // syncloop cumulative drift
772+
framework.Failf("expected %s back-off got=%s on delay2", kubelet.MaxCrashLoopBackOff, delay2)
773773
}
774774
})
775775

test/e2e_node/container_restart_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,54 @@ var _ = SIGDescribe("Container Restart", feature.CriProxy, framework.WithSerial(
8585
doTest(ctx, f, 3, containerName, 13)
8686
})
8787
})
88+
89+
ginkgo.Context("Reduced default container restart backs off as expected", func() {
90+
91+
tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) {
92+
initialConfig.FeatureGates = map[string]bool{"ReduceDefaultCrashLoopBackOffDecay": true}
93+
})
94+
95+
ginkgo.BeforeEach(func() {
96+
if err := resetCRIProxyInjector(e2eCriProxy); err != nil {
97+
ginkgo.Skip("Skip the test since the CRI Proxy is undefined.")
98+
}
99+
})
100+
101+
ginkgo.AfterEach(func() {
102+
err := resetCRIProxyInjector(e2eCriProxy)
103+
framework.ExpectNoError(err)
104+
})
105+
106+
ginkgo.It("Reduced default restart backs off.", func(ctx context.Context) {
107+
// 0s, 0s, 10s, 30s, 60s, 90s, 120s, 150s, 180s, 210s, 240s, 270s, 300s
108+
doTest(ctx, f, 3, containerName, 13)
109+
})
110+
})
111+
112+
ginkgo.Context("Lower node config container restart takes precedence", func() {
113+
114+
tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) {
115+
initialConfig.FeatureGates = map[string]bool{"ReduceDefaultCrashLoopBackOffDecay": true}
116+
initialConfig.CrashLoopBackOff.MaxContainerRestartPeriod = &metav1.Duration{Duration: time.Duration(1 * time.Second)}
117+
initialConfig.FeatureGates = map[string]bool{"KubeletCrashLoopBackOffMax": true}
118+
})
119+
120+
ginkgo.BeforeEach(func() {
121+
if err := resetCRIProxyInjector(e2eCriProxy); err != nil {
122+
ginkgo.Skip("Skip the test since the CRI Proxy is undefined.")
123+
}
124+
})
125+
126+
ginkgo.AfterEach(func() {
127+
err := resetCRIProxyInjector(e2eCriProxy)
128+
framework.ExpectNoError(err)
129+
})
130+
131+
ginkgo.It("Reduced default restart backs off.", func(ctx context.Context) {
132+
// 0s, 0s, 1s, 2s, 3s, 4s, 5s, 6s, 7s, and so on
133+
doTest(ctx, f, 3, containerName, 298)
134+
})
135+
})
88136
})
89137

90138
func doTest(ctx context.Context, f *framework.Framework, targetRestarts int, containerName string, maxRestarts int) {

test/featuregates_linter/test_data/versioned_feature_list.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,12 @@
10601060
lockToDefault: true
10611061
preRelease: GA
10621062
version: "1.33"
1063+
- name: ReduceDefaultCrashLoopBackOffDecay
1064+
versionedSpecs:
1065+
- default: false
1066+
lockToDefault: false
1067+
preRelease: Alpha
1068+
version: "1.33"
10631069
- name: RelaxedDNSSearchValidation
10641070
versionedSpecs:
10651071
- default: false

0 commit comments

Comments
 (0)