Skip to content

Revisions with initialScale > 1 that are no longer referenced by any Route cannot scale down to 0 #16260

@Papilionidae

Description

@Papilionidae

Problem Description

Revisions with initialScale > 1 that are no longer referenced by any Route (i.e., routingState = "reserve") cannot scale down to 0, causing resource waste when old revisions are replaced by new ones.

Expected Behavior

When a revision's routingState becomes "reserve" (meaning it is no longer referenced by any Route), it should be able to scale down to 0 immediately, regardless of initialScale configuration. This is important because:

  • When a new revision becomes ready and replaces the old one, the old revision's routingState changes to "reserve"
  • These old revisions should free up resources quickly by scaling down to 0
  • The initialScale constraint should not prevent this cleanup

Actual Behavior

Revisions with routingState = "reserve" and initialScale > 1 cannot scale down to 0 because:

  1. The initialScale logic in scaler.go forces min = initialScale even when the revision is no longer referenced by any Route
  2. This prevents the autoscaler from scaling down below initialScale, even though ScaleBounds() already returns min=0 for unreachable revisions (which includes routingState = "reserve")

Steps to Reproduce

1. Deploy a Knative Service with minScale=1 and initialScale=2:

apiVersion: serving.knative.dev/v1
   kind: Service
   metadata:
     name: helloworld
   spec:
     template:
       metadata:
         annotations:
           autoscaling.knative.dev/minScale: "1"
           autoscaling.knative.dev/initialScale: "2"
       spec:
         containers:
         - image: gcr.io/knative-samples/helloworld-go

2. Deploy a new revision with an invalid image (e.g., imagePullBackOff):

spec:
     template:
       spec:
         containers:
         - image: invalid-image:latest

3. The new revision will have 2 pods in ImagePullBackOff state and will be marked as Unreachable
4. Deploy a third revision with a valid image
5. Observed: The second revision (with ImagePullBackOff pods) remains at 2 pods and cannot scale down to 0, even though the old revision (helloworld-00002) has routingState = "reserve" (no longer referenced by Route):

$ kubectl get po -n paas-uat
NAME                                                  READY   STATUS             RESTARTS   AGE
helloworld-nodejs-00002-deployment-564896c9fc-v7ntx   0/2     ImagePullBackOff   0          3h
helloworld-nodejs-00002-deployment-564896c9fc-vsqrh   0/2     ImagePullBackOff   0          3h
helloworld-nodejs-00003-deployment-847f88dbd8-6vfll   2/2     Running            0          168m

Root Cause Analysis

The issue is in serving/pkg/reconciler/autoscaling/kpa/scaler.go, in the scale() method:

// Line 343-349
if initialScale > 1 && !pa.Status.IsScaleTargetInitialized() {
    // Ignore initial scale if minScale >= initialScale.
    if min < initialScale {
        logger.Debugf("Adjusting min to meet the initial scale: %d -> %d", min, initialScale)
    }
    min = intMax(initialScale, min)
}

This code forces min = initialScale regardless of whether the revision's routingState is "reserve". However:

  • When a revision's routingState = "reserve", it means it's no longer referenced by any Route
  • ScaleBounds() already returns min=0 for unreachable revisions (see pa_lifecycle.go:90), and routingState = "reserve" results in Reachability = Unreachable (see revision/resources/pa.go:77-78)
  • The initialScale logic should respect the routingState = "reserve" condition and not override the min=0 value

Relationship Between routingState and Reachability

According to serving/pkg/reconciler/revision/resources/pa.go:

  • routingState = "active"Reachability = Reachable
  • routingState = "reserve"Reachability = Unreachable
  • routingState = "pending" or unset → Reachability = Unknown

So routingState = "reserve" is equivalent to Reachability = Unreachable for the purpose of determining whether a revision should be allowed to scale down.

Proposed Solution

Modify the initialScale check to ignore initialScale when the revision's routingState = "reserve" (i.e., when pa.Spec.Reachability == ReachabilityUnreachable):

if initialScale > 1 && !pa.Status.IsScaleTargetInitialized() && pa.Spec.Reachability != autoscalingv1alpha1.ReachabilityUnreachable {
    // Ignore initial scale if minScale >= initialScale.
    if min < initialScale {
        logger.Debugf("Adjusting min to meet the initial scale: %d -> %d", min, initialScale)
    }
    min = intMax(initialScale, min)
}

This change ensures that:

  1. Revisions with routingState = "reserve" (no longer referenced by Route) can scale down to 0 immediately
  2. The initialScale logic only applies to revisions that are still referenced by Routes (routingState = "active")
  3. Resources are freed promptly when old revisions are replaced by new ones

Environment

  • Knative Serving version: Knative v1.19.6
  • Autoscaler configuration: initialScale=2, minScale=1

Metadata

Metadata

Assignees

Labels

kind/bugCategorizes issue or PR as related to a bug.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions