← Back to Blog

Kubernetes Resource Management

Anything that needs to run efficiently needs to be managed. Kubernetes already has a mechanism to automatically spread pods across nodes evenly, but that's only one part of resource management.

Kubernetes resource management overview

Why do we need to manage resources in Kubernetes?

In practice there are quite a few other factors we need to think about, since Kubernetes doesn't come with the ability to handle them by default. Managing resources effectively in Kubernetes helps us:

  • Save on operating costs (Cost): This is probably the factor most people care about. Managing resources effectively can save a huge amount on cluster operating costs. I've seen many cases where you can optimize 30 to 70% of cost just by managing resources more effectively.
  • Improve stability: When applications get sensible resource allocation, those resources run more stably.

In this post, I'll introduce the concepts that help you manage resources in Kubernetes, along with some of my own personal experience with each concept and tool.

Allocatable Resources (Node)

Allocatable Resources is info shown on each Kubernetes node. This figure shows how much resource you can actually use on each node. To view this info, run:

kubectl describe node <node-name>

Example output looks like this:

Name:               node-pool-1
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/hostname=node-pool-1
                    node-role.kubernetes.io/worker=true
Annotations:        csi.volume.kubernetes.io/nodeid: {"ebs.csi.aws.com":"i-0a12345b67890cdef"}
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Mon, 10 Aug 2024 10:45:23 +0700
Taints:             node.kubernetes.io/unreachable:NoSchedule
Unschedulable:      false
Lease:
  HolderIdentity:  node-pool-1
  AcquireTime:     Mon, 13 Oct 2024 10:45:35 +0700
  RenewTime:       Mon, 13 Oct 2024 11:00:35 +0700

Capacity: # <= The node's actual specs
  cpu:                4000mi
  ephemeral-storage:  100Gi
  hugepages-2Mi:      0
  memory:             16Gi
  pods:               110
Allocatable: # <= The amount of resource actually usable
  cpu:                3496mi
  ephemeral-storage:  95Gi
  hugepages-2Mi:      0
  memory:             15Gi
  pods:               110

System Info:
  Machine ID:                 1234567890abcdef
  System UUID:                98765432-1A2B-3C4D-5E6F-789012345678
  Boot ID:                    11223344-5566-7788-99AA-BBCCDDEEFF00
  Kernel Version:             5.11.0-1019-aws
  OS Image:                   Ubuntu 20.04.6 LTS
  Operating System:           linux
  Architecture:               amd64
  Container Runtime Version:  docker://20.10.8
  Kubelet Version:            v1.24.3
  Kube-Proxy Version:         v1.24.3

Conditions:
  Type             Status    LastHeartbeatTime                 LastTransitionTime                Reason             Message
  ----             ------    -----------------                 ------------------                ------             -------
  MemoryPressure   False     Mon, 13 Oct 2024 11:00:05 +0700    Mon, 10 Aug 2024 10:45:23 +0700   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     False     Mon, 13 Oct 2024 11:00:05 +0700    Mon, 10 Aug 2024 10:45:23 +0700   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure      False     Mon, 13 Oct 2024 11:00:05 +0700    Mon, 10 Aug 2024 10:45:23 +0700   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True      Mon, 13 Oct 2024 11:00:05 +0700    Mon, 10 Aug 2024 10:45:23 +0700   KubeletReady                 kubelet is posting ready status

Addresses:
  InternalIP:  192.168.1.15
  Hostname:    node-pool-1

Non-terminated Pods:          (12 in total)
  Namespace                   Name                            CPU Requests  CPU Limits   Memory Requests  Memory Limits  AGE
  ---------                   ----                            ------------  ----------   ---------------  -------------  ---
  kube-system                 kube-proxy-abc123               100m (2%)     200m (5%)    64Mi (0%)        128Mi (0%)     45d
  kube-system                 coredns-12345abc                100m (2%)     100m (2%)    70Mi (0%)        70Mi (0%)      45d
  kube-system                 fluentd-abc123                  200m (5%)     200m (5%)    200Mi (1%)       400Mi (2%)     45d
  default                     nginx-deployment-abc123         500m (12%)    1 (25%)      512Mi (3%)       1Gi (6%)       10d

Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                1.6 (40%)    2.5 (62%)
  memory             846Mi (5%)   1.598Gi (10%)
  ephemeral-storage  0 (0%)       0 (0%)
  hugepages-2Mi      0 (0%)       0 (0%)

Events:
  Type    Reason                   Age                  From                     Message
  ----    ------                   ----                 ----                     -------
  Normal  NodeHasSufficientMemory   45d                  kubelet                  Node node-pool-1 status is now: NodeHasSufficientMemory
  Normal  NodeHasNoDiskPressure     45d                  kubelet                  Node node-pool-1 status is now: NodeHasNoDiskPressure
  Normal  NodeReady                 45d                  kubelet                  Node node-pool-1 status is now: NodeReady

Pay attention to the two sections Capacity and Allocatable.

  • Capacity shows the node's actual resources. For example, in the output above it's a node with 4 vCPU - 16 GB Memory - 100 GB Disk. This is the spec you requested from your cloud provider if you're on the cloud.
  • In practice, though, once you add a node with the above spec to a K8s cluster, you can't use 100% of these resources. The actual amount of resource available for running applications in K8s on this node is shown in Allocatable. A lot of people mistakenly think that ordering a 4 vCPU node means they can use the entire resource, but in reality K8s reserves a portion to run system components like OS processes or K8s's own components. In this example, the usable resource is lower than Capacity, at 3496mi CPU - 15GB Memory - 95GB Disk.

As far as I know, there's no fixed formula for computing Allocatable from Capacity — you'll have to check directly for each node type. That said, the Allocatable/Capacity ratio tends to be larger on bigger-spec nodes, and smaller on smaller-spec nodes. So sometimes using a few large nodes gives you more usable resource than running many small nodes with the same total CPU/Memory spec!

