, ,

VKS Cluster Performance Optimization with Monitoring and Scaling (VCAP-VKS Exam Series, Part 34)

Objective 5.5 is a measurement objective wearing a scaling costume. Here is the metrics pipeline, the four scaling levers, what each one measurably moves on a VCF 9.0 estate, and the request gap that makes all of them lie to you.

VCAP-VKS Exam Series · Part 34 of 34
Key takeaways: objective 5.5 reads Optimize cluster performance using monitoring and scaling features. Monitoring is named first for a reason, and it is the half candidates skip. Kubernetes schedules and autoscales on resource requests, not on consumption, so a cluster whose workloads have wrong requests will scale confidently in the wrong direction. Four levers exist and they operate on wildly different timescales: a HorizontalPodAutoscaler acts in tens of seconds, a node addition takes minutes, and a VM Class change is a rolling replace measured in tens of minutes. Node size is a Supervisor decision, not a workload cluster decision, so horizontal scaling can never fix a pod that is larger than the VM Class bound to the vSphere Namespace. Measure, then move one lever, then measure again.
Who this is for: a candidate who installed the autoscaler deliberately in Part 22, learned the lifecycle commands in Part 20, and now has to decide which lever a given set of numbers is actually asking for. This Part covers objective 5.5, published wording Optimize cluster performance using monitoring and scaling features. Terms defined on first use: a resource request is the amount of CPU or memory a pod reserves and the only figure the scheduler considers; a limit is the cap the kernel enforces; metrics-server is the cluster add on that serves the Kubernetes resource metrics API; HorizontalPodAutoscaler (HPA) changes replica count based on observed utilisation; Cluster Autoscaler (CA) changes worker node count based on unschedulable pods; a MachineDeployment is the Cluster API object that owns one worker node pool; a VM Class is the vSphere sizing template that fixes how big a node virtual machine can be; a Kubernetes Release, abbreviated VKr in current Broadcom release notes and TKR in older ones, is the versioned node image and component set VKS offers you.

Two numbers that decide every scaling action

Thirty eight and seventy one. On the morning I started drafting this Part, aggregate CPU requests across the six worker nodes in the ns-payments cluster of our reference estate sat at 38 percent of allocatable, while measured CPU consumption over the same fifteen minute window sat at 71 percent. Both figures are correct. Only one of them is visible to anything that scales. Kubernetes places pods on requests. Cluster Autoscaler reacts to pods that cannot be placed, which is a request question. HPA reacts to utilisation expressed as a percentage of requests. A 33 point gap between what the workloads asked for and what they take means every automatic mechanism in that cluster is reasoning from a figure that bears no relation to reality.

That single observation is most of objective 5.5. Optimisation on VKS is a measurement you repair first and a lever you pull second, and getting those two in the wrong order produces clusters that grow nodes forever while individual pods stay slow. Popular study advice for this objective is to learn the autoscaler flags. Learn them, certainly, but understand that a cluster with unset or badly guessed requests will make the autoscaler behave like a random number generator with a credit card.

Trace the hard ceiling back to the Supervisor, because that is where it lives and this series has said so in every Part that touched a workload cluster. A VKS worker node is a virtual machine sized by a VM Class, and a VM Class becomes usable only when a vSphere administrator binds it to the vSphere Namespace that owns the cluster. If best-effort-large at 4 vCPU and 16 GB is the only class bound to ns-payments, a pod requesting 6 CPU will sit Pending indefinitely while Cluster Autoscaler cheerfully adds nodes that pod can never fit on, right up to the maximum you configured. Horizontal scaling cannot change the shape of a node. Every scaling conversation in VKS eventually terminates at a Supervisor object, and on this objective it terminates at a VM Class or at the vSphere Namespace CPU and memory limits.

Worth restating once for this final Part: VKS is the product formerly called TKG Service or TKGS. Broadcom renamed the product and left the plumbing alone, so scaling documentation still sits under URL paths containing operating-tkg-service-clusters and autoscaling-tkg-service-clusters, node pools are Cluster API MachineDeployments, and autoscaler annotations carry the cluster.x-k8s.io prefix. None of that is stale documentation. It is what your estate actually calls things.

Median time to measurable effect, by lever Reference estate, VCF 9.0 with VKS 3.3.1, six worker nodes on best-effort-large, 20 timed events per lever Manual pod replica scale 0.4 min HPA replica change 0.5 min Manual node pool scale out 4.9 min Cluster Autoscaler node add 5.2 min VM Class change, six nodes 47.0 min 0 10 20 30 40 50 minutes from decision to measurable change in workload capacity
Levers separated by two orders of magnitude. Picking the slow one during an incident is the most common optimisation mistake I see.

Preflight for a measurement pass

Last Part we rescued an upgrade that had wedged on a drain. This Part the cluster is healthy, every Machine is Running on the target release, and the complaint is latency rather than failure. Two contexts still apply and you will move between them constantly. Supervisor context holds the Cluster, MachineDeployment, VM Class binding and vSphere Namespace limits. Workload cluster context holds the pods, the metrics API, HPA objects and the Prometheus deployment. I use kubectl with the vSphere plugin for both, because the plugin issues one credential that covers Supervisor and workload cluster contexts and swapping with kubectl config use-context is faster than juggling kubeconfig files, which matters when you are timing a scale event.

Establish versions before anything else, because autoscaler behaviour and package availability both move with them, and because a mismatched package minor version fails reconciliation silently enough to waste an afternoon.

# Tested against: VCF 9.0, vCenter 9.0.0.0 build 24755230, Supervisor 9.0.0, # VKS 3.3.1, VKr v1.32.0+vmware.1, kubectl v1.32.2, kubectl-vsphere 9.0.0, VCF CLI 9.0.0 $ kubectl config use-context pay-01 Switched to context "pay-01". $ kubectl top nodes NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% pay-01-hbn7q-4kx2s 742m 18% 5410Mi 34% pay-01-md-0-hx8vp-6b9c4f7d8-9wtq4 3021m 75% 11884Mi 74% pay-01-md-0-hx8vp-6b9c4f7d8-l2mkr 2864m 71% 12633Mi 79% pay-01-md-0-hx8vp-6b9c4f7d8-rk4bd 2712m 67% 10902Mi 68%

If that command returns nothing useful, you have found your first problem rather than a broken tool. Here is the real failure line, and it is the single most common blocker on this objective because everything downstream of it depends on the resource metrics API being served.

$ kubectl top nodes error: Metrics API not available $ kubectl get apiservice v1beta1.metrics.k8s.io NAME SERVICE AVAILABLE AGE v1beta1.metrics.k8s.io kube-system/metrics-server False (MissingEndpoints) 31d $ kubectl -n kube-system get pods -l k8s-app=metrics-server NAME READY STATUS RESTARTS AGE metrics-server-6c9d84f7bd-x84qn 0/1 Pending 0 22m $ kubectl -n kube-system describe pod -l k8s-app=metrics-server | grep -A2 Events Events: Warning FailedScheduling 22m default-scheduler 0/4 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 3 Insufficient cpu.

Read that carefully, because it is a small joke at your expense. Your monitoring add on cannot be scheduled for exactly the reason you were about to investigate. Insufficient cpu here is a request arithmetic statement, not a busy CPU statement, and the fix is either honest requests on the workloads squatting on the reservation or one more node. That is also a useful reminder that the resource metrics API is not optional infrastructure on this objective, since HPA and kubectl top both read from it.

Metrics pipeline from kubelet to dashboard

Three distinct layers exist on a VCF 9.0 estate and candidates routinely treat them as one thing. Layer one is the resource metrics pipeline: kubelet exposes CPU and memory, metrics-server aggregates it, and the API server serves it at metrics.k8s.io. Upstream Kubernetes documentation is unambiguous that this layer exists for autoscaling and for kubectl top, and it explicitly warns against using metrics-server as a monitoring solution. It keeps a short in memory window and no history at all. Layer two is Prometheus, delivered as a VKS standard package, which scrapes and retains, with Grafana on top for visualisation. Layer three is VCF Operations, which correlates Kubernetes objects with the ESXi hosts, datastores and vSAN underneath them, and which is the only layer that will tell you a pod is slow because a disk group is rebuilding.

Here is where this objective quietly reaches back into Part 7. Broadcom documents that the Prometheus and Grafana packages require a default storage class on the VKS cluster where you deploy them. A default storage class exists only because a vSphere Storage Policy was assigned to the vSphere Namespace on the Supervisor and then nominated as default in the cluster spec. Skip that and your monitoring stack never comes up, which is a Supervisor failure presenting as a monitoring failure. It is a neat illustration of the third premise of this whole series.

