Create your own
Lesson illustration

Optimizing Container Performance with Kubernetes Resources

Introduction

Welcome to your second lesson. In our previous session, we established that the competing consumer pattern—running multiple replicas of your subscriber service—is the architectural solution to your Pub/Sub message backlog. By processing messages in parallel, you can significantly increase throughput.

This naturally leads to the question: how do we run these replicas effectively? Before Kubernetes can run even one copy of your application (a Pod), let alone scale it to many, it needs to understand its resource requirements. Simply running more replicas without defining their resource needs can lead to instability, unpredictable performance, and high costs.

In this lesson, we will focus on the fundamental mechanism for managing application resources in Kubernetes. You will learn to define Kubernetes resource requests and limits for a container. Mastering this concept is essential for ensuring the predictable performance of each replica and is a mandatory prerequisite for implementing the autoscaling you need.

Why Declare Resources? The Core Problem

When multiple application pods run on the same virtual machine (a Node), they share that machine's CPU and memory. Without any controls, a single, misbehaving pod could consume all available resources, starving other applications and even crashing the node itself. This is known as the "noisy neighbor" problem.

To solve this, Kubernetes requires you to declare your resource intentions. This serves two primary purposes:

  1. Scheduling: To ensure a pod has the minimum resources it needs to function.
  2. Enforcement: To prevent a pod from consuming more than its fair share of resources.

These two purposes are handled by two distinct declarations: requests and limits.

Requests vs. Limits: A Fundamental Distinction

Let's break down these two concepts. They are the foundation of resource management in Kubernetes.

  • Resource requests specify the minimum guaranteed amount of CPU and memory for a container.

    • Purpose: The Kubernetes scheduler uses this value to find a node with enough available capacity to run your pod. If no node can satisfy the pod's total resource requests, the pod will remain in a Pending state until resources become available.
    • Analogy: Think of a request as a reservation. You're telling Kubernetes, "I need at least this much CPU and memory to function correctly. Please find me a machine that can guarantee it."
  • Resource limits specify the maximum amount of CPU and memory a container is allowed to use.

    • Purpose: The kubelet (the Kubernetes agent running on each node) enforces this limit at runtime.
    • Analogy: A limit is a hard cap. You're telling Kubernetes, "My application should never, under any circumstances, use more than this amount of resource."

The following video provides an excellent overview of these concepts.

All You Need to Know in 12 Minutes: Pods' Requests and Limits in Kubernetes

This video, 'All You Need to Know in 12 Minutes: Pods' Requests and Limits in Kubernetes', clearly explains why we need these settings and how they work.

Please watch from the beginning until 05:28. Focus on understanding: The business reasons for setting resources (cost, stability). The basic syntax for defining requests and limits in a pod's YAML configuration. The units used for CPU (millicores) and memory (bytes).

As the video explains, requests and limits are configured within the resources section of a container's definition in your Deployment YAML. Here's a typical example:

spec:
  containers:
  - name: pubsub-subscriber
    image: my-subscriber-image:latest
    resources:
      requests:
        memory: "128Mi"
        cpu: "250m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  • CPU: 250m means 250 millicores, or 0.25 of a CPU core. 1000m would be one full core.
  • Memory: 128Mi means 128 mebibytes.

The Consequences of Exceeding Limits

What happens when a container tries to use more resources than its limit is critically different for CPU and memory. This is arguably the most important concept to grasp for ensuring application stability, especially given your service's high memory usage.

  • CPU Limit Exceeded: CPU is a "compressible" resource. If a container hits its CPU limit, Kubernetes throttles it. The application will not be terminated, but its performance will be degraded as it's given fewer CPU cycles. This can manifest as increased latency or general slowness.

  • Memory Limit Exceeded: Memory is an "incompressible" resource. There is no way to "throttle" memory usage. If a container tries to allocate memory beyond its limit, it is terminated by the node with an "Out of Memory" event, commonly known as OOMKill. If this pod is part of a Deployment, Kubernetes will automatically try to restart it, but if the underlying issue isn't fixed, it can lead to a cycle of crashes (CrashLoopBackOff).

This visual perfectly illustrates the difference:

Kubernetes Pod Resource Requests and Limits
This diagram shows that exceeding the CPU limit leads to throttling, while exceeding the memory limit results in the process being killed (OOMKilled).

To solidify your understanding, please continue with the video you started earlier.

All You Need to Know in 12 Minutes: Pods' Requests and Limits in Kubernetes

This next segment of the video details exactly how Kubernetes handles requests during scheduling and what happens when limits are breached during runtime.

Watch from 05:28 to 07:49. Pay close attention to the different outcomes for exceeding CPU versus memory limits.

This distinction is directly relevant to your goal. Simply increasing replicas without proper memory limits could lead to your pods being constantly OOMKilled as they process messages, creating instability instead of improving throughput.

The Trade-off: Predictability vs. Efficiency

How you set requests and limits relative to each other represents a trade-off between performance predictability and resource efficiency (cost). This relationship defines a pod's Quality of Service (QoS) Class.

  1. Guaranteed (requests == limits):

    • Pros: Highest predictability. The pod is guaranteed the resources it needs and is the last to be terminated if a node is under pressure.
    • Cons: Potentially inefficient and costly. You are reserving (and often paying for) resources that the application may only use during peak moments.
  2. Burstable (requests < limits):

    • Pros: A good balance of efficiency and predictability. The pod is guaranteed its requested amount but can "burst" to use more available resources on the node up to its limit. This allows for higher density of pods on a node, reducing costs.
    • Cons: Performance is less predictable than Guaranteed. If multiple pods on a node try to burst simultaneously, they will be throttled or could be terminated if memory runs out.
  3. BestEffort (no requests or limits set):

    • Pros: Runs on any available space.
    • Cons: No performance guarantees. These are the first pods to be terminated under node pressure. This should be avoided for all production workloads.

The following article offers a deeper perspective on this trade-off.

The Case for Kubernetes Resource Limits: Predictability vs. Efficiency

The official Kubernetes blog post 'The Case for Kubernetes Resource Limits' frames this as a choice between predictability and efficiency, which is a useful mental model.

Read the section 'Configuring the limits'. It discusses the two main strategies: setting requests = limits (Guaranteed QoS) and giving a small amount of headroom (Burstable QoS).

Now, let's watch the final segment of the video, which ties these QoS concepts together.

All You Need to Know in 12 Minutes: Pods' Requests and Limits in Kubernetes

This last part of the video explains how the relationship between requests and limits determines the QoS class and why that matters when a node is under pressure.

Please watch from 07:49 to 10:57. Focus on how Kubernetes uses the QoS class to decide which pods to kill first during resource shortages.

How to Determine the Right Values

You might now be asking: how do I choose the right values for requests and limits? The answer is to measure your application's actual usage under a realistic load.

While we will cover the specific tools to do this in the next lesson, the general strategy is:

  1. Run your application without limits (or with very high ones) in a test environment.
  2. Simulate a realistic production load (e.g., publish a backlog of messages to your Pub/Sub topic).
  3. Monitor the CPU and memory consumption over time.
  4. Set requests based on the application's steady-state or average usage.
  5. Set limits to accommodate expected peaks, leaving some headroom. For memory, it's wise to be more generous to avoid OOMKills.

GKE provides tools to help with this process. The Optimization tab in the GKE console can visualize resource usage and even provide recommendations.

Optimization in GKE

This video from Google Cloud Tech demonstrates how GKE helps with 'workload right-sizing'. It provides a fantastic visual of how requested resources relate to actual usage.

Watch the following two clips: (05:24 - 06:56): This shows examples of misconfigured workloads. Pay attention to the 'beta workload', where memory usage exceeds requests—a dangerous situation that could lead to OOMKills. (07:06 - 08:40): This demonstrates how GKE can provide Vertical Pod Autoscaler (VPA) recommendations to help you set appropriate request values based on historical usage.

This "right-sizing" is the optimization problem at the heart of running reliable and cost-effective services on Kubernetes.

Conclusion

In this lesson, you've learned the critical role of resource requests and limits in Kubernetes. Without them, scaling is a gamble. With them, you provide the stability and predictability needed for a production system.

Key Takeaways:

  • Requests are used for scheduling and guarantee a minimum amount of resources. The sum of requests on a node cannot exceed its capacity.
  • Limits are enforced at runtime and define the maximum resources a container can use.
  • Exceeding a CPU limit leads to throttling (slower performance).
  • Exceeding a memory limit leads to an OOMKill (container termination). This is a critical distinction for application stability.
  • The relationship between requests and limits determines the QoS class (Guaranteed, Burstable, BestEffort), which influences both cost-efficiency and a pod's resilience to node pressure.
  • The best way to set these values is to measure your application's performance under a realistic load.

You now have the conceptual tools to define a robust "template" for your subscriber pods. The next step is to get concrete data. In the next lesson, we will get hands-on and learn to use kubectl top and kubectl describe to inspect the real-time resource usage of a running pod. This will give you the data needed to confidently set requests and limits for your service.

Can't find a good explanation? Sign up and we'll make it for you

Sign up