Horizontal pod autoscaling

Prerequisites

  • Install the kubectl binary on your Ansible box
  • Install the UCP Client bundle for the admin user
  • Confirm that you can connect to the cluster by running a test command, for example, kubectl get nodes
  • Install metrics-server as shown in ../blog/install-metrics-server.html

Introduction

The Horizontal Pod Autoscaler automatically scales the number of pods in a deployment or replica set based on the observed CPU utilization.

Assigning CPU Resources to Containers and Pods

It is possible to request the minimum amount of CPU that a container requires. If the cluster has this amount available, the container will be allowed to start. However, the container will not be scheduled if the requested CPU resource is not available. You can also specify a CPU limit to set a maximum amount of CPU resources the container is allowed.

A Container is guaranteed to have as much CPU as specified in its request, but is not allowed to use more CPU than its limit. The CPU resource is measured in CPU units. One CPU, in Kubernetes, is equivalent to:

  • One AWS vCPU
  • One GCP Core
  • One Azure vCore
  • One Hyperthread on a bare-metal Intel processor with Hyperthreading

Fractional values are allowed. A Container that requests 0.5 CPU is guaranteed half as much CPU as a Container that requests 1 CPU. You can use the suffix m to mean milli or one thousandth of a CPU. For example, 100m CPU and 0.1 CPU are the same.

CPU is always requested as an absolute quantity, never as a relative quantity; 0.1 is the same amount of CPU on a single-core,dual-core, or 48-core machine.

Workload

To demonstrate the Horizontal Pod Autoscaler, we will serve up a single PHP page that performs a compute-intensive workload:

<?php
  $x = 0.0001;
  for ($i = 0; $i <= 1000000; $i++) {
    $x += sqrt($x);
  }
  echo "OK!";
?>

This file is used as the index page for a web server that is deployed using a custom docker image. A Dockerfile based on the php-apache image is used to containerize our worload:

FROM php:5-apache
ADD index.php /var/www/html/index.php
RUN chmod a+rx index.php

Evert time the index page is accessed, the computation will be performed and the message "OK!" will be returned to the client.

Deploying the service

First, we will start a deployment running the image and expose it as a service. We set the CPU request to 200m or 200/1000 equals 0.2 CPU. Note that, in this instance, we do not set an upper limit and so the container can use as much CPU as is available on the node.

$ kubectl run php-apache --image=k8s.gcr.io/hpa-example --requests=cpu=200m --expose --port=80

In a separate terminal, you can set up a watch for the pods, deployments and replica sets that are created:

# watch -n 10 kubectl get pods,deploy,rs

Every 10.0s: kubectl get pods,deploy,rs                    Fri Mar  1 14:43:14 2019

NAME                             READY     STATUS    RESTARTS   AGE
pod/php-apache-7bf9f4b44-lmzfl   1/1       Running   0          37s

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/php-apache   1         1         1            1           38s

NAME                                         DESIRED   CURRENT   READY     AGE
replicaset.extensions/php-apache-7bf9f4b44   1         1         1         37s

In another terminal, you can use kubectl top to monitor the CPU usage. Here, you can see that the single pod is using minimal CPU and memory resources (1m equals 1/1000th CPU, '7Mi' equals 7MB memory).

# watch -n 10 kubectl top pods

Every 10.0s: kubectl top pods | grep php-apache    Fri Mar  1 14:43:26 2019

php-apache-7bf9f4b44-lmzfl   1m           7Mi

You can use the kubectl describe command to see details of the deployment. You can see the CPU request of 200m and that, for now, only one replica is deployed.

# kubectl describe deploy php-apache

