Настраиваем память JVM-приложения в Kubernetes

8721e5303b173b537edd50d4cbe3d2bd.jpg

Друзья, всем привет! Как известно, в Kubernetes у каждого pod«а есть ограничение на использование памяти (limits.memory), и, как показывает опыт, далеко не всегда очевидно, как JVM-приложение интерпретирует эту настройку, что порой может приводить к OOMKill.

Я хотел бы поделиться одним из способов настройки памяти для Java-приложений в Kubernetes. Сразу скажу, что итоговые настройки, к которым мы придём, будут приведены лишь в качестве примера и должны настраиваться индивидуально под каждое приложение. Рассматривать будем настройки и метрики обычного микросервиса на Spring boot, интегрированного со Spring Boot Admin (далее просто SBA).

Для начала немного освежим теорию по устройству памяти в Java. Вкратце, глобально память делится на два раздела, упрощенно:

Итого:

Heap (Eden, Survivor, Tenured) + Non-heap (Metaspace + Code Cache + Thread stack area + Direct buffers + Symbol tables + Other JVM structures).

Теперь рассмотрим, как работает c памятью приложение на Spring Boot без какой-либо настройки памяти, задав memory.limits в Кubernetes значение 1280 Мб. 

Если настроен Native memory tracking (NMT), то подробную информацию можно получить командой jcmd 1 VM.native_memory.

Native Memory Tracking

Total: reserved=2014514KB, committed=626614KB
-                 Java Heap (reserved=327680KB, committed=259652KB)
                            (mmap: reserved=327680KB, committed=259652KB) 
 
-                     Class (reserved=1229865KB, committed=205737KB)
                            (classes #36029)
                            (  instance classes #33803, array classes #2226)
                            (malloc=7209KB #105907) 
                            (mmap: reserved=1222656KB, committed=198528KB) 
                            (  Metadata:   )
                            (    reserved=174080KB, committed=173568KB)
                            (    used=169946KB)
                            (    free=3622KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=24960KB)
                            (    used=22922KB)
                            (    free=2038KB)
                            (    waste=0KB =0.00%)
 
-                    Thread (reserved=134801KB, committed=10461KB)
                            (thread #82)
                            (stack: reserved=134408KB, committed=10068KB)
                            (malloc=296KB #494) 
                            (arena=97KB #163)
 
-                      Code (reserved=251808KB, committed=80620KB)
                            (malloc=4120KB #15476) 
                            (mmap: reserved=247688KB, committed=76500KB) 
 
-                        GC (reserved=2219KB, committed=2003KB)
                            (malloc=1147KB #4677) 
                            (mmap: reserved=1072KB, committed=856KB) 
 
-                  Compiler (reserved=907KB, committed=907KB)
                            (malloc=777KB #1701) 
                            (arena=131KB #5)
 
-                  Internal (reserved=9479KB, committed=9479KB)
                            (malloc=9479KB #19066) 
 
-                     Other (reserved=4154KB, committed=4154KB)
                            (malloc=4154KB #191) 
 
-                    Symbol (reserved=40259KB, committed=40259KB)
                            (malloc=33634KB #418136) 
                            (arena=6624KB #1)
 
-    Native Memory Tracking (reserved=9068KB, committed=9068KB)
                            (malloc=32KB #432) 
                            (tracking overhead=9035KB)
 
-               Arena Chunk (reserved=2484KB, committed=2484KB)
                            (malloc=2484KB) 
 
-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #189) 
 
-                 Arguments (reserved=31KB, committed=31KB)
                            (malloc=31KB #498) 
 
-                    Module (reserved=1158KB, committed=1158KB)
                            (malloc=1158KB #6305) 
 
-              Synchronizer (reserved=558KB, committed=558KB)
                            (malloc=558KB #4724) 
 
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB) 
 
-                   Unknown (reserved=32KB, committed=32KB)
                            (mmap: reserved=32KB, committed=32KB)

SBA:

Теперь посмотрим на данные из админки:

4fcc057034d30f68ffe804e974e0d4b2.pngd53d00abb8491aa81495a6f8cc6356f1.png

Видно, что для Heap выделено максимально всего 324 Мб и для Non-heap 1,33 Гб, при том что памяти на pod было выделено всего 1280 Мб. Если сложить размеры Heap и Non-heap, то видно, что объём памяти, который готово использовать приложение, выходит далеко за пределы ограничения для контейнера. Что ж, OOMKill нам обеспечен :)

Попробуем немного настроить распределение. При этом стоит помнить, что у нас для разных стендов (QA, stage, prod) могут требоваться различные объёмы памяти. Для сборки образов мы используем библиотеку JIB, которая позволяет удобно настраивать параметры запуска приложения в entry.sh.

Наше приложение запускается в Docker по команде:

java \
-Xms${HEAP_SIZE_MB}M \
-Xmx${HEAP_SIZE_MB}M \
-Xss1M \
-XX:MaxMetaspaceSize=${METASPACE_SIZE_MB}M \
-XX:CompressedClassSpaceSize=${COMPRESSED_CLASS_SPACE_SIZE_MB}M \
-XX:ReservedCodeCacheSize=${RESERVED_CODE_CACHE_SIZE_MB}M \
-XX:MaxDirectMemorySize=${DIRECT_MEMORY_SIZE_MB}M \
-XX:NativeMemoryTracking=summary \
-cp "/app/resources:/app/classes:/app/libs/*" ru.example.application.DemoApplication

Тут настройки Heap и Non-heap расписаны по отдельности. Попробуем разобраться. Настройки Heap:

-Xms${HEAP_SIZE_MB}M \
-Xmx${HEAP_SIZE_MB}M \

 Настройки Non-heap:

-Xss1M \
-XX:MaxMetaspaceSize=${METASPACE_SIZE_MB}M \
-XX:CompressedClassSpaceSize=${COMPRESSED_CLASS_SPACE_SIZE_MB}M \
-XX:ReservedCodeCacheSize=${RESERVED_CODE_CACHE_SIZE_MB}M \
-XX:MaxDirectMemorySize=${DIRECT_MEMORY_SIZE_MB}M \

Эти переменные можно высчитать при запуске приложения в entry.sh, например по формуле (примерной):

#Converting a pod memory limit from bytes to megabytes
POD_MEM_LIMIT_MB=`expr $POD_MEM_LIMIT / 1024 / 1024`
 
#Calculating the metaspace size
METASPACE_SIZE_MB=`expr $POD_MEM_LIMIT_MB / 5`
 
#Calculating the compressed class space size
COMPRESSED_CLASS_SPACE_SIZE_MB=`expr $METASPACE_SIZE_MB / 5`
 
#Calculating the reserved code cache size
#(not a part of the metaspace but it is easier to get it relatively)
RESERVED_CODE_CACHE_SIZE_MB=`expr $METASPACE_SIZE_MB / 3`
echo "RESERVED_CODE_CACHE_SIZE_MB="$RESERVED_CODE_CACHE_SIZE_MB
 
#Calculating the reserved code cache size
DIRECT_MEMORY_SIZE_MB=`expr $METASPACE_SIZE_MB / 16`
echo "DIRECT_MEMORY_SIZE_MB="$DIRECT_MEMORY_SIZE_MB
 
#Calculating the reserved system usage and other purposes
OTHER_USAGE_MB=`expr $POD_MEM_LIMIT_MB / 4`
 
#Calculating total non heap size
NON_HEAP_SIZE_MB=`expr $METASPACE_SIZE_MB + $RESERVED_CODE_CACHE_SIZE_MB + $DIRECT_MEMORY_SIZE_MB + $OTHER_USAGE_MB`
 
#Calculating the heap size
HEAP_SIZE_MB=`expr $POD_MEM_LIMIT_MB - $NON_HEAP_SIZE_MB`

Размер Metaspace, как и других сегментов, можно указать и фиксированным, но для примера оставим вычисляемым.

И после такой настройки снова выполняемjcmd 1 VM.native_memory и картина видится уже немного иной:

Подробнее

Total: reserved=1109330KB, committed=961126KB
-                 Java Heap (reserved=618496KB, committed=618496KB)
                            (mmap: reserved=618496KB, committed=618496KB) 
 
-                     Class (reserved=230915KB, committed=202995KB)
                            (classes #35923)
                            (  instance classes #33695, array classes #2228)
                            (malloc=6659KB #96909) 
                            (mmap: reserved=224256KB, committed=196336KB) 
                            (  Metadata:   )
                            (    reserved=172032KB, committed=171512KB)
                            (    used=167998KB)
                            (    free=3514KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=52224KB, committed=24824KB)
                            (    used=22857KB)
                            (    free=1967KB)
                            (    waste=0KB =0.00%)
 
-                    Thread (reserved=98449KB, committed=10349KB)
                            (thread #82)
                            (stack: reserved=98056KB, committed=9956KB)
                            (malloc=296KB #494) 
                            (arena=97KB #163)
 
-                      Code (reserved=91279KB, committed=59095KB)
                            (malloc=3559KB #14025) 
                            (mmap: reserved=87720KB, committed=55536KB) 
 
-                        GC (reserved=3134KB, committed=3134KB)
                            (malloc=1114KB #4516) 
                            (mmap: reserved=2020KB, committed=2020KB) 
 
-                  Compiler (reserved=665KB, committed=665KB)
                            (malloc=534KB #1752) 
                            (arena=131KB #5)
 
-                  Internal (reserved=8429KB, committed=8429KB)
                            (malloc=8429KB #16669) 
 
-                     Other (reserved=4792KB, committed=4792KB)
                            (malloc=4792KB #149) 
 
-                    Symbol (reserved=40213KB, committed=40213KB)
                            (malloc=33588KB #415272) 
                            (arena=6624KB #1)
 
-    Native Memory Tracking (reserved=8808KB, committed=8808KB)
                            (malloc=27KB #341) 
                            (tracking overhead=8782KB)
 
-               Arena Chunk (reserved=2419KB, committed=2419KB)
                            (malloc=2419KB) 
 
-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #189) 
 
-                 Arguments (reserved=31KB, committed=31KB)
                            (malloc=31KB #498) 
 
-                    Module (reserved=1114KB, committed=1114KB)
                            (malloc=1114KB #6172) 
 
-              Synchronizer (reserved=543KB, committed=543KB)
                            (malloc=543KB #4597) 
 
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB) 
 
-                   Unknown (reserved=32KB, committed=32KB)
                            (mmap: reserved=32KB, committed=32KB)

SBA:

Теперь посмотрим на данные из админки:

f58df57f2aaee56bb942cec891a72be7.pngaa00480ce0e8f28eca14affba4523485.png

Если теперь сложить размеры сегментов, то теперь у нас все предельные размеры Heap+Non-heapниже, чем ограничение памяти pod«а и есть запас на прочие расходы.

Итоги

Процесс настройки памяти довольно непростой и требует учёта многих мелких и крупных подробностей и факторов, многие из которых не были упомянуты в этой статье. Мы прошлись по базовым элементам настройки памяти приложения, а также по одному из вариантов диагностики, при этом, стоит помнить, что приведённые настройки носят примерный характер и в боевой среде рассчитываются индивидуально для каждого приложения. Спасибо!

Полезные ссылки:

© Habrahabr.ru