Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 87

Resource Management for Pods

Recommended resources

Hello! Welcome to your next lesson in the "Containerization & Kubernetes" module.

In our last session, we implemented liveness and readiness probes to make our Spring Boot application self-healing. We taught Kubernetes how to check if our application is healthy and ready for traffic. Now, we'll address another critical aspect of production readiness: resource management. A healthy application is of little use if it consumes all available node resources, causing instability for itself and other services.

This lesson focuses on the learning outcome: Configure resource requests and limits for Pods to ensure cluster stability and efficient resource usage. Mastering this topic is crucial for senior-level interviews, as it demonstrates your ability to design performant, stable, and cost-effective systems.

1. The Core Concepts: Requests vs. Limits

In Kubernetes, we manage a container's resource consumption using two primary parameters for both CPU and memory:

  • Requests: This is the amount of resources that Kubernetes guarantees for your container. The Kubernetes scheduler uses this value to find a node with enough available capacity to run your Pod. Think of it as reserving a minimum amount of resources.
  • Limits: This is the maximum amount of resources your container is allowed to use. The container runtime enforces this limit.

It's vital to understand the difference in how CPU and memory are handled:

  • CPU is a compressible resource. If a container hits its CPU limit, it gets throttled, meaning its CPU access is restricted. The application slows down but continues to run.
  • Memory is an incompressible resource. If a container tries to use more memory than its limit, it is terminated by the system. In Kubernetes, this results in an OOMKilled (Out of Memory Killed) error, and the container is restarted.

To understand the mechanics of how Kubernetes uses requests for planning and limits for enforcement, let's watch a segment from a CNCF talk that goes "under the hood."

Resource Requests and Limits Under the Hood: The Journey of a Pod Spec - Kohei Ota & Kaslin Fields

The video 'Resource Requests and Limits Under the Hood' explains the journey of a pod spec, clarifying the distinct roles of requests for scheduling and limits for enforcement.

Watch the video from 01:51 to 12:15. As you watch, focus on: The role of requests in the scheduler's decision-making process (planning). The role of limits in enforcement by the kubelet and container runtime using Linux cgroups. The consequences of exceeding limits: CPU throttling vs. OOM kills. How CPU cores are translated into cgroup settings (CFS quota/period).

As the video explained, these settings also determine a Pod's Quality of Service (QoS) class, which Kubernetes uses to decide which Pods to evict first when a node is under resource pressure:

  • Guaranteed: requests are set equal to limits for all resources. These are the last Pods to be killed.
  • Burstable: At least one resource has a request that is lower than its limit.
  • BestEffort: No requests or limits are set. These are the first Pods to be killed.

2. Practical Consequences of Misconfiguration

Theory is important, but seeing the real-world impact of these settings is crucial. Let's explore the common failure modes you'll encounter in production and will almost certainly be asked about in an interview.

The following video provides excellent hands-on demonstrations of what happens when resource settings go wrong.

Day 16/40 - Kubernetes Requests and Limits - CKA Full Course 2025

The video 'Kubernetes Requests and Limits' by Tech Tutorials with Piyush provides clear, practical demonstrations of common failure scenarios related to resource misconfiguration.

Please watch the sections from 12:27 to 16:04. They demonstrate two key scenarios: Exceeding the Memory Limit (12:27 - 14:31): Observe how a Pod that tries to use more memory than its limit is terminated with an OOMKilled status. Requesting Unavailable Resources (14:31 - 16:04): See what happens when a Pod requests more resources than any single node can provide. Notice that it gets stuck in a Pending state.

Let's summarize the key scenarios:

Scenario Pod Status Reason
Container exceeds its memory limit OOMKilled then CrashLoopBackOff The Linux kernel's OOM killer terminates the process. Kubernetes then repeatedly tries to restart it.
Container exceeds its CPU limit Running The container is throttled, leading to degraded performance, but it is not terminated.
Pod requests more resources than a node has Pending The scheduler cannot find any node that satisfies the Pod's resource request, so it never gets scheduled.
Pod has no limits and consumes all resources Running (initially), then potential node instability The rogue Pod can starve other Pods and even critical system daemons, leading to node-wide failures.
Test your understanding! (Interview Question)

You've just deployed a new service. After a few minutes, you run kubectl get pods and see its status is CrashLoopBackOff. When you run kubectl describe pod <pod-name>, you see the "Last State" reason was OOMKilled. What does this tell you, and what are your immediate next steps to debug and resolve this?

