Often times, you will run into obscure errors that require you to take a peek under the JVM hood, which in itself can be a daunting task. In this tutorial, we will take a look on how to troubleshoot a running application in a Kubernetes cluster with VisualVM as well as how to inspect Heap Dumps and how to pass them from the container to our machine.

For this example, we have a Hello Spring Boot Application (the code can be found here) that saves a timestamp of each request in memory (in order to provide us some interesting data for us to analyze later on). We will be working hands on with VisualVM so make sure to download it and start it up, it is very straightforward and here is the link. One could also use JConsole for such a task but let’s leave that for some other time.

We use this slim JRE11 Docker image and expose an additional port as we will need to make a JMX connection to the JVM

FROM openjdk:11.0.2-jre-stretch

COPY target/demo*.jar /opt/app.jar
EXPOSE 8080 9010

ENTRYPOINT exec java $JAVA_OPTS -jar /opt/app.jar

We will use a standard K8s deployment file in order to deploy this to the cluster (minikube is fine). Notice that we have defined hard limits on the container (request == limits) so we can limit the amount of memory being used and cause some interesting behavour.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: demo-app
          image: uroshtrifunovic/spring-boot-demo:jvm-k8s
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: 128Mi
              cpu: 100m
            limits:
              memory: 128Mi
              cpu: 100m
          env:
            - name: JAVA_OPTS
              value: "-Dcom.sun.management.jmxremote \
                      -Dcom.sun.management.jmxremote.authenticate=false \
                      -Dcom.sun.management.jmxremote.ssl=false \
                      -Dcom.sun.management.jmxremote.local.only=false \
                      -Dcom.sun.management.jmxremote.port=9010 \
                      -Dcom.sun.management.jmxremote.rmi.port=9010 \
                      -Djava.rmi.server.hostname=127.0.0.1"

Also notice all the JVM arguments we have defined in order to be able to connect remotely via JMX. Let’s go through them one by one and see what they do:

  • -Dcom.sun.management.jmxremote - enable JMX connections
  • -Dcom.sun.management.jmxremote.authenticate=false - disable auth
  • -Dcom.sun.management.jmxremote.ssl=false - self-explanatory
  • -Dcom.sun.management.jmxremote.local.only=false - the same 🙂

These last 3 arguments are needed for k8s port-forward to work:

  • -Dcom.sun.management.jmxremote.port=9010
  • -Dcom.sun.management.jmxremote.rmi.port=9010
  • -Djava.rmi.server.hostname=127.0.0.1″

We deploy our application into the cluster and will do a kubectl port-forward in order to open a port and connect through it via JMX as we don’t want to expose our JMX to the world, just ourselves or if this was PROD – then we could have used a secure connection, with IP whitelisting with all the bells and whistles. For the sake of simplicity – kubectl port-forward FTW to port 9010!

At this point, we can start our VisualVM. We should see our application running in the “Local” section and connect, like so: VisualVM main menu

We are greeted with an overview of our JVM, flags, arguments and other semi-useful information. If you switch to the “Montor” tab, you will notice graphs for Threads, CPU, classes and Heap. See those nice spikes in the Heap? That’s our friend, the Garbage Collector doing it’s work for us.

VisualVM Monitor tab displays overall information

Now, let’s start using our application and drive some traffic through it. I will just keep calling the /hello endpoint and monitor the behaviour in VisualVM (note that your URL will be different from mine, here I am using minikube).

$ while true; do curl "http://172.17.0.2:32245/hello"; echo "\r"; done

The following is what we can see in our monitoring tool:

The memory graph will reflect the garbage collecting in form of spikes on the graph

Now we are actually getting some CPU usage and we see that the GC is also working more often.

You can use this technique to troubleshoot issues like OOM errors or whenever you want to see how busy or idle are your threads.

That’s all folks!

Keep on hacking!