# Prove the prerequisite before installing anything $ kubectl get storageclass NAME PROVISIONER RECLAIMPOLICY DEFAULT AGE vsan-default-storage-policy csi.vsphere.vmware.com Delete true 44d vsan-high-performance csi.vsphere.vmware.com Delete false 44d # Real failure when the default is missing, seen on a cluster built without one $ kubectl -n tkg-system get packageinstall prometheus NAME PACKAGE NAME PACKAGE VERSION DESCRIPTION AGE prometheus prometheus.tanzu.vmware.com 2.54.1+vmware.1 Reconcile failed 9m $ kubectl -n prometheus get pvc NAME STATUS VOLUME CAPACITY STORAGECLASS AGE prometheus-server-pvc Pending 9m $ kubectl -n prometheus describe pvc prometheus-server-pvc | tail -3 Events: Normal FailedBinding 9m persistentvolume-controller no persistent volumes available for this claim and no storage class is set

Fix that on the Supervisor by assigning the storage policy to the vSphere Namespace, then nominate it as the default in the cluster spec, and the package reconciles on its own within a scrape cycle. Do not annotate a storage class as default by hand inside the workload cluster as a shortcut, because the next cluster reconciliation from the Supervisor topology will quietly undo it and your monitoring will fall over on a Tuesday for no visible reason.

flowchart TD
  A[Workload reported slow] --> B[Compare requests against actual usage]
  B --> C{Requests set and realistic}
  C -->|No| D[Correct requests and limits first]
  D --> B
  C -->|Yes| E{Any pods Pending}
  E -->|Yes| F[Read the scheduling event text]
  F --> G{Insufficient cpu or memory}
  G -->|Yes| H[Add worker nodes or raise namespace limits]
  G -->|No| I[Check VM class binding and node selectors]
  E -->|No| J{Utilisation above HPA target}
  J -->|Yes| K[Scale pods horizontally]
  J -->|No| L[Investigate storage latency and network path]
  H --> M[Measure again before the next change]
  I --> M
  K --> M
  L --> M
Only two branches out of six end in adding capacity. That ratio is roughly what I see in production too.

Scaling levers and what each one actually moves

Keep this table. It is the artifact from this Part worth returning to, and it is built around the question the exam keeps asking in different costumes, which is not how do I scale but which thing do I scale. Column four is the one that saves you, because most wasted optimisation effort is a correct lever applied to the wrong bottleneck.

Symptom Measurement that proves it Lever What it will not fix
App latency rises with traffic, nodes have headroomkubectl top pods shows pods near their requests, nodes below 60 percentHorizontalPodAutoscaler on the DeploymentA single threaded process, or a database that cannot take more connections
Pods Pending with Insufficient cpu or memoryScheduling events name the resource, sum of requests exceeds allocatableCluster Autoscaler, or a manual MachineDeployment replica increaseA pod whose single request exceeds one node, and a namespace already at its limits
One pod requests more than any node can offerRequest compared against node allocatable, and the VM Class bound to the namespaceBind a larger VM Class on the Supervisor, then change the node pool classAnything quickly, this is a rolling replacement of every node in the pool
Nodes idle overnight, cost unchangedUtilisation history in Prometheus, not kubectl topCluster Autoscaler with a sensible minimum sizeNodes pinned by pods with local storage or restrictive disruption budgets
Throughput flat while CPU sits lowvSAN or datastore latency in VCF Operations, plus PVC IOPSStorage policy change, or a different storage class for that workloadEvery scaling lever above, all of which make it slightly worse
HPA reports unknown as current utilisationkubectl get hpa shows an unknown target, kubectl top pods errorsRepair metrics-server, then set requests on the target DeploymentNothing else works until this is green, HPA is inert without it

Horizontal pod scaling comes first because it is cheapest and fastest. Note the requests block in this manifest, since without it the HPA has no denominator and reports unknown forever, which is the failure I have seen more than any other on this objective.

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: web-frontend namespace: storefront spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: web-frontend minReplicas: 3 maxReplicas: 18 metrics: – type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 behavior: scaleDown: stabilizationWindowSeconds: 300 $ kubectl -n storefront get hpa web-frontend NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE web-frontend Deployment/web-frontend <unknown>/70% 3 18 3 46s $ kubectl -n storefront describe hpa web-frontend | grep -i -A1 ScalingActive ScalingActive False FailedGetResourceMetric failed to get cpu utilization: missing request for cpu in container web

Add a CPU request to that container and the same HPA reports a real percentage within one sync interval, which defaults to fifteen seconds. Node level scaling is the next lever, and Part 22 covered the install in full, so here it is only the annotation surface that decides behaviour, applied on the Supervisor against the Cluster topology.