Name:                   php-apache
Namespace:              default
CreationTimestamp:      Fri, 01 Mar 2019 14:41:45 +0000
Labels:                 run=php-apache
Annotations:            deployment.kubernetes.io/revision=1
Selector:               run=php-apache
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  run=php-apache
  Containers:
   php-apache:
    Image:      k8s.gcr.io/hpa-example
    Port:       80/TCP
    Host Port:  0/TCP
    Requests:
      cpu:        200m
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   php-apache-7bf9f4b44 (1/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  8m    deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 1

Expose NodePort

By default, the service that has been generated is assigned a ClusterIP. It can be convenient to use a NodePort to expose the service.

# kubectl get svc php-apache
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
php-apache   ClusterIP   10.96.103.118   <none>        80/TCP    1m

Use the kubectl patch command to change the service type to NodePort and to set an explicit port, in this instance 33999.

# kubectl patch svc php-apache --type='json' -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'

# kubectl patch svc php-apache --type='json' -p '[{"op": "add", "path":"/spec/ports/0/nodePort", "value":33999}]'

Now, when you inspect the service, you will see that it can be accessed through any node in the cluster, on the specified port. You will use this when generating a load on the service.

# kubectl get svc php-apache

NAME         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
php-apache   NodePort   10.96.103.118   <none>        80:33999/TCP   2m

Create autoscaler

Use the kubectl autoscale command to generate the autoscaler.

# kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10

horizontalpodautoscaler.autoscaling/php-apache autoscaled

In this instance, you specify that when the CPU hits 50% utilization, another pod should be deployed. (In reality, you may want to set this threshold higher, for example, to 70% or 80%). Remember that you set the CPU request to 200m, so you should see a new pod being created when CPU utilization rises above 100m in absolute terms. You also specifiy that, at most, 10 pods should be deployed.

In a separate terminal, run a watch on the hpa resource. Note that, as there is still no load on the web server, the target utilization shows 0%/50%.

# watch -n 10 kubectl get hpa

Every 10.0s: kubectl get hpa                        Fri Mar  1 14:47:07 2019

NAME         REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache   0%/50%    1         10        1          1m

Generate load

In another terminal, run a simple shell script to repeatedly access the index page, using any of the nodes in the cluster and the specified port number.

# while true; do wget -q -O- http://hpe2-ucp01.am2.cloudra.local:33999; done

OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK!OK...

Wait for scaling

After a slight delay, kubectl top pods will report that the single pod is consuming significant CPU resources - recall that you did not specify a limit to set an upper boundary on the CPU utilization. In this instance, it is consuming more than 8 times the target threshold (50% of 200m or 100m), so new pods should be deployed shortly.

Every 10.0s: kubectl top pods                      Fri Mar  1 14:52:34 2019

NAME                         CPU(cores)   MEMORY(bytes)
php-apache-7bf9f4b44-lmzfl   887m         10Mi

The hpa resource also indicates that the target is being exceeded (443%/50%).

Every 10.0s: kubectl get hpa                        Fri Mar  1 14:52:43 2019

NAME         REFERENCE               TARGETS    MINPODS   MAXPODS   REPLICAS
   AGE
php-apache   Deployment/php-apache   443%/50%   1         10        1
   6m

Scaling to 4 pods

After a certain amount of time, the watch on pods, deployments and replica sets will show new pods being deployed:

Every 10.0s: kubectl get pods,deploy,rs                    Fri Mar  1 14:53:00 2019

NAME                             READY     STATUS    RESTARTS   AGE
pod/php-apache-7bf9f4b44-4q9xb   1/1       Running   0          26s
pod/php-apache-7bf9f4b44-6jmg4   1/1       Running   0          26s
pod/php-apache-7bf9f4b44-jsrlh   1/1       Running   0          26s
pod/php-apache-7bf9f4b44-lmzfl   1/1       Running   0          10m

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/php-apache   4         4         4            4           10m
NAME                                         DESIRED   CURRENT   READY     AGE
replicaset.extensions/php-apache-7bf9f4b44   4         4         4         10m

Use kubectl describe on the deployment to see details of the scaling event.

# kubectl describe deploy php-apache
Name:                   php-apache
...
NewReplicaSet:   php-apache-7bf9f4b44 (4/4 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  11m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 1
  Normal  ScalingReplicaSet  1m    deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 4

Impact of scaling to 4

After a while, kubectl top pods will show that the 4 pods are coping better than one, but they are still exceeding the target, in this instance, 50% of 200m or 100m.

Every 10.0s: kubectl top pods                      Fri Mar  1 14:55:28 2019

NAME                         CPU(cores)   MEMORY(bytes)
php-apache-7bf9f4b44-4q9xb   232m         10Mi
php-apache-7bf9f4b44-6jmg4   236m         10Mi
php-apache-7bf9f4b44-jsrlh   226m         9Mi
php-apache-7bf9f4b44-lmzfl   199m         10Mi

Similarly, the hpa resource shows that the overall target threshold is still being missed (111%/50%).

Every 10.0s: kubectl get hpa                        Fri Mar  1 14:55:48 2019

NAME         REFERENCE               TARGETS    MINPODS   MAXPODS   REPLICAS    AGE
php-apache   Deployment/php-apache   111%/50%   1         10        4           10m

Scaling to 8 pods

As the thresholds are still being exceeded, another round of scaling takes place, this time from 4 to 8 pods.

Every 10.0s: kubectl get pods,deploy,rs                    Fri Mar  1 14:58:40 2019

NAME                             READY     STATUS    RESTARTS   AGE
pod/php-apache-7bf9f4b44-2bcc9   1/1       Running   0          3s
pod/php-apache-7bf9f4b44-4q9xb   1/1       Running   0          6m
pod/php-apache-7bf9f4b44-6jmg4   1/1       Running   0          6m
pod/php-apache-7bf9f4b44-7r7lp   1/1       Running   0          3s
pod/php-apache-7bf9f4b44-bpw8s   1/1       Running   0          3s
pod/php-apache-7bf9f4b44-d5jp7   1/1       Running   0          3s
pod/php-apache-7bf9f4b44-jsrlh   1/1       Running   0          6m
pod/php-apache-7bf9f4b44-lmzfl   1/1       Running   0          16m

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/php-apache   8         8         8            8           16m
NAME                                         DESIRED   CURRENT   READY     AGE
replicaset.extensions/php-apache-7bf9f4b44   8         8         8         16m

Again, you can use kubectl describe on the deployment for confirmation of the scaling event.

# kubectl describe deploy php-apache
Name:                   php-apache
...
NewReplicaSet:   php-apache-7bf9f4b44 (8/8 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  17m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 1
  Normal  ScalingReplicaSet  7m    deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 4
  Normal  ScalingReplicaSet  1m    deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 8

Impact of scaling to 8

After a while, kubectl top pods will show that the 8 pods are almost sufficient to achieve the required threshold. On average, it would seem that most pods are close to the 100m requirement.

Every 10.0s: kubectl top pods                      Fri Mar  1 15:01:37 2019

NAME                         CPU(cores)   MEMORY(bytes)
php-apache-7bf9f4b44-2bcc9   83m          9Mi
php-apache-7bf9f4b44-4q9xb   98m          10Mi
php-apache-7bf9f4b44-6jmg4   143m         10Mi
php-apache-7bf9f4b44-7r7lp   80m          9Mi
php-apache-7bf9f4b44-bpw8s   111m         10Mi
php-apache-7bf9f4b44-d5jp7   157m         10Mi
php-apache-7bf9f4b44-jsrlh   108m         9Mi
php-apache-7bf9f4b44-lmzfl   96m          10Mi

Similarly, the hpa resource shows that the overall target threshold is close to being achieved (54%/50%).

Every 10.0s: kubectl get hpa                        Fri Mar  1 15:01:55 2019

NAME         REFERENCE               TARGETS   MINPODS   MAXPODS   REPLICAS
  AGE
php-apache   Deployment/php-apache   54%/50%   1         10        8
  16m

Further scaling and tolerance

Given this particular workload and your environment, the exact number of pods deployed is not certain. In this instance, another two pods were subsequently deployed, but then one was removed to leave a steady-state of 9 pods required to meet the CPU requirements. Tolerance levels are deployed to stop thrashing, where pods are added and removed repeatedly.

Scaling down

Stop the shell script that is generating the workload. The CPU usage reported by kubectl top pods will drop to a minimal level (1m) while the hpa resouce will show a target of 0%/50%.

Every 10.0s: kubectl top pods                      Fri Mar  1 15:15:28 2019

NAME                         CPU(cores)   MEMORY(bytes)
php-apache-7bf9f4b44-2bcc9   1m           9Mi
php-apache-7bf9f4b44-4q9xb   1m           10Mi
php-apache-7bf9f4b44-6jmg4   1m           10Mi
php-apache-7bf9f4b44-7r7lp   1m           9Mi
php-apache-7bf9f4b44-bpw8s   1m           10Mi
php-apache-7bf9f4b44-d5jp7   1m           10Mi
php-apache-7bf9f4b44-jsrlh   1m           9Mi
php-apache-7bf9f4b44-lmzfl   1m           10Mi
php-apache-7bf9f4b44-q6qcf   1m           10Mi

In time, the pods will be scaled down to one, as shown below:

Every 10.0s: kubectl get pods,deploy,rs                    Fri Mar  1 15:16:41 2019

NAME                             READY     STATUS        RESTARTS   AGE
pod/php-apache-7bf9f4b44-2bcc9   0/1       Terminating   0          18m
pod/php-apache-7bf9f4b44-4q9xb   0/1       Terminating   0          24m
pod/php-apache-7bf9f4b44-d5jp7   0/1       Terminating   0          18m
pod/php-apache-7bf9f4b44-lmzfl   1/1       Running       0          34m

NAME                               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/php-apache   1         1         1            1           34m
NAME                                         DESIRED   CURRENT   READY     AGE
replicaset.extensions/php-apache-7bf9f4b44   1         1         1         34m

You can again run kubectl describe on the deployment, to see the scaling event.

# kubectl describe deploy php-apache
Name:                   php-apache
...
NewReplicaSet:   php-apache-7bf9f4b44 (1/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  35m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 1
  Normal  ScalingReplicaSet  25m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 4
  Normal  ScalingReplicaSet  19m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 8
  Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set php-apache-7bf9f4b44 to 10
  Normal  ScalingReplicaSet  6m    deployment-controller  Scaled down replica set php-apache-7bf9f4b44 to 9
  Normal  ScalingReplicaSet  1m    deployment-controller  Scaled down replica set php-apache-7bf9f4b44 to 1

Teardown

Run the following commands to clean up:

# kubectl delete deploy php-apache
deployment.extensions "php-apache" deleted

# kubectl delete service php-apache
service "php-apache" deleted

# kubectl delete hpa php-apache
horizontalpodautoscaler.autoscaling "php-apache" deleted

Resources

Video:

Horizontal pod autoscaling walkthrough on Vimeo.

Introduction to HPA at https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

Example is based on https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/

Assigning CPU resources: https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/