Choosing the right node type for a Kubernetes cluster also matters a lot.

  • If your cluster runs microservices whose pods use a small amount of resource, using many small-spec nodes is a good fit, since it's more convenient for node autoscaling and you can use up the entire resource on that node.
  • If your cluster runs several different kinds of applications, like microservices + databases, you might approach it using multiple Node Pools for this cluster. Node pool 1 with a small spec running microservices, Node pool 2 with a higher spec running the database, since databases typically use a large amount of memory.

Resource Quota (namespace)

Resource Quota limits the total resources a namespace can use. This is very useful when you have multiple teams working in the same cluster and want to make sure no team hogs the entire resource pool. For example, in clusters used by multiple dev teams, you can assign each dev team a namespace and limit the amount of resource usable in each namespace via Resource Quota.

The manifest for a Resource Quota looks like this:

# resource-quota.yml file
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-team-quota
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: "8Gi"
    limits.cpu: "8"
    limits.memory: "16Gi"

With the example above, every pod in the namespace must satisfy the conditions defined in the manifest file:

  • The namespace can have at most 10 pods
  • Total CPU request and limit in the namespace is 4 and 8
  • Total Memory request and limit in the namespace is 8GB and 16GB

Run the following command to apply the Resource Quota:

kubectl apply -f resource-quota.yml --namespace dev-team-a

Resource request/limit

Resource request and limit are configurations set for a container inside a pod when deployed on Kubernetes. This config defines how resources get allocated to that container. A Resource request and limit config looks like this:

apiVersion: v1
kind: Pod
metadata:
  name: resource-example
spec:
  containers:
    - name: app
      image: busybox
      resources:
        requests:
          memory: "256Mi"
          cpu: "500m"
        limits:
          memory: "512Mi"
          cpu: "1"
  • Resource Requests: The minimum resource a container requests. The scheduler uses this figure to determine whether a node has enough resource to run the pod.
  • Resource Limits: The maximum resource a container is allowed to use. If a container exceeds this limit, it gets throttled (for CPU) or OOMKilled (for memory).

In the example above:

  • The container requests at least 0.5 CPU and 256Mi Memory.
  • The container may not use more than 1 CPU and 512Mi Memory. If it uses more than 512Mi Memory, Kubernetes will kill this container.

Configuring resource requests and limits is a fairly tricky balancing act between cost and performance, since setting the resource just a little too low can lead to the pod getting killed for using too much Memory.

Resource request and limit tradeoff, source: CAST AI

Source: CAST AI

Before figuring out how to configure resource requests and limits appropriately, we first need to know about the Quality of Service concept in Kubernetes.

Quality of Service (QoS)

Quality of Service (QoS) helps Kubernetes prioritize resource allocation for pods based on their configured resource request and limit. Kubernetes classifies QoS into three groups: Guaranteed, Burstable, and Best-Effort. Basically, this is how we rank how important each pod is in Kubernetes. When a node runs out of resources, it will kill the lowest-priority pods first to push them onto other nodes, until the node's usage stabilizes, at which point it stops. In this case, you'll see the affected pods fall into the Evicted state.

So how do you define a pod's Quality of Service?

Kubernetes QoS classes
  • Guaranteed: If both the CPU and memory request and limit are equal, the pod gets assigned QoS "Guaranteed". This is the highest priority level when the system runs short on resources.
  • Burstable: If the request is lower than the limit, the pod belongs to the "Burstable" group, with medium priority.
  • Best-Effort: If a pod doesn't configure a request or limit at all, it gets assigned QoS "Best-Effort", the lowest priority. This pod will be affected the most when resources become scarce.

Which kinds of applications should use which Quality of Service?

  • In my experience, Stateful applications like databases, message queues, or critical applications like payments, etc. should be configured to use QoS Guaranteed. The resource level request = limit should be configured at the threshold matching the pod's highest usage over the past week, plus 10%.

Example: if a Redis pod used as a cache has the following memory usage thresholds over the past week: Min: 1.1 GB, Avg: 1.4 GB, Max: 1.9 GB, then I'd configure resource request = resource limit = 2.1 GB

  • For QoS Burstable, I use this for Stateless applications with a high tolerance for failure (in simple terms, applications that still shut down gracefully when killed suddenly, without causing errors). Usually APIs, cron jobs, etc.

Example: a Python API pod has the following memory usage thresholds over the past week: Min: 300 MB, Avg: 350 MB, Max: 440 MB. I'd configure the memory request as 350 MB and the memory limit as 450MB

Configuring the memory request equal to the average resource usage helps ensure the pod gets enough resource to operate normally. The memory limit will be a bit higher than the max memory point to make sure that during peak hours the pod is also unlikely to get killed, keeping the system more stable.

  • As for QoS Best-Effort, I prioritize this for pods providing secondary features to Kubernetes, like: Logging, Tracing, Monitoring, etc. Since when these pods get killed, the worst case is losing a few seconds of metrics, with no critical issue at all.

Wrapping up

In short, managing resources in Kubernetes is an important factor for ensuring applications run efficiently and don't affect other applications in the same cluster. By using Resource Requests and Limits, Resource Quotas, along with the QoS mechanism, you can tightly control how resources get used, thereby optimizing the system's performance and stability.

If you found this post useful and it brought you value, don't hesitate to give me an Upvote + a Follow. That's the motivation for me to share more knowledge about Kubernetes and DevOps.

Have a nice day!

If you're running into technical challenges or need help with systems, DevOps tools, I'm confident I can help. Get in touch at hoangviet.io.vn

Have thoughts on this?

I'd love to hear your perspective. Send me a message.

Get in touch