Show answer

What it tells you:
The OOMKilled status is a clear indicator that the application inside the container tried to allocate more memory than its configured limits.memory value. The Kubernetes node, via the underlying Linux cgroups, enforced this limit by terminating the process. The CrashLoopBackOff status occurs because Kubernetes keeps restarting the Pod, and it repeatedly hits the same memory limit and gets killed again.

Next Steps:

  1. Verify the Limit: Check the Pod's YAML manifest to see what the current limits.memory is set to. It's likely too low for the application's needs, especially during startup.
  2. Analyze Application Memory Usage:
    • Metrics: If you have a monitoring tool like Prometheus/Grafana or Datadog, check the historical memory usage (JVM heap and non-heap) for that service to understand its typical footprint.
    • Profiling: If it's a new service, you may need to run it locally with a Java profiler (like VisualVM) to understand its memory consumption under a realistic load.
  3. Increase the Limit: As an immediate mitigation, increase the limits.memory in your Deployment YAML to a more reasonable value based on your analysis and redeploy.
  4. Investigate for Memory Leaks: A constantly growing memory footprint that eventually leads to an OOMKilled event could also indicate a memory leak in the application code. This requires deeper code analysis and profiling over time.

3. Production Best Practices for Spring Boot

Now, let's bridge the gap from general Kubernetes concepts to specific, actionable advice for your Java and Spring Boot applications. Given the unique behavior of the JVM, applying generic advice can be counterproductive.

Memory: Guarantee Your Resources

For Java applications, the memory footprint is relatively predictable once the application is warmed up. However, if the JVM needs more memory and it isn't available, the result is fatal.

Best Practice: For Java workloads, always set memory requests and limits to the same value.

This approach gives your Pod a Guaranteed QoS class. It prevents your Pod from being killed due to node memory pressure (as long as it stays within its limit) and provides a stable, predictable memory environment for the JVM.

CPU: Request, But Don't Limit

Unlike memory, CPU is compressible. A typical Spring Boot application has "bursty" CPU needs—it requires more CPU during startup, JIT compilation, or handling a spike in traffic than during steady-state operation.

Best Practice: Set a realistic CPU request based on your application's typical needs, but do not set a CPU limit.

This is a powerful, non-obvious strategy perfect for a senior-level interview discussion.

  • Why set a request? It guarantees your application gets its fair share of CPU when the node is busy.
  • Why not set a limit? A CPU limit artificially prevents your application from using idle CPU cycles on the node. By omitting the limit, your app can "burst" and use available CPU during its high-demand phases, improving startup time and response latency without being throttled.

The following article provides a fantastic, in-depth justification for these practices.

Production Considerations for Spring on Kubernetes

The article 'Production Considerations for Spring on Kubernetes' offers crucial, Java-specific advice on resource configuration. It explains why the standard advice isn't always best for JVM workloads.

Please read the sections 'CPU Requests and Limits' and 'Memory Requests and Limits'. Focus on: The arguments for setting memory requests equal to memory limits. The detailed rationale for not setting CPU limits in production. The quote from Tim Hockin (Kubernetes co-founder) supporting this strategy.

JVM Tuning: Making Java Container-Aware

Setting the container resources is only half the battle. You also need to ensure the JVM running inside the container respects these boundaries. Modern JDKs (10+) are container-aware by default (-XX:+UseContainerSupport), but there are still nuances.

  1. Explicitly Set Active Processor Count: The way the JVM calculates the number of available CPUs can be unreliable inside a container. It might be based on CPU shares, which can fluctuate. To avoid ambiguity and ensure consistent performance of thread pools (like Fork/Join), it's best to set it explicitly.

    • Flag: -XX:ActiveProcessorCount=N
  2. Manage Heap Size: You must allocate enough container memory for both the JVM heap and non-heap memory (metaspace, thread stacks, direct memory, etc.). A common rule of thumb is to allocate about 75-80% of the container's memory to the heap.

    • Flag: -XX:MaxRAMPercentage=75.0
    • Important: This flag lets the JVM calculate the max heap size based on the container's memory limit. Your container's memory limit must be high enough to accommodate the heap plus non-heap memory. For example, if your app needs 1.5GB of heap and 500MB of non-heap, you must set the container memory limit to at least 2Gi.

The following article provides more detail on these JVM flags and how they interact with Kubernetes.

Java on containers: a guide to efficient deployment

The article 'Java on containers: a guide to efficient deployment' provides more detail on the specific JVM flags you can use to optimize your application's behavior within a container.

Read the section titled 'How to tune the JVM’s memory limits for Kubernetes infrastructure'. Pay close attention to the roles of the -XX:+UseContainerSupport and -XX:MaxRAMPercentage flags.

A sample configuration in your deployment.yaml would look like this:

spec:
  template:
    spec:
      containers:
      - name: my-spring-app
        image: my-app:1.0.0
        env:
        - name: JAVA_TOOL_OPTIONS
          value: "-XX:ActiveProcessorCount=2 -XX:MaxRAMPercentage=75.0"
        resources:
          requests:
            cpu: "1000m"      # Request 1 CPU core
            memory: "2Gi"     # Request 2 GiB of memory
          limits:
            memory: "2Gi"     # Limit memory to the same value
            # No CPU limit is set, allowing the app to burst
Test your understanding! (Comprehensive Interview Question)

You are tasked with deploying a new, business-critical Spring Boot microservice to a production Kubernetes cluster. How would you determine and configure its CPU and memory resources, and what corresponding JVM options would you set? Justify your choices, focusing on stability and performance.

Show answer

Here is a senior-level answer structure:

"My approach focuses on creating a stable, predictable, and performant configuration by combining Kubernetes resource management with JVM-specific tuning.

  1. Resource Profiling: First, I wouldn't guess the resource needs. I would profile the application under a realistic load in a staging environment to determine its steady-state CPU and memory usage. This gives me a baseline for my requests.

  2. Memory Configuration (requests == limits):

    • Kubernetes: I would set both the requests.memory and limits.memory to the same value, for instance, 2Gi. This places the Pod in the Guaranteed QoS class, making it the least likely to be evicted under node pressure. This is critical for a business-critical service. The value would be based on the profiling data, ensuring it covers heap, metaspace, thread stacks, and any direct memory usage.
    • JVM: I would use the -XX:MaxRAMPercentage=75.0 flag. This tells the JVM to use 75% of the container's 2Gi memory limit for its heap, leaving a 500Mi buffer for non-heap memory. This prevents the JVM from trying to allocate a heap that, when combined with non-heap memory, would exceed the container limit and cause an OOMKill.
  3. CPU Configuration (requests only):

    • Kubernetes: I would set a requests.cpu based on the application's steady-state needs from profiling, for example, 1000m (1 core). However, I would not set a limits.cpu.
    • Justification: This guarantees the pod 1 CPU core when the node is contested. By not setting a limit, the application is free to 'burst' and use any idle CPU on the node during startup or traffic spikes. This improves performance without risking stability, as CPU is a compressible resource and throttling is less desirable than using available capacity.
    • JVM: To ensure the JVM's internal thread pools (like the common Fork/Join pool) are sized correctly and don't rely on fluctuating cgroup metrics, I would explicitly set -XX:ActiveProcessorCount=2. This allows the JVM to size its pools for 2 cores, enabling it to take advantage of potential CPU bursting even if its request is only for 1 core.

This combined configuration provides a guaranteed, stable memory environment while allowing for opportunistic CPU bursting, maximizing both reliability and performance."


Conclusion

You have now learned how to manage one of the most critical aspects of running applications in Kubernetes. Proper resource management is the foundation of a stable and efficient cluster.

Key Takeaways:

  • Requests are for scheduling, Limits are for enforcement. They are fundamental to how Kubernetes allocates and protects resources.
  • Exceeding a memory limit is fatal (OOMKilled), while exceeding a CPU limit leads to performance degradation (throttling).
  • For Java/Spring Boot applications, a proven production strategy is to set memory requests equal to limits, and to set a CPU request but no CPU limit.
  • Always complement your Kubernetes resource settings with container-aware JVM flags like -XX:MaxRAMPercentage and -XX:ActiveProcessorCount for a truly robust setup.

Preview of the Next Lesson:
Manually setting the replica count for a service is inflexible. What happens when traffic spikes? In our next lesson, "Explain how Horizontal Pod Autoscaling (HPA) works and configure it based on CPU utilization," we will build directly on today's concepts. You will learn how to configure Kubernetes to automatically scale the number of your Pods up or down based on their CPU utilization, which is measured against the very requests we configured today.