| Question | Useful evidence |
|---|---|
| Which Pod currently uses the most CPU? | kubectl top pod |
| Which container inside a Pod uses the memory? | kubectl top pod --containers |
| Why did a container restart? | Pod status, last termination state, and events |
| Was CPU usage high fifteen minutes ago? | A time-series monitoring system |
| Is the container being CPU-throttled? | cgroup or Prometheus container metrics |
| Is Metrics Server itself unhealthy? | APIService status, Deployment status, and logs |
To find answers to these questions, the monitoring system depends on collection, transport, storage, query, and interpretation capabilities.
1
2
kubectl create namespace rke2-monitoring
kubectl get namespace rke2-monitoring
All workload resources used in this lecture belong in this namespace.
Create resource-observer.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
apiVersion: v1
kind: Pod
metadata:
name: resource-observer
namespace: rke2-monitoring
labels:
app: resource-observer
spec:
restartPolicy: Always
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 20m
memory: 24Mi
limits:
cpu: 100m
memory: 64Mi
- name: cpu-worker
image: busybox:1.36
command:
- /bin/sh
- -c
- |
echo "Starting bounded CPU workload"
while true; do
i=0
while [ "$i" -lt 50000 ]; do
i=$((i + 1))
done
done
resources:
requests:
cpu: 50m
memory: 8Mi
limits:
cpu: 200m
memory: 32Mi
- name: memory-holder
image: busybox:1.36
command:
- /bin/sh
- -c
- |
echo "Allocating a file in a memory-backed volume"
dd if=/dev/zero of=/memory/payload bs=1M count=48
sleep 36000
volumeMounts:
- name: memory-data
mountPath: /memory
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
cpu: 50m
memory: 96Mi
volumes:
- name: memory-data
emptyDir:
medium: Memory
sizeLimit: 64Mi
Deploy the pod
1
2
3
kubectl apply -f resource-observer.yaml
kubectl -n rke2-monitoring wait --for=condition=Ready pod/resource-observer --timeout=120s
kubectl -n rke2-monitoring get pod resource-observer -o wide
The containers provide three different behaviors:
web provides a low-usage application container;cpu-worker continuously requests CPU time but is limited to 200m;memory-holder writes 48Mi to a memory-backed volume.The resource limits bound the load generated by this hands-on exercise.
flowchart TB
CG["Linux cgroups / CRI stats"] --> KL["kubelet on each node"]
KL -->|"resource endpoint"| MS["RKE2 packaged Metrics Server"]
MS --> MA["Kubernetes Metrics API"]
MA --> TOP["kubectl top"]
MA --> HPA["Horizontal Pod Autoscaler"]
kubectl top and autoscalers query the Metrics API.This pipeline primarily exposes:
It does not provide a long-term history, application request rates, traces, or logs.
CPU
1 CPU is one Kubernetes CPU unit.1000m is one CPU; 250m is one quarter of a CPU.kubectl top represents recent consumption.Memory
Ki, Mi, or Gi.| Value | Source | Purpose |
|---|---|---|
| Request | resources.requests in the Pod specification | The scheduler uses it when choosing a node |
| Limit | resources.limits in the Pod specification | The runtime enforces it with Linux cgroups |
| Current usage | Runtime/cgroup statistics collected through the kubelet and Metrics Server | Shows recent CPU and memory use |
| Historical usage | Samples stored by Prometheus or another monitoring system | Shows changes over time |
Inspect the resource configuration of all three containers:
1
kubectl -n rke2-monitoring get pod resource-observer -o jsonpath='{range .spec.containers[*]}{.name}{"\n requests: "}{.resources.requests}{"\n limits: "}{.resources.limits}{"\n\n"}{end}'
resource-observer.cpu-worker CPU limit is 200m.memory-holder memory limit is 96Mi.The node measures container usage. The kubelet exposes the measurements, Metrics Server collects them, and kubectl top displays them:
1
kubectl top pod resource-observer -n rke2-monitoring --containers
Illustrative output:
1
2
3
4
POD NAME CPU(cores) MEMORY(bytes)
resource-observer web 1m 8Mi
resource-observer cpu-worker 198m 1Mi
resource-observer memory-holder 1m 49Mi
Your values will differ. The expected pattern is that cpu-worker uses the most CPU and memory-holder uses the most memory. Metrics Server does not preserve earlier samples.
Prometheus or another monitoring system can collect and store repeated samples:
1
2
3
4
5
TIME CONTAINER CPU MEMORY
10:00 cpu-worker 195m 1Mi
10:00 memory-holder 1m 49Mi
10:05 cpu-worker 199m 1Mi
10:05 memory-holder 1m 49Mi
This history can show whether usage is stable, increasing, or temporary.
For the illustrative output above:
cpu-worker uses more than its 50m request. This is allowed.200m limit.memory-holder uses more than its 16Mi request.Its usage remains below its 96Mi limit.
RKE2 distributes Metrics Server as a packaged component named rke2-metrics-server.
On an RKE2 server, packaged component manifests are managed below:
1
/var/lib/rancher/rke2/server/manifests/
RKE2 rewrites packaged manifests when the service starts. Do not directly edit those generated files. RKE2 configuration or the supported chart customization mechanism should be used when a packaged component must be changed.
The component can be disabled through RKE2 server configuration:
1
2
disable:
- rke2-metrics-server
Disabling a packaged component actively removes it. This hands-on exercise assumes it is not disabled.
Metrics Server supplies current CPU and memory measurements for:
kubectl top;It is not a replacement for Prometheus or another monitoring platform.
| Capability | Metrics Server | Prometheus-style monitoring |
|---|---|---|
| Current node CPU and memory | Yes | Yes |
| Current Pod/container CPU and memory | Yes | Yes |
| Historical queries | No | Yes |
| Dashboards | No | Yes |
| Alert rules | No | Yes |
| Application metrics | No | Yes, when exported and scraped |
| Detailed throttling/restart series | No | Yes, when collected |
In an HA design, server nodes may be tainted so ordinary workloads do not run on them. If every available node is restricted to critical add-ons, Metrics Server can remain Pending until an eligible agent node is added.
Always inspect the actual scheduling event:
1
kubectl -n kube-system describe pod -l app.kubernetes.io/name=metrics-server
Resource metrics behavior depends on the RKE2, Kubernetes, and Metrics Server versions. Record them before troubleshooting.
1
2
3
kubectl version
kubectl get nodes -o wide
kubectl -n kube-system get deployment rke2-metrics-server -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
1
2
3
kubectl -n kube-system get addons.k3s.cattle.io | grep metrics
kubectl -n kube-system get deployment rke2-metrics-server
kubectl -n kube-system get pods -l app.kubernetes.io/name=metrics-server -o wide
The expected state is:
Running and ready;If the label query returns no Pod, inspect the Deployment’s selector instead of guessing:
1
2
kubectl -n kube-system get deployment rke2-metrics-server -o jsonpath='{.spec.selector.matchLabels}{"\n"}'
kubectl -n kube-system get pods --show-labels | grep metrics-server
1
2
kubectl get apiservice | grep metrics.k8s.io
kubectl api-versions | grep metrics.k8s.io
Inspect the registered APIService:
1
kubectl describe apiservice $(kubectl get apiservice -o name | grep metrics.k8s.io | head -n 1)
Look for an Available condition with status True.
Query API discovery directly:
1
kubectl get --raw /apis/metrics.k8s.io/
This tests the API path without relying on the formatting performed by kubectl top.
Confirm that the running hands-on Pod has metrics:
1
kubectl top pod resource-observer -n rke2-monitoring --containers
1
kubectl -n kube-system logs deployment/rke2-metrics-server --tail=100
Common failure categories include:
Record the exact message. Avoid applying broad insecure TLS workarounds before identifying the certificate or addressing problem.
Metrics may take a short time to appear after a node or Pod starts.
1
2
3
4
kubectl top nodes
kubectl top pods -A
kubectl top pods -A --sort-by=cpu
kubectl top pods -A --sort-by=memory
Questions:
kubectl top node reports recent usage. Node objects report capacity and allocatable resources.
1
kubectl get nodes -o custom-columns='NAME:.metadata.name,CPU:.status.capacity.cpu,CPU-ALLOCATABLE:.status.allocatable.cpu,MEMORY:.status.capacity.memory,MEMORY-ALLOCATABLE:.status.allocatable.memory'
allocatable is the resource amount available for Pods after Kubernetes accounts for resources reserved for the node and system.
Do not calculate production headroom from a single kubectl top sample. Capacity planning normally uses historical percentiles, workload growth, and failure scenarios.
1
2
3
kubectl top pod -n kube-system
kubectl top pod -n rke2-monitoring
kubectl top pod -n rke2-monitoring -l app=resource-observer --containers
Useful filters include:
--containers for attribution inside multi-container Pods;Allow the metrics pipeline to collect samples, then run:
1
2
kubectl top pod -n rke2-monitoring
kubectl top pod resource-observer -n rke2-monitoring --containers
Repeat the container view several times:
1
2
3
4
5
for sample in 1 2 3 4 5; do
date
kubectl top pod resource-observer -n rke2-monitoring --containers
sleep 10
done
Expected observations:
cpu-worker should dominate CPU consumption;memory-holder should dominate memory consumption;Display each container’s requests and limits:
1
kubectl -n rke2-monitoring get pod resource-observer -o jsonpath='{range .spec.containers[*]}{.name}{"\n requests: "}{.resources.requests}{"\n limits: "}{.resources.limits}{"\n\n"}{end}'
Complete the following for each container:
| Container | CPU request | CPU limit | Observed CPU | Memory request | Memory limit | Observed memory |
|---|---|---|---|---|---|---|
web | ||||||
cpu-worker | ||||||
memory-holder |
Interpret the results:
The cpu-worker has a 200m CPU limit and continuously asks for CPU time. Its observed usage should remain near the configured boundary, subject to measurement windows and node conditions.
1
kubectl top pod resource-observer -n rke2-monitoring --containers
This is evidence that usage is bounded, but kubectl top does not directly report a throttling counter. To prove CPU throttling, use cgroup statistics or a monitoring system that collects metrics such as throttled CFS periods.
Create memory-failure.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: Pod
metadata:
name: memory-failure
namespace: rke2-monitoring
labels:
app: memory-failure
spec:
restartPolicy: Never
containers:
- name: allocator
image: python:3.12-alpine
command:
- python3
- -c
- |
import time
print("Allocating 96 MiB with a 64 MiB container limit", flush=True)
payload = bytearray(96 * 1024 * 1024)
print(len(payload), flush=True)
time.sleep(3600)
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
Apply and watch it:
1
2
kubectl apply -f memory-failure.yaml
kubectl -n rke2-monitoring get pod memory-failure -w
Exit the watch with Ctrl-C after the container terminates.
Inspect status and events:
1
2
3
kubectl -n rke2-monitoring describe pod memory-failure
kubectl -n rke2-monitoring get pod memory-failure -o jsonpath='{.status.containerStatuses[0].state.terminated.reason}{"\n"}'
kubectl -n rke2-monitoring get events --sort-by=.lastTimestamp
Attempt to inspect current metrics:
1
kubectl top pod memory-failure -n rke2-monitoring --containers
The terminated container may disappear before Metrics Server exposes a useful sample. This demonstrates an important limitation:
Current-state metrics alone may not preserve the evidence needed after a short failure.
The Pod’s termination reason and events are the stronger evidence here. Historical monitoring would make it possible to correlate memory growth with the termination.
A complete diagnosis includes:
kubectl top may not retain a sample;Possible corrections depend on the cause:
Simply increasing the limit can postpone a leak and move the failure to the node level.
Discover the version served by the cluster:
1
kubectl api-versions | grep metrics.k8s.io
Save the first advertised version:
1
2
3
4
5
METRICS_VERSION=$(
kubectl api-versions |
awk -F/ '/^metrics.k8s.io\// {print $2; exit}'
)
echo "$METRICS_VERSION"
Query resource-observer through the raw Metrics API:
1
kubectl get --raw "/apis/metrics.k8s.io/${METRICS_VERSION}/namespaces/rke2-monitoring/pods/resource-observer" | python3 -m json.tool
Locate:
timestamp;window;kubectl top is a convenient client for these API data, not the collector.
On an RKE2 node, the bundled crictl can query containerd through the CRI socket:
1
sudo /var/lib/rancher/rke2/bin/crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock stats
Locate the resource-observer Pod sandbox and its containers:
1
2
3
POD_ID=$(sudo /var/lib/rancher/rke2/bin/crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock pods --name resource-observer -q)
sudo /var/lib/rancher/rke2/bin/crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock ps --pod "$POD_ID"
The runtime view and kubectl top may differ because:
Agreement in broad behavior is more meaningful than exact equality between two asynchronous samples.
On the node hosting resource-observer:
1
kubectl -n rke2-monitoring get pod resource-observer -o wide
Then, on that node:
1
sudo systemd-cgtop
Exit with q.
This is a host-level view. It is useful when the Kubernetes API or Metrics Server is unavailable, but translating host cgroups back to Kubernetes workloads requires care.
Use a historical monitoring system when the question includes:
A common RKE2/Rancher monitoring stack uses:
ServiceMonitor and PodMonitor resources for scrape discovery;Monitoring stacks consume meaningful CPU, memory, and storage themselves. Check the supported Rancher/RKE2 release documentation and cluster capacity before installing one. Do not add a full stack merely to make kubectl top work.