Skip to content

Commit 7fbf63a

Browse files
authored
HPA support for pod-level resource specifications (kubernetes#132430)
* HPA support for pod-level resource specifications * Add e2e tests for HPA support for pod-level resource specifications
1 parent e2ab840 commit 7fbf63a

File tree

8 files changed

+337
-64
lines changed

8 files changed

+337
-64
lines changed

pkg/controller/podautoscaler/replica_calculator.go

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ import (
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/labels"
2929
"k8s.io/apimachinery/pkg/util/sets"
30+
"k8s.io/apiserver/pkg/util/feature"
3031
corelisters "k8s.io/client-go/listers/core/v1"
32+
resourcehelpers "k8s.io/component-helpers/resource"
3133
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
3234
metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
35+
"k8s.io/kubernetes/pkg/features"
3336
)
3437

3538
const (
@@ -94,7 +97,7 @@ func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentRepl
9497
return 0, 0, 0, time.Time{}, fmt.Errorf("did not receive metrics for targeted pods (pods might be unready)")
9598
}
9699

97-
requests, err := calculatePodRequests(podList, container, resource)
100+
requests, err := calculateRequests(podList, container, resource)
98101
if err != nil {
99102
return 0, 0, 0, time.Time{}, err
100103
}
@@ -449,31 +452,76 @@ func groupPods(pods []*v1.Pod, metrics metricsclient.PodMetricsInfo, resource v1
449452
return
450453
}
451454

452-
func calculatePodRequests(pods []*v1.Pod, container string, resource v1.ResourceName) (map[string]int64, error) {
455+
// calculateRequests computes the request value for each pod for the specified
456+
// resource.
457+
// If container is non-empty, it uses the request of that specific container.
458+
// If container is empty, it uses pod-level requests if pod-level requests are
459+
// set on the pod. Otherwise, it sums the requests of all containers in the pod
460+
// (including restartable init containers).
461+
// It returns a map of pod names to their calculated request values.
462+
func calculateRequests(pods []*v1.Pod, container string, resource v1.ResourceName) (map[string]int64, error) {
463+
podLevelResourcesEnabled := feature.DefaultFeatureGate.Enabled(features.PodLevelResources)
453464
requests := make(map[string]int64, len(pods))
454465
for _, pod := range pods {
455-
podSum := int64(0)
456-
// Calculate all regular containers and restartable init containers requests.
457-
containers := append([]v1.Container{}, pod.Spec.Containers...)
458-
for _, c := range pod.Spec.InitContainers {
459-
if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways {
460-
containers = append(containers, c)
461-
}
466+
var request int64
467+
var err error
468+
// Determine if we should use pod-level requests: see KEP-2837
469+
// https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2837-pod-level-resource-spec/README.md
470+
usePodLevelRequests := podLevelResourcesEnabled &&
471+
resourcehelpers.IsPodLevelRequestsSet(pod) &&
472+
// If a container name is specified in the HPA, it takes precedence over
473+
// the pod-level requests.
474+
container == ""
475+
476+
if usePodLevelRequests {
477+
request, err = calculatePodLevelRequests(pod, resource)
478+
} else {
479+
request, err = calculatePodRequestsFromContainers(pod, container, resource)
462480
}
463-
for _, c := range containers {
464-
if container == "" || container == c.Name {
465-
if containerRequest, ok := c.Resources.Requests[resource]; ok {
466-
podSum += containerRequest.MilliValue()
467-
} else {
468-
return nil, fmt.Errorf("missing request for %s in container %s of Pod %s", resource, c.Name, pod.ObjectMeta.Name)
469-
}
470-
}
481+
if err != nil {
482+
return nil, err
471483
}
472-
requests[pod.Name] = podSum
484+
requests[pod.Name] = request
473485
}
474486
return requests, nil
475487
}
476488

489+
// calculatePodLevelRequests computes the requests for the specific resource at
490+
// the pod level.
491+
func calculatePodLevelRequests(pod *v1.Pod, resource v1.ResourceName) (int64, error) {
492+
podLevelRequests := resourcehelpers.PodRequests(pod, resourcehelpers.PodResourcesOptions{})
493+
podRequest, ok := podLevelRequests[resource]
494+
if !ok {
495+
return 0, fmt.Errorf("missing pod-level request for %s in Pod %s", resource, pod.Name)
496+
}
497+
return podRequest.MilliValue(), nil
498+
}
499+
500+
// calculatePodRequestsFromContainers computes the requests for the specified
501+
// resource by summing requests from all containers in the pod.
502+
// If a container name is specified, it uses only that container.
503+
func calculatePodRequestsFromContainers(pod *v1.Pod, container string, resource v1.ResourceName) (int64, error) {
504+
// Calculate all regular containers and restartable init containers requests.
505+
containers := append([]v1.Container{}, pod.Spec.Containers...)
506+
for _, c := range pod.Spec.InitContainers {
507+
if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways {
508+
containers = append(containers, c)
509+
}
510+
}
511+
512+
request := int64(0)
513+
for _, c := range containers {
514+
if container == "" || container == c.Name {
515+
containerRequest, ok := c.Resources.Requests[resource]
516+
if !ok {
517+
return 0, fmt.Errorf("missing request for %s in container %s of Pod %s", resource, c.Name, pod.Name)
518+
}
519+
request += containerRequest.MilliValue()
520+
}
521+
}
522+
return request, nil
523+
}
524+
477525
func removeMetricsForPods(metrics metricsclient.PodMetricsInfo, pods sets.Set[string]) {
478526
for _, pod := range pods.UnsortedList() {
479527
delete(metrics, pod)

pkg/controller/podautoscaler/replica_calculator_test.go

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ import (
3131
"k8s.io/apimachinery/pkg/runtime"
3232
"k8s.io/apimachinery/pkg/runtime/schema"
3333
"k8s.io/apimachinery/pkg/util/sets"
34+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3435
"k8s.io/client-go/informers"
3536
"k8s.io/client-go/kubernetes/fake"
3637
core "k8s.io/client-go/testing"
3738
"k8s.io/client-go/tools/cache"
39+
featuregatetesting "k8s.io/component-base/featuregate/testing"
3840
"k8s.io/kubernetes/pkg/api/legacyscheme"
3941
"k8s.io/kubernetes/pkg/controller"
4042
metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
43+
"k8s.io/kubernetes/pkg/features"
4144
cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
4245
emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
4346
metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
@@ -2225,17 +2228,18 @@ func TestGroupPods(t *testing.T) {
22252228
}
22262229
}
22272230

2228-
func TestCalculatePodRequests(t *testing.T) {
2231+
func TestCalculateRequests(t *testing.T) {
22292232
containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
22302233
testPod := "test-pod"
22312234

22322235
tests := []struct {
2233-
name string
2234-
pods []*v1.Pod
2235-
container string
2236-
resource v1.ResourceName
2237-
expectedRequests map[string]int64
2238-
expectedError error
2236+
name string
2237+
pods []*v1.Pod
2238+
container string
2239+
resource v1.ResourceName
2240+
enablePodLevelResources bool
2241+
expectedRequests map[string]int64
2242+
expectedError error
22392243
}{
22402244
{
22412245
name: "void",
@@ -2246,7 +2250,7 @@ func TestCalculatePodRequests(t *testing.T) {
22462250
expectedError: nil,
22472251
},
22482252
{
2249-
name: "pod with regular containers",
2253+
name: "Sum container requests if pod-level feature is disabled",
22502254
pods: []*v1.Pod{{
22512255
ObjectMeta: metav1.ObjectMeta{
22522256
Name: testPod,
@@ -2265,7 +2269,8 @@ func TestCalculatePodRequests(t *testing.T) {
22652269
expectedError: nil,
22662270
},
22672271
{
2268-
name: "calculate requests with special container",
2272+
name: "Pod-level resources are enabled, but not set: fallback to sum container requests",
2273+
enablePodLevelResources: true,
22692274
pods: []*v1.Pod{{
22702275
ObjectMeta: metav1.ObjectMeta{
22712276
Name: testPod,
@@ -2278,13 +2283,37 @@ func TestCalculatePodRequests(t *testing.T) {
22782283
},
22792284
},
22802285
}},
2281-
container: "container1",
2286+
container: "",
22822287
resource: v1.ResourceCPU,
2283-
expectedRequests: map[string]int64{testPod: 100},
2288+
expectedRequests: map[string]int64{testPod: 150},
2289+
expectedError: nil,
2290+
},
2291+
{
2292+
name: "Pod-level resources override container requests when feature enabled and pod resources specified",
2293+
enablePodLevelResources: true,
2294+
pods: []*v1.Pod{{
2295+
2296+
ObjectMeta: metav1.ObjectMeta{
2297+
Name: testPod,
2298+
Namespace: testNamespace,
2299+
},
2300+
Spec: v1.PodSpec{
2301+
Resources: &v1.ResourceRequirements{
2302+
Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(800, resource.DecimalSI)},
2303+
},
2304+
Containers: []v1.Container{
2305+
{Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2306+
{Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2307+
},
2308+
},
2309+
}},
2310+
container: "",
2311+
resource: v1.ResourceCPU,
2312+
expectedRequests: map[string]int64{testPod: 800},
22842313
expectedError: nil,
22852314
},
22862315
{
2287-
name: "container missing requests",
2316+
name: "Fail if at least one of the containers is missing requests and pod-level feature/requests are not set",
22882317
pods: []*v1.Pod{{
22892318
ObjectMeta: metav1.ObjectMeta{
22902319
Name: testPod,
@@ -2293,6 +2322,7 @@ func TestCalculatePodRequests(t *testing.T) {
22932322
Spec: v1.PodSpec{
22942323
Containers: []v1.Container{
22952324
{Name: "container1"},
2325+
{Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
22962326
},
22972327
},
22982328
}},
@@ -2301,6 +2331,71 @@ func TestCalculatePodRequests(t *testing.T) {
23012331
expectedRequests: nil,
23022332
expectedError: fmt.Errorf("missing request for %s in container %s of Pod %s", v1.ResourceCPU, "container1", testPod),
23032333
},
2334+
{
2335+
name: "Pod-level resources override missing container requests when feature enabled and pod resources specified",
2336+
enablePodLevelResources: true,
2337+
pods: []*v1.Pod{{
2338+
ObjectMeta: metav1.ObjectMeta{
2339+
Name: testPod,
2340+
Namespace: testNamespace,
2341+
},
2342+
Spec: v1.PodSpec{
2343+
Resources: &v1.ResourceRequirements{
2344+
Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(800, resource.DecimalSI)},
2345+
},
2346+
Containers: []v1.Container{
2347+
{Name: "container1"},
2348+
{Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2349+
},
2350+
},
2351+
}},
2352+
container: "",
2353+
resource: v1.ResourceCPU,
2354+
expectedRequests: map[string]int64{testPod: 800},
2355+
expectedError: nil,
2356+
},
2357+
{
2358+
name: "Container: if a container name is specified, calculate requests only for that container",
2359+
pods: []*v1.Pod{{
2360+
ObjectMeta: metav1.ObjectMeta{
2361+
Name: testPod,
2362+
Namespace: testNamespace,
2363+
},
2364+
Spec: v1.PodSpec{
2365+
Containers: []v1.Container{
2366+
{Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2367+
{Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2368+
},
2369+
},
2370+
}},
2371+
container: "container1",
2372+
resource: v1.ResourceCPU,
2373+
expectedRequests: map[string]int64{testPod: 100},
2374+
expectedError: nil,
2375+
},
2376+
{
2377+
name: "Container: if a container name is specified, calculate requests only for that container and ignore pod-level requests",
2378+
enablePodLevelResources: true,
2379+
pods: []*v1.Pod{{
2380+
ObjectMeta: metav1.ObjectMeta{
2381+
Name: testPod,
2382+
Namespace: testNamespace,
2383+
},
2384+
Spec: v1.PodSpec{
2385+
Resources: &v1.ResourceRequirements{
2386+
Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(800, resource.DecimalSI)},
2387+
},
2388+
Containers: []v1.Container{
2389+
{Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2390+
{Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2391+
},
2392+
},
2393+
}},
2394+
container: "container1",
2395+
resource: v1.ResourceCPU,
2396+
expectedRequests: map[string]int64{testPod: 100},
2397+
expectedError: nil,
2398+
},
23042399
{
23052400
name: "pod with restartable init containers",
23062401
pods: []*v1.Pod{{
@@ -2327,7 +2422,9 @@ func TestCalculatePodRequests(t *testing.T) {
23272422

23282423
for _, tc := range tests {
23292424
t.Run(tc.name, func(t *testing.T) {
2330-
requests, err := calculatePodRequests(tc.pods, tc.container, tc.resource)
2425+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResources, tc.enablePodLevelResources)
2426+
2427+
requests, err := calculateRequests(tc.pods, tc.container, tc.resource)
23312428
assert.Equal(t, tc.expectedRequests, requests, "requests should be as expected")
23322429
assert.Equal(t, tc.expectedError, err, "error should be as expected")
23332430
})

test/e2e/autoscaling/autoscaling_timer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ var _ = SIGDescribe(feature.ClusterSizeAutoscalingScaleUp, framework.WithSlow(),
8686
nodeMemoryMB := (&nodeMemoryBytes).Value() / 1024 / 1024
8787
memRequestMB := nodeMemoryMB / 10 // Ensure each pod takes not more than 10% of node's allocatable memory.
8888
replicas := 1
89-
resourceConsumer := e2eautoscaling.NewDynamicResourceConsumer(ctx, "resource-consumer", f.Namespace.Name, e2eautoscaling.KindDeployment, replicas, 0, 0, 0, cpuRequestMillis, memRequestMB, f.ClientSet, f.ScalesGetter, e2eautoscaling.Disable, e2eautoscaling.Idle)
89+
resourceConsumer := e2eautoscaling.NewDynamicResourceConsumer(ctx, "resource-consumer", f.Namespace.Name, e2eautoscaling.KindDeployment, replicas, 0, 0, 0, cpuRequestMillis, memRequestMB, f.ClientSet, f.ScalesGetter, e2eautoscaling.Disable, e2eautoscaling.Idle, nil)
9090
ginkgo.DeferCleanup(resourceConsumer.CleanUp)
9191
resourceConsumer.WaitForReplicas(ctx, replicas, 1*time.Minute) // Should finish ~immediately, so 1 minute is more than enough.
9292

0 commit comments

Comments
 (0)