diff --git a/src/runtime/pkg/oci/utils.go b/src/runtime/pkg/oci/utils.go index 229f065740..d527ce4ef1 100644 --- a/src/runtime/pkg/oci/utils.go +++ b/src/runtime/pkg/oci/utils.go @@ -1513,9 +1513,16 @@ func CalculateSandboxSizing(spec *specs.Spec) (numCPU float32, memSizeMB uint32) numCPU, memSizeMB = calculateVMResources(period, quota, memory) // When cpuManagerPolicy=static is in use, kubelet sets quota=-1 - // (unconstrained) and assigns CPUs via cpuset instead. Fall back - // to deriving the CPU count from shares (1024 shares per CPU). - if numCPU == 0 && shares > 0 { + // (unconstrained) and assigns CPUs via cpuset instead. In that case + // we derive the CPU count from the CPU shares (1024 shares per CPU). + // + // We must gate this on quota being explicitly unconstrained (< 0) + // rather than on numCPU == 0: a quota of 0 (or absent) means a + // BestEffort sandbox with no CPU request, which has to contribute 0 + // vCPUs. Such a sandbox still carries the cgroup-floor shares value + // (2), and deriving from it would inflate every sandbox by one vCPU + // (e.g. peer-pods would boot default_vcpus+1). + if quota < 0 && numCPU == 0 && shares > 0 { numCPU = float32(math.Ceil(float64(shares) / 1024.0)) } diff --git a/src/runtime/pkg/oci/utils_test.go b/src/runtime/pkg/oci/utils_test.go index 9046e93eb7..a746258f5f 100644 --- a/src/runtime/pkg/oci/utils_test.go +++ b/src/runtime/pkg/oci/utils_test.go @@ -1263,6 +1263,13 @@ func makeSizingAnnotations(memory, quota, period string) *specs.Spec { return &spec } +func makeSizingAnnotationsWithShares(memory, quota, period, shares string) *specs.Spec { + spec := makeSizingAnnotations(memory, quota, period) + spec.Annotations[ctrAnnotations.SandboxCPUShares] = shares + + return spec +} + func TestCalculateContainerSizing(t *testing.T) { assert := assert.New(t) @@ -1376,6 +1383,42 @@ func TestCalculateSandboxSizing(t *testing.T) { expectedCPU: 4, expectedMem: 4096, }, + // cpuManagerPolicy=static: kubelet leaves the quota + // unconstrained (-1) and pins CPUs via cpuset, so the CPU + // count must be derived from the shares (1024 shares per CPU). + { + spec: makeSizingAnnotationsWithShares("1048576", "-1", "100", "2048"), + expectedCPU: 2, + expectedMem: 1, + }, + // Shares that don't divide evenly are rounded up. + { + spec: makeSizingAnnotationsWithShares("0", "-1", "100", "1536"), + expectedCPU: 2, + expectedMem: 0, + }, + // BestEffort sandbox: no CPU request means quota is 0/absent, + // but the cgroup still carries the floor shares value (2). This + // must contribute 0 vCPUs, otherwise every sandbox (e.g. a + // peer-pod) would be inflated by one vCPU. + { + spec: makeSizingAnnotationsWithShares("0", "0", "100", "2"), + expectedCPU: 0, + expectedMem: 0, + }, + // An explicit quota always wins over shares: the shares-based + // fallback only applies when the quota is unconstrained. + { + spec: makeSizingAnnotationsWithShares("0", "200", "100", "8192"), + expectedCPU: 2, + expectedMem: 0, + }, + // Unconstrained quota but no shares set: nothing to derive from. + { + spec: makeSizingAnnotationsWithShares("0", "-1", "100", "0"), + expectedCPU: 0, + expectedMem: 0, + }, } for _, tt := range testCases {