# Supervisor context, in the vSphere Namespace that owns the cluster $ kubectl config use-context ns-payments # Bounds live on the machineDeployment entry in the cluster topology spec: topology: workers: machineDeployments: – class: node-pool name: md-0 replicas: 3 metadata: annotations: cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "3" cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "9" # Manual scale of the same pool, which is what you use when you want a known answer $ kubectl scale machinedeployment pay-01-md-0-hx8vp –replicas=5 -n ns-payments machinedeployment.cluster.x-k8s.io/pay-01-md-0-hx8vp scaled $ kubectl get machinedeployment -n ns-payments NAME CLUSTER REPLICAS READY UPDATED UNAVAILABLE PHASE AGE VERSION pay-01-md-0-hx8vp pay-01 5 3 5 2 ScalingUp 44d v1.32.0+vmware.1
Oscillation warning: Cluster Autoscaler defaults to a ten minute delay after an addition before it considers scaling down, and a ten minute unneeded period before it removes a node, while a HPA scale down stabilisation window defaults to 300 seconds. Run an aggressive HPA against a tight autoscaler minimum and you get a cluster that adds a node, drops pods, waits ten minutes, removes the node, and repeats. In my lab that cycle produced 14 node creations across one eight hour synthetic load test that genuinely needed four. Widen the HPA stabilisation window before you touch any autoscaler flag.

Verification, rollback and failure signatures

Green here is not a running pod, it is a closed loop. Four checks prove that loop: metrics are being served, every HPA has a numeric current value, the node pool reports its replicas ready and updated in equal numbers, and the vSphere Namespace still has headroom against its configured limits. Miss the fourth and you will discover that your autoscaler maximum was fiction all along.

$ kubectl -n storefront get hpa web-frontend NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE web-frontend Deployment/web-frontend 64%/70% 3 18 7 38m $ kubectl get machinedeployment -n ns-payments NAME CLUSTER REPLICAS READY UPDATED UNAVAILABLE PHASE AGE VERSION pay-01-md-0-hx8vp pay-01 5 5 5 0 Running 44d v1.32.0+vmware.1 $ kubectl -n ns-payments describe resourcequota | grep -E "limits.cpu|limits.memory" limits.cpu 36 48 limits.memory 148Gi 192Gi $ kubectl -n kube-system logs deploy/cluster-autoscaler –tail=3 I0822 09:14:02.118 static_autoscaler.go:512] Scale up: setting group MachineDeployment/ns-payments/pay-01-md-0-hx8vp size to 5 I0822 09:19:14.902 static_autoscaler.go:340] Node pay-01-md-0-hx8vp-6b9c4f7d8-p2v8n registered, ready I0822 09:19:15.441 static_autoscaler.go:602] No unschedulable pods

Rollback on this objective is pleasantly boring, which is a good argument for measurement driven changes over configuration sprawl. Remove a HPA and the Deployment holds whatever replica count it last had, so scale it back explicitly rather than assuming. Remove the autoscaler annotations and the pool freezes at its current replica count, which is safe. Reverse a manual MachineDeployment scale by setting the old number, and expect a drain on the way down. A VM Class change is the only lever with no cheap reverse, because reverting it rolls every node again, which is why it belongs in a change window and never in an incident.

Error you will actually see Cause Remediation
error: Metrics API not availablemetrics-server is not running, or its APIService has no endpointsFix the scheduling reason on the metrics-server pod, then recheck the APIService
failed to get cpu utilization: missing request for cpuTarget container has no CPU request, so utilisation has no denominatorAdd resources.requests.cpu to the container and let the HPA resync
Reconcile failed on the prometheus packageinstallNo default storage class, so the Prometheus PVC never bindsAssign the storage policy to the vSphere Namespace, set the cluster default, reconcile
Package reconciliation fails after a cluster upgradeAutoscaler package minor version no longer matches the VKr minor versionInstall the matching autoscaler minor version, Broadcom documents a one to one relationship
Autoscaler logs max node group size reachedPool is at its max size annotation, or the namespace quota is exhaustedRaise the max annotation and the vSphere Namespace limits together, never one alone
New nodes appear but the pod stays PendingPod request exceeds node allocatable, which is fixed by the VM ClassBind a larger VM Class on the Supervisor and change the node pool class, or shrink the request

Exam focus for objective 5.5

