This project demonstrates the difference in behavior for a Java application experiencing an Out-of-Memory (OOM) condition in a container environment, specifically simulating the issues seen when migrating from cgroup v1 to cgroup v2 on platforms like EKS.
- The "Bad" Scenario (cgroup v1 simulation): An older or misconfigured JVM is not aware of the container's memory limits. It attempts to use more memory than allocated, causing the container runtime to abruptly kill the process (
OOMKilled
). The application has no chance to handle the error gracefully. - The "Good" Scenario (cgroup v2): A modern, container-aware JVM correctly detects the memory limits. It respects these limits and throws a catchable
java.lang.OutOfMemoryError
when the heap is exhausted, allowing the application to shut down gracefully.
This project is pre-configured to demonstrate the "bad" scenario.
src/main/java/com/example/oom/OomApp.java
: A simple Java app that continuously allocates 1MB chunks of memory to the heap.- It prints the JVM's max heap size on startup.
- It has a
try-catch
block to gracefully handleOutOfMemoryError
.
Dockerfile
: Builds the Java app and configures the runtime.- It uses
ENV JAVA_OPTS="-XX:-UseContainerSupport ..."
to explicitly disable the JVM's container awareness. This is the key to simulating the cgroup v1 problem. The JVM will now base its heap size on the node's memory, not the container's limit.
- It uses
k8s/deployment.yaml
: Deploys the application to Kubernetes with a256Mi
memory limit.
-
Build and Push the Docker Image
# Navigate to the project root directory cd oom-java-app # Build the image (replace with your registry if needed) docker build -t alliot/oom-jdk11:latest . # Push the image docker push alliot/oom-jdk11:latest
-
Deploy to Kubernetes
kubectl apply -f k8s/deployment.yaml
-
Observe the Failure
# Find your pod name POD_NAME=$(kubectl get pods -l app=oom-app -o jsonpath='{.items[0].metadata.name}') # Watch the logs kubectl logs -f $POD_NAME
You will see:
- The application starts.
- The
JVM Max Heap Size (Xmx)
will be a large value (e.g., >1000 MB), ignoring the256Mi
limit from the deployment YAML. - The application will log memory allocations.
- The logs will suddenly stop after allocating around 256 MB. The graceful
OutOfMemoryError
message is never printed.
-
Confirm the
OOMKilled
Statuskubectl describe pod $POD_NAME
In the output, you will see that the pod's
State
isTerminated
withReason: OOMKilled
.
Now, let's reconfigure the JVM to be container-aware, as it should be in a cgroup v2 environment.
-
Modify the
Dockerfile
- Open the
Dockerfile
. - Comment out or change the
ENV JAVA_OPTS
line to enable container support:(Note: For modern JVMs,# ENV JAVA_OPTS="-XX:-UseContainerSupport -XX:+UnlockDiagnosticVMOptions -Xlog:os+container=debug" ENV JAVA_OPTS="-XX:+UseContainerSupport"
+UseContainerSupport
is the default, so you could also just remove theENV
line entirely).
- Open the
-
Rebuild, Push, and Redeploy
# Rebuild and push the image with the same tag docker build -t alliot/oom-jdk11:latest . docker push alliot/oom-jdk11:latest # Delete the old deployment to ensure the new image is pulled kubectl delete deployment oom-app-deployment # Re-apply the deployment kubectl apply -f k8s/deployment.yaml
-
Observe the Success
# Find the new pod name POD_NAME=$(kubectl get pods -l app=oom-app -o jsonpath='{.items[0].metadata.name}') # Watch the logs kubectl logs -f $POD_NAME
You will now see:
- The
JVM Max Heap Size (Xmx)
is a much smaller, more reasonable number (e.g., ~170 MB), as it respects the256Mi
container limit. - The application allocates memory until it hits the JVM's heap limit.
- The
SUCCESS: JVM gracefully caught OutOfMemoryError!
message is printed to the console. - The pod exits with a
Completed
status, notOOMKilled
.
- The