<div dir="ltr"><div>Hey everyone!<br>It's been a while since the last update but I'm happy to share that we finally manage to run ZGC in our production clusters!<br>Here's our story in case it can help someone else.<br><br>In our company we run our servers using Java OpenJDK Temurin-21.0.6+7 in Google Kubernetes Engine. <br>During our first experiments, we ran canary pods using ZGC in some of our services, as opposed to the non-canary ones that were using G1GC.<br>Concretely, we used the following arguments:<br>```<br>-XX:+UseZGC<br>-XX:+ZGenerational<br>-XX:+DisableExplicitGC<br>-XX:InitialRAMPercentage=50.0<br>-XX:MaxRAMPercentage=50.0<br>-XX:+AlwaysPreTouch<br>-XX:+UseLargePages<br>```<br>Even though we set the  `-XX:+UseLargePage` JVM flag, our Kubernetes nodes were not set to support Huge Pages. Therefore, ZGC was defaulting to `tempfs` as Heap Backing Filesystem as one could observe in the following gc log lines:<br>```<br>[info ][gc,init] Heap Backing File: /memfd:java_heap<br>[info ][gc,init] Heap Backing Filesystem: tmpfs (0x1021994)<br>```<br>While pause time and cpu usage were looking very promising, to our surprise we saw how **our containers were killed** after an hour by the Out Of Memory (**OOM**) killer.</div><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><br>Tracking down the reason it was killed, we saw how `container_memory_working_set_bytes` (used by the OOM killer) was reaching the configured limit (12Gi) for our canary pods compared to our non-canary ones. Moreover, studying it a bit more we saw how two metrics were influencing that one: `container_memory_cache` and `container_memory_mapped_file`.<br><div><br>With our current canaries setup, ZGC was [creating an anonymous mapped file](<a href="https://github.com/openjdk/zgc/blob/master/src/hotspot/os/linux/gc/z/zPhysicalMemoryBacking_linux.cpp#L208">https://github.com/openjdk/zgc/blob/master/src/hotspot/os/linux/gc/z/zPhysicalMemoryBacking_linux.cpp#L208</a>) on tmpfs, which meant reserving a large contiguous block of memory and committing it immediately, meaning the OS marked all those pages as in use right from the start—even if the application wasn't utilising all of that space. As a result, our application started with a high baseline memory consumption, and even normal memory growth quickly pushed it to the limit.<br><br>After understanding all this, we decided to enable **huge/large page** support in our [Kubernetes node pool](<a href="https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/">https://kubernetes.io/docs/tasks/manage-hugepages/scheduling-hugepages/</a>), and configure our canaries to request them following the [OpenJDK guidelines](<a href="https://wiki.openjdk.org/pages/viewpage.action?pageId=35455001#Main-EnablingLargePages">https://wiki.openjdk.org/pages/viewpage.action?pageId=35455001#Main-EnablingLargePages</a>).<br><br>We configured our nodes to utilize huge pages with a page size of 2048KiB (2MiB), a significant increase over the standard 4KiB pages. Using the same JVM arguments as before, our GC logs now display the following entries:<br>```<br>[info ][gc,init] Heap Backing File: /memfd:java_heap.hugetlb<br>[info ][gc,init] Heap Backing Filesystem: hugetlbfs (0x958458f6)<br>```<br>So huge pages were finally being used!</div><div style="text-align:right">Marçal </div></div></div>As a result, `container_memory_cache` and `container_memory_mapped_file` metrics dropped significantly and so did the `container_memory_working_set_bytes`.<br>This prevented OOM killer from terminating our canaries, allowing them to enjoy the **under 1ms GC pause times** while also experiencing a notable **reduction in both JVM hiccups (~99%) and CPU usage (~40%)**.  <br><br>Hope that can be useful to someone!<br>Best,</div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">Missatge de Marçal Perapoch Amadó <<a href="mailto:marcal.perapoch@gmail.com">marcal.perapoch@gmail.com</a>> del dia dc., 30 d’oct. 2024 a les 16:00:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div dir="ltr"><p>Hello everyone,</p><p>I wanted to provide an update on my recent findings.</p><p>First, I’m attaching an updated screenshot of the graphs I previously shared, titled "experiment_results_corrected.png." In this updated view:</p><ul><li>The <strong>green line</strong> represents one of our regular pods running G1.</li><li>The <strong>yellow line</strong> represents our canary pod using ZGC.</li></ul><p>One notable difference is in the <strong>Page Cache memory</strong>, which starts at approximately the initial/max heap size for ZGC and significantly influences the <strong>Memory Working Set</strong> value. Over time, as the application continued to run, this higher baseline led Kubernetes to kill and restart the container due to OOM, exceeding the 12GB memory limit twice. The first occurrence happened around 11:00. You’ll see a large spike there, which is simply the aggregation of the previous container (killed by OOM) and the newly restarted container, as Kubernetes immediately restarts the pod after killing it.</p><p>That’s essentially what I reported last time, but I was still curious about the source of this high <strong>Active Page Cache</strong>.</p><p>After further investigation, I noticed another metric showing a significant difference between the canary and regular pods: <strong>container_memory_mapped_file</strong>, which reflects the size of memory-mapped files. This can be seen in the attached "memory_mapped_files.png," where the pod with ZGC shows a notably higher mapped file size than the one with G1.</p><p>While researching, I came across two relevant quotes that might shed light on this behavior:</p><br>>  On Linux the _physical_ memory is basically an anonymous file that is only stored in RAM (and not on disk), ZGC uses [memfd_create](<a href="https://www.systutorials.com/docs/linux/man/2-memfd_create/" target="_blank">https://www.systutorials.com/docs/linux/man/2-memfd_create/</a>) to create it. The file can then be extended with [ftruncate](<a href="https://www.systutorials.com/docs/linux/man/2-ftruncate/" target="_blank">https://www.systutorials.com/docs/linux/man/2-ftruncate/</a>), ZGC is allowed to extend the physical memory (= the anonymous file) up to the maximum heap size. Physical memory is then [mmap](<a href="https://www.systutorials.com/docs/linux/man/2-mmap/)ed" target="_blank">https://www.systutorials.com/docs/linux/man/2-mmap/)ed</a> into the virtual address space.<br>source: <a href="https://dinfuehr.github.io/blog/a-first-look-into-zgc/" target="_blank">https://dinfuehr.github.io/blog/a-first-look-into-zgc/</a><br><br>> As part of the Java memory footprint, we have `MappedByteBuffers` - the files mapped to virtual memory of a process. **NMT does not track them**, however, MappedByteBuffers can also take physical memory. <br>source: <a href="https://stackoverflow.com/questions/53451103/java-using-much-more-memory-than-heap-size-or-size-correctly-docker-memory-limi" target="_blank">https://stackoverflow.com/questions/53451103/java-using-much-more-memory-than-heap-size-or-size-correctly-docker-memory-limi</a><br><br>So I took a couple of pmap snapshots, one from the canary and one from a regular pod, and guess what - the canary one is full of:<br>```<br>Address           Kbytes     RSS   Dirty Mode  Mapping<br>0000040000000000    6144    6100    6100 rw-s- memfd:java_heap (deleted)<br>0000040000600000    2048       0       0 -----   [ anon ]<br>0000040000800000    2048    2048    2048 rw-s- memfd:java_heap (deleted)<br>0000040000a00000    4096       0       0 -----   [ anon ]<br>0000040000e00000   71680   69212   69212 rw-s- memfd:java_heap (deleted)<br>0000040005400000    2048       0       0 -----   [ anon ]<br>0000040005600000    2048    2048    2048 rw-s- memfd:java_heap (deleted)<br>0000040005800000    2048       0       0 -----   [ anon ]<br>0000040005a00000   14336   14336   14336 rw-s- memfd:java_heap (deleted)<br>```<br>While the non-canary one has no entry like that one.<br><p>Summing the <code>RSS</code> values for entries with names starting with <code>memfd:java_heap</code> matches the values we see in the <strong>container_memory_mapped_file</strong> metric. This metric, in turn, drives the active page cache, which is the main difference we observe between ZGC and G1.</p><p>In short, it seems that Kubernetes includes these memory-mapped files in the <strong>memory working set</strong> calculation, impacting the decision to kill the container when it exceeds limits.<br><br>I hope this summary makes sense and may be helpful to others. Any feedback or thoughts would be very welcome, though I understand if this is outside the main topic.</p><p>Thanks a lot,</p><span style="color:rgb(0,0,0)"></span><br><br>As a reminder, here's a summary of the metrics shown in the graph (which btw I think my previous email is not visible in the mailing list):<br>* CLOUD_GKE: Memory Working Set (bytes): corresponds to the k8s `container_memory_working_set_bytes` which represents the amount of memory that the container is actively using and cannot be evicted.<br>    --> This is what the OOM killer is watching for.<br>* CLOUD_GKE: Resident Set Size (bytes): corresponds to the k8s `container_memory_rss` which is the size of RSS in bytes. Represents the amount of physical memory that a process has in the main memory that doesn’t correspond to anything on disk.  Typically includes stacks, heaps, and anonymous memory maps.  <br>* CLOUD_GKE: Page cache memory (bytes): corresponds to the k8s `container_memory_cache` - number of bytes of page cache memory<br>* CLOUD_GKE: Active page cache (bytes): corresponds to the k8s Active page cache computed as `container_memory_working_set_bytes - container_memory_rss`. It contains memory pages that are frequently accessed and currently in use by processes.<br><div><div><div dir="ltr" class="gmail_signature"><br><div><br></div><div style="text-align:right">Marçal </div></div></div><br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">Missatge de Thomas Stüfe <<a href="mailto:thomas.stuefe@gmail.com" target="_blank">thomas.stuefe@gmail.com</a>> del dia dt., 29 d’oct. 2024 a les 16:29:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div>Hi Marcel,</div><div><br></div><div>the GC log shows a contiguous log spanning times from 10:59 to 11:29. This does not correspond to your graphs, where the yellow lines indicate that the pod had been killed at about 11:10. You sure this is the right GC log?</div><div><br></div><div>The spikes look strange, and I don't see anything in the gc log that explains them. <br></div><div><br></div><div>/Thomas<br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Tue, Oct 29, 2024 at 3:18 PM Marçal Perapoch Amadó <<a href="mailto:marcal.perapoch@gmail.com" target="_blank">marcal.perapoch@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><p>Hello again,<br><br>Thanks a lot for having a look!</p><p>The logs I shared earlier were from a testing environment. I initially thought we could replicate the issue there, and I wanted to provide more insights from our experiments quickly, so I ran the test in that environment. However, in hindsight, this may not have been the best approach.</p><p>Today, we've repeated the experiment with a pod from our live environment. I've attached an image that shows four Kubernetes metrics, which I believe highlight differences between the pod running ZGC and the one running G1.</p><p>As Florian mentioned, the issue might stem from how Kubernetes or the container host interprets these metrics, so I’m not sure if anything can be done from the application side to address this. I just wanted to share this, in case these additional insights ring a bell and help identify any potential issues.<br><br>Description of the metrics shown in the attached image:<br><br>* CLOUD_GKE: Memory Working Set (bytes): corresponds to the k8s `container_memory_working_set_bytes` which represents the amount of memory that the container is actively using and cannot be evicted. This is what the OOM killer is watching for.<br>* CLOUD_GKE: Resident Set Size (bytes): corresponds to the k8s `container_memory_rss` which is the size of RSS in bytes.<br>* CLOUD_GKE: Page cache memory (bytes): corresponds to the k8s `container_memory_cache` - number of bytes of page cache memory<br>* CLOUD_GKE: Active page cache (bytes): corresponds to the k8s Active page cache computed as `container_memory_working_set_bytes - container_memory_rss`. It contains memory pages that are frequently accessed and currently in use by processes.<br><br>The yellow line is our canary pod using the following jvm args:<br>```<br>-XX:+UseZGC<br>-XX:+ZGenerational<br>-XX:InitialRAMPercentage=50.0<br>-XX:MaxRAMPercentage=50.0<br>-XX:NativeMemoryTracking=summary<br>-XX:+HeapDumpOnOutOfMemoryError<br>```<br><br>The green line corresponds to a regular pod using G1 and the same heap size.<br><br>Both share the same specs, 12GB ram, 4 CPU, and `OpenJDK 64-Bit Server VM Temurin-21.0.4+7 (build 21.0.4+7-LTS, mixed mode, sharing)`<br><br>As shown in the attached image, the main difference between the pod running with G1 and the pod using ZGC (canary) is that the one with ZGC starts with an active page cache of 6GB. This seems to correspond to the initial/max heap size of the JVM. As a result, the ZGC pod has a much higher baseline for its **Memory Working Set** right from the start.<br><br>Over time, as the application continues to run, this higher baseline causes Kubernetes to eventually kill and restart the pod due to Out Of Memory errors. This occurred twice because the pod exceeded the 12GB memory limit.<br></p>I have also attached the gc log and NMT summary for this run.<br><br>Cheers,<br><br></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">Missatge de Florian Weimer <<a href="mailto:fweimer@redhat.com" target="_blank">fweimer@redhat.com</a>> del dia dl., 28 d’oct. 2024 a les 16:58:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">* Marçal Perapoch Amadó:<br>
<br>
>> As in, Java OOMEs? OOM killer? Or the pod being killed from the pod management?<br>
<br>
> Our canary pods using ZGC were OOM killed, yes. It's also visible in<br>
> our metrics how the "container_memory_working_set_bytes" of the pods<br>
> using zgc went above 20GB even though they were set to use a max heap<br>
> of 6GB.<br>
<br>
I think some container hosts kill processes based on RSS alone, so even<br>
memory-mapped I/O can trigger this.  From the hosts perspective, it<br>
doesn't matter if the memory is just used for caching and could be<br>
discarded any time because it's a read-only MAP_SHARED mapping from a<br>
file.<br>
<br>
Thanks,<br>
Florian<br>
<br>
</blockquote></div>
</blockquote></div>
</blockquote></div></div>
</blockquote></div>