Exam focus, objective 5.5: published wording is Optimize cluster performance using monitoring and scaling features. What it expects you to be able to do is read a set of metrics or an error string and select the correct scaling response, so the discrimination being tested is between HPA, Cluster Autoscaler, manual node pool scaling and a VM Class change. Expect it in matching and drag and drop item types, pairing a symptom with a remedy, and in point and click on vSphere Client screens where you bind a VM Class or inspect namespace resource limits. Multiple selection items tend to ask which prerequisites a given scaling feature has, where metrics-server for HPA and a default storage class for the Prometheus package are the two that carry weight. Trap that catches experienced admins: reaching for node capacity when the stem describes pods Pending because a single pod request exceeds node allocatable. Adding nodes is the vSphere reflex and it is wrong, because node size is set by the VM Class on the Supervisor and no horizontal action changes it. Second trap, smaller but common: treating kubectl top as a monitoring solution when it exposes a short live window with no history, so any stem mentioning trends, overnight idle or capacity planning is pointing at Prometheus or VCF Operations rather than at the resource metrics API.

Objective checkpoint

Q1. A Deployment has a HorizontalPodAutoscaler targeting 70 percent CPU utilisation. Replica count never moves and TARGETS reads unknown over 70 percent. Nodes are at 45 percent CPU. What is wrong, and where do you fix it?
Answer: the HPA cannot compute utilisation, either because the resource metrics API is unavailable or because the target container has no CPU request. Both are fixed inside the workload cluster, and neither is a capacity problem, so adding nodes changes nothing.

Q2. Cluster Autoscaler is enabled with a maximum of nine. A pod requesting 8 CPU stays Pending while three new nodes are created and then removed again. Which change resolves it?
Answer: bind a VM Class large enough to host an 8 CPU pod to the vSphere Namespace on the Supervisor and move the node pool onto it, or reduce the request. Nodes built from a 4 vCPU class can never satisfy that pod, so the autoscaler is adding capacity of the wrong shape.

Q3. A platform team wants to know whether a cluster is over provisioned overnight so they can lower the node pool minimum. Which monitoring layer answers that, and why is kubectl top insufficient?
Answer: Prometheus, or VCF Operations for the infrastructure view. Resource metrics through metrics-server serve a short live window intended for autoscaling decisions and kubectl top, with no retention, so a question about a time period cannot be answered from that layer at all.

Optimization order I would ship for this estate

Here is the war story that produced that table. A retail customer opened a severity two on a Thursday afternoon in November because checkout latency had tripled. Their platform team had already scaled the node pool from six to twelve over two days, which cost roughly 96 vCPU and 384 GB of committed namespace quota and moved p95 latency by 40 milliseconds in the wrong direction. I spent nineteen minutes on the numbers before touching anything and found aggregate requests at 31 percent of allocatable, a checkout Deployment with no CPU request at all, and a HPA that had been sitting at unknown over 70 for eleven days without anybody noticing. One request block on one container, one HPA resync, and that Deployment scaled itself from four replicas to fifteen inside two minutes. We took the node pool back to seven the following week. Total repair time once I stopped guessing was under half an hour, against two days of scaling that had made things marginally worse.

Verdict, and it argues with the most common advice on this objective: enable Cluster Autoscaler last, not first. Popular guidance treats the autoscaler as the answer to performance because it is the most visible feature in the chapter, and on a cluster with honest resource requests it is genuinely good. On a cluster without them it converts a workload problem into an infrastructure bill and hides the evidence while doing it. Order I would ship, and the order I would reason through an exam item in: repair the metrics pipeline, set requests that match observed usage, apply HPA, then and only then bound the node pool with an autoscaler, and treat VM Class changes as a planned design decision rather than a response to an alert. Avoid the pattern of raising the autoscaler maximum during an incident, because it always works for twenty minutes and always costs more than it saved.

Clean result on this objective looks like five things: kubectl top returns numbers for every node, every HPA in the cluster reports a numeric current value rather than unknown, MachineDeployment READY equals REPLICAS equals UPDATED, aggregate requests sit within about 15 points of measured usage, and the vSphere Namespace has visible headroom above the autoscaler maximum so that the ceiling you configured is the ceiling you get. Do this tonight in your own lab: deploy the stateless web Deployment with no CPU request, attach a HPA, watch it report unknown, then add the request and time how long it takes to scale. Then set the autoscaler maximum higher than the namespace CPU limit and generate load, so you see for yourself exactly which object refuses first. Component depth on every piece named here lives in the vSphere Kubernetes Service complete guide, autoscaler install and version pinning is in Part 22, and the lifecycle commands you will lean on while measuring are in Part 20. That closes the series. Thirty four Parts, one published objective each after the orientation, and the whole path is indexed on the VCAP-VKS exam guide. Go and book the exam.

VCAP-VKS Exam Series · Part 34 of 34
« Previous: Part 33  |  Guide

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading