Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion vertical-pod-autoscaler/docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This document is auto-generated from the flag definitions in the VPA admission-c
| `address` | string | ":8944" | The address to expose Prometheus metrics. |
| `alsologtostderr` | | | log to standard error as well as files (no effect when -logtostderr=true) |
| `client-ca-file` | string | "/etc/tls-certs/caCert.pem" | Path to CA PEM file. |
| `container-recommendation-max-allowed-cpu` | string | | Maximum amount of CPU that will be recommended for a container. |
| `feature-gates` | mapStringBool | | A set of key=value pairs that describe feature gates for alpha/experimental features. Options are:<br>AllAlpha=true\|false (ALPHA - default=false)<br>AllBeta=true\|false (BETA - default=false)<br>CPUStartupBoost=true\|false (ALPHA - default=false)<br>InPlaceOrRecreate=true\|false (ALPHA - default=false) |
| `ignored-vpa-object-namespaces` | string | | A comma-separated list of namespaces to ignore when searching for VPA objects. Leave empty to avoid ignoring any namespaces. These namespaces will not be cleaned by the garbage collector. |
| `kube-api-burst` | float | 100 | QPS burst limit when making requests to Kubernetes apiserver |
Expand All @@ -37,7 +38,7 @@ This document is auto-generated from the flag definitions in the VPA admission-c
| `tls-cert-file` | string | "/etc/tls-certs/serverCert.pem" | Path to server certificate PEM file. |
| `tls-ciphers` | string | | A comma-separated or colon-separated list of ciphers to accept. Only works when min-tls-version is set to tls1_2. |
| `tls-private-key` | string | "/etc/tls-certs/serverKey.pem" | Path to server certificate key PEM file. |
| `v,` | | : 4 | , --v Level set the log level verbosity (default 4) |
| `v,` | | : 4 | , --v Level set the log level verbosity (default 4) |
| `vmodule` | moduleSpec | | comma-separated list of pattern=N settings for file-filtered logging |
| `vpa-object-namespace` | string | | Specifies the namespace to search for VPA objects. Leave empty to include all namespaces. If provided, the garbage collector will only clean this namespace. |
| `webhook-address` | string | | Address under which webhook is registered. Used when registerByURL is set to true. |
Expand Down
11 changes: 10 additions & 1 deletion vertical-pod-autoscaler/pkg/admission-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/informers"
kube_client "k8s.io/client-go/kubernetes"
typedadmregv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
Expand Down Expand Up @@ -78,6 +79,7 @@ var (
registerWebhook = flag.Bool("register-webhook", true, "If set to true, admission webhook object will be created on start up to register with the API server.")
webhookLabels = flag.String("webhook-labels", "", "Comma separated list of labels to add to the webhook object. Format: key1:value1,key2:value2")
registerByURL = flag.Bool("register-by-url", false, "If set to true, admission webhook will be registered by URL (webhookAddress:webhookPort) instead of by service name")
maxAllowedCPU = flag.String("container-recommendation-max-allowed-cpu", "", "Maximum amount of CPU that will be recommended for a container.")
)

func main() {
Expand All @@ -93,6 +95,13 @@ func main() {
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}

if *maxAllowedCPU != "" {
if _, err := resource.ParseQuantity(*maxAllowedCPU); err != nil {
klog.ErrorS(err, "Failed to parse maxAllowedCPU")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
}

healthCheck := metrics.NewHealthCheck(time.Minute)
metrics_admission.Register()
server.Initialize(&commonFlags.EnableProfiling, healthCheck, address)
Expand Down Expand Up @@ -145,7 +154,7 @@ func main() {
hostname,
)

calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider), patch.NewObservedContainersCalculator()}
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider, *maxAllowedCPU), patch.NewObservedContainersCalculator()}
as := logic.NewAdmissionServer(podPreprocessor, vpaPreprocessor, limitRangeCalculator, vpaMatcher, calculators)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
as.Serve(w, r)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ import (
"strings"

core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/klog/v2"

resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
resourcehelpers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/resources"
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
)
Expand All @@ -37,13 +41,19 @@ const (

type resourcesUpdatesPatchCalculator struct {
recommendationProvider recommendation.Provider
maxAllowedCPU resource.Quantity
}

// NewResourceUpdatesCalculator returns a calculator for
// resource update patches.
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider) Calculator {
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider, maxAllowedCPU string) Calculator {
var maxAllowedCPUQuantity resource.Quantity
if maxAllowedCPU != "" {
maxAllowedCPUQuantity = resource.MustParse(maxAllowedCPU)
}
return &resourcesUpdatesPatchCalculator{
recommendationProvider: recommendationProvider,
maxAllowedCPU: maxAllowedCPUQuantity,
}
}

Expand All @@ -52,11 +62,22 @@ func (*resourcesUpdatesPatchCalculator) PatchResourceTarget() PatchResourceTarge
}

func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
klog.Infof("Calculating patches for pod %s/%s with VPA %s", pod.Namespace, pod.Name, vpa.Name)
result := []resource_admission.PatchRecord{}

containersResources, annotationsPerContainer, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
if err != nil {
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
return nil, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
}

if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
// If update mode is "Off", we don't want to apply any recommendations,
// but we still want to apply startup boost.
for i := range containersResources {
containersResources[i].Requests = nil
containersResources[i].Limits = nil
}
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
}

if annotationsPerContainer == nil {
Expand All @@ -65,9 +86,65 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v

updatesAnnotation := []string{}
for i, containerResources := range containersResources {
// Apply startup boost if configured
if features.Enabled(features.CPUStartupBoost) {
policy := vpa_api_util.GetContainerResourcePolicy(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
if policy != nil && policy.Mode != nil && *policy.Mode == vpa_types.ContainerScalingModeOff {
klog.V(4).InfoS("Not applying startup boost for container", "containerName", pod.Spec.Containers[i].Name, "reason", "scaling mode is Off")
continue
} else {
startupBoostPolicy := getContainerStartupBoostPolicy(&pod.Spec.Containers[i], vpa)
if startupBoostPolicy != nil {
originalRequest := pod.Spec.Containers[i].Resources.Requests[core.ResourceCPU]
boostedRequest, err := calculateBoostedCPU(originalRequest, startupBoostPolicy)
if err != nil {
return nil, err
}

if !c.maxAllowedCPU.IsZero() && boostedRequest.Cmp(c.maxAllowedCPU) > 0 {
boostedRequest = &c.maxAllowedCPU
}
if containerResources.Requests == nil {
containerResources.Requests = core.ResourceList{}
}
controlledValues := vpa_api_util.GetContainerControlledValues(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
resourceList := core.ResourceList{core.ResourceCPU: *boostedRequest}
if controlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
vpa_api_util.CapRecommendationToContainerLimit(resourceList, pod.Spec.Containers[i].Resources.Limits)
}
containerResources.Requests[core.ResourceCPU] = resourceList[core.ResourceCPU]

if controlledValues == vpa_types.ContainerControlledValuesRequestsAndLimits {
if containerResources.Limits == nil {
containerResources.Limits = core.ResourceList{}
}
originalLimit := pod.Spec.Containers[i].Resources.Limits[core.ResourceCPU]
if originalLimit.IsZero() {
originalLimit = pod.Spec.Containers[i].Resources.Requests[core.ResourceCPU]
}
boostedLimit, err := calculateBoostedCPU(originalLimit, startupBoostPolicy)
if err != nil {
return nil, err
}
if !c.maxAllowedCPU.IsZero() && boostedLimit.Cmp(c.maxAllowedCPU) > 0 {
boostedLimit = &c.maxAllowedCPU
}
containerResources.Limits[core.ResourceCPU] = *boostedLimit
}
originalResources, err := annotations.GetOriginalResourcesAnnotationValue(&pod.Spec.Containers[i])
if err != nil {
return nil, err
}
result = append(result, GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, originalResources))
}
}
}

newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containerResources)
result = append(result, newPatches...)
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
if len(newPatches) > 0 {
result = append(result, newPatches...)
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
}
}

if len(updatesAnnotation) > 0 {
Expand All @@ -77,6 +154,49 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
return result, nil
}

func getContainerStartupBoostPolicy(container *core.Container, vpa *vpa_types.VerticalPodAutoscaler) *vpa_types.StartupBoost {
policy := vpa_api_util.GetContainerResourcePolicy(container.Name, vpa.Spec.ResourcePolicy)
startupBoost := vpa.Spec.StartupBoost
if policy != nil && policy.StartupBoost != nil {
startupBoost = policy.StartupBoost
}
return startupBoost
}

func calculateBoostedCPU(baseCPU resource.Quantity, startupBoost *vpa_types.StartupBoost) (*resource.Quantity, error) {
if startupBoost == nil {
return &baseCPU, nil
}

boostType := startupBoost.CPU.Type
if boostType == "" {
boostType = vpa_types.FactorStartupBoostType
}

switch boostType {
case vpa_types.FactorStartupBoostType:
if startupBoost.CPU.Factor == nil {
return nil, fmt.Errorf("startupBoost.CPU.Factor is required when Type is Factor or not specified")
}
factor := *startupBoost.CPU.Factor
if factor < 1 {
return nil, fmt.Errorf("boost factor must be >= 1")
}
boostedCPU := baseCPU.MilliValue()
boostedCPU = int64(float64(boostedCPU) * float64(factor))
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
case vpa_types.QuantityStartupBoostType:
if startupBoost.CPU.Quantity == nil {
return nil, fmt.Errorf("startupBoost.CPU.Quantity is required when Type is Quantity")
}
quantity := *startupBoost.CPU.Quantity
boostedCPU := baseCPU.MilliValue() + quantity.MilliValue()
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
default:
return nil, fmt.Errorf("unsupported startup boost type: %s", startupBoost.CPU.Type)
}
}

func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_util.ContainerToAnnotationsMap, containerResources vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, string) {
var patches []resource_admission.PatchRecord
// Add empty resources object if missing.
Expand Down
Loading
Loading