From 2d7c68f4c0ec92ff16e1d38ce5f2a5861152c93e Mon Sep 17 00:00:00 2001 From: Dave Protasowski Date: Wed, 15 Apr 2026 16:23:30 -0400 Subject: [PATCH 1/2] use the /scale subresource to when updating replica count This has the added benefit of not having to deepcopy and computing a JSONPatch via diffing two json byte strings which would take about 400ms --- config/core/200-roles/clusterrole.yaml | 3 +++ pkg/reconciler/autoscaling/kpa/kpa_test.go | 6 ++--- pkg/reconciler/autoscaling/kpa/scaler.go | 25 +++++++++---------- pkg/reconciler/autoscaling/kpa/scaler_test.go | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/config/core/200-roles/clusterrole.yaml b/config/core/200-roles/clusterrole.yaml index 3fcca910ba43..e76a24f76652 100644 --- a/config/core/200-roles/clusterrole.yaml +++ b/config/core/200-roles/clusterrole.yaml @@ -67,3 +67,6 @@ rules: resources: ["clusterroles"] verbs: ["delete"] resourceNames: ["knative-serving-certmanager"] + - apiGroups: ["*"] + resources: ["*/scale"] + verbs: ["patch"] diff --git a/pkg/reconciler/autoscaling/kpa/kpa_test.go b/pkg/reconciler/autoscaling/kpa/kpa_test.go index 107d232ff0c8..797af201c9aa 100644 --- a/pkg/reconciler/autoscaling/kpa/kpa_test.go +++ b/pkg/reconciler/autoscaling/kpa/kpa_test.go @@ -264,7 +264,7 @@ func TestReconcile(t *testing.T) { minScalePatch := clientgotesting.PatchActionImpl{ ActionImpl: clientgotesting.ActionImpl{Namespace: testNamespace}, Name: deployName, - Patch: []byte(fmt.Sprintf(`[{"op":"replace","path":"/spec/replicas","value":%d}]`, defaultScale)), + Patch: fmt.Appendf(nil, `[{"op":"add","path":"/spec/replicas","value":%d}]`, defaultScale), } inactiveKPAMinScale := func(g int32) *autoscalingv1alpha1.PodAutoscaler { @@ -1030,7 +1030,7 @@ func TestReconcile(t *testing.T) { WantPatches: []clientgotesting.PatchActionImpl{{ ActionImpl: clientgotesting.ActionImpl{Namespace: testNamespace}, Name: deployName, - Patch: []byte(fmt.Sprintf(`[{"op":"replace","path":"/spec/replicas","value":%d}]`, 20)), + Patch: fmt.Appendf(nil, `[{"op":"add","path":"/spec/replicas","value":%d}]`, 20), }}, }, { Name: "initial scale reached, mark PA as active", @@ -1058,7 +1058,7 @@ func TestReconcile(t *testing.T) { WantPatches: []clientgotesting.PatchActionImpl{{ ActionImpl: clientgotesting.ActionImpl{Namespace: testNamespace}, Name: deployName, - Patch: []byte(fmt.Sprintf(`[{"op":"replace","path":"/spec/replicas","value":%d}]`, 20)), + Patch: fmt.Appendf(nil, `[{"op":"add","path":"/spec/replicas","value":%d}]`, 20), }}, }, { Name: "initial scale zero: scale to zero", diff --git a/pkg/reconciler/autoscaling/kpa/scaler.go b/pkg/reconciler/autoscaling/kpa/scaler.go index 84dba5dc81d7..d542c73e8d9a 100644 --- a/pkg/reconciler/autoscaling/kpa/scaler.go +++ b/pkg/reconciler/autoscaling/kpa/scaler.go @@ -298,7 +298,10 @@ func (ks *scaler) handleScaleToZero(ctx context.Context, pa *autoscalingv1alpha1 } } -func (ks *scaler) applyScale(ctx context.Context, pa *autoscalingv1alpha1.PodAutoscaler, desiredScale int32, +func (ks *scaler) applyScale( + ctx context.Context, + pa *autoscalingv1alpha1.PodAutoscaler, + desiredScale int32, ps *autoscalingv1alpha1.PodScalable, ) error { logger := logging.FromContext(ctx) @@ -308,19 +311,15 @@ func (ks *scaler) applyScale(ctx context.Context, pa *autoscalingv1alpha1.PodAut return err } - psNew := ps.DeepCopy() - psNew.Spec.Replicas = &desiredScale - patch, err := duck.CreatePatch(ps, psNew) - if err != nil { - return err - } - patchBytes, err := patch.MarshalJSON() - if err != nil { - return err - } + patch := fmt.Sprintf(`[{"op":"add","path":"/spec/replicas","value":%d}]`, desiredScale) - _, err = ks.dynamicClient.Resource(*gvr).Namespace(pa.Namespace).Patch(ctx, ps.Name, types.JSONPatchType, - patchBytes, metav1.PatchOptions{}) + _, err = ks.dynamicClient.Resource(*gvr).Namespace(pa.Namespace).Patch( + ctx, + ps.Name, + types.JSONPatchType, + []byte(patch), + metav1.PatchOptions{}, + "scale") if err != nil { return fmt.Errorf("failed to apply scale %d to scale target %s: %w", desiredScale, name, err) } diff --git a/pkg/reconciler/autoscaling/kpa/scaler_test.go b/pkg/reconciler/autoscaling/kpa/scaler_test.go index bf0b7f718b47..0ef2c3119643 100644 --- a/pkg/reconciler/autoscaling/kpa/scaler_test.go +++ b/pkg/reconciler/autoscaling/kpa/scaler_test.go @@ -785,7 +785,7 @@ func checkReplicas(t *testing.T, dynamicClient *fakedynamic.FakeDynamicClient, d if patch.GetName() != deployment.Name { continue } - want := fmt.Sprintf(`[{"op":"replace","path":"/spec/replicas","value":%d}]`, expectedScale) + want := fmt.Sprintf(`[{"op":"add","path":"/spec/replicas","value":%d}]`, expectedScale) if got := string(patch.GetPatch()); got != want { t.Errorf("Patch = %s, wanted %s", got, want) } From 0f85a0614a94ec479ff14ecc87c13429fc633bcf Mon Sep 17 00:00:00 2001 From: Dave Protasowski Date: Wed, 15 Apr 2026 18:34:27 -0400 Subject: [PATCH 2/2] drop spec.template from PodScalable This property is no longer needed by the autoscaler so we don't need to store the PodTemplate in memory --- docs/serving-api.md | 24 ------------------- .../autoscaling/v1alpha1/podscalable_types.go | 19 ++------------- .../v1alpha1/zz_generated.deepcopy.go | 1 - 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/docs/serving-api.md b/docs/serving-api.md index 77aad5ea6a63..1f6dd48d01da 100644 --- a/docs/serving-api.md +++ b/docs/serving-api.md @@ -565,18 +565,6 @@ Kubernetes meta/v1.LabelSelector - - -template
- - -Kubernetes core/v1.PodTemplateSpec - - - - - - @@ -633,18 +621,6 @@ Kubernetes meta/v1.LabelSelector - - -template
- - -Kubernetes core/v1.PodTemplateSpec - - - - - -

PodScalableStatus diff --git a/pkg/apis/autoscaling/v1alpha1/podscalable_types.go b/pkg/apis/autoscaling/v1alpha1/podscalable_types.go index b667c7b593db..ee48f87635a8 100644 --- a/pkg/apis/autoscaling/v1alpha1/podscalable_types.go +++ b/pkg/apis/autoscaling/v1alpha1/podscalable_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "knative.dev/pkg/apis" @@ -43,9 +42,8 @@ type PodScalable struct { // PodScalableSpec is the specification for the desired state of a // PodScalable (or at least our shared portion). type PodScalableSpec struct { - Replicas *int32 `json:"replicas,omitempty"` - Selector *metav1.LabelSelector `json:"selector"` - Template corev1.PodTemplateSpec `json:"template"` + Replicas *int32 `json:"replicas,omitempty"` + Selector *metav1.LabelSelector `json:"selector"` } // PodScalableStatus is the observed state of a PodScalable (or at @@ -80,19 +78,6 @@ func (t *PodScalable) Populate() { Values: []string{"baz", "blah"}, }}, }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "foo": "bar", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "container-name", - Image: "container-image:latest", - }}, - }, - }, } t.Status = PodScalableStatus{ Replicas: 42, diff --git a/pkg/apis/autoscaling/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/autoscaling/v1alpha1/zz_generated.deepcopy.go index 5c328191ba80..38ba90053d18 100644 --- a/pkg/apis/autoscaling/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/autoscaling/v1alpha1/zz_generated.deepcopy.go @@ -299,7 +299,6 @@ func (in *PodScalableSpec) DeepCopyInto(out *PodScalableSpec) { *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } - in.Template.DeepCopyInto(&out.Template) return }