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

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

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

  • Heap. Этот раздел делится на подразделы:

    • Eden — для вновь созданных объектов, чистится при каждой сборке мусора (Minor GC).

    • Survivor — для объектов, переживших Minor GC.

    • Tenured — для долгоживущих объектов, чистится при Major GC.

  • Non-heap. Состоит из:

    • Metaspace (и в составе него Compressed Class Space) — метаданные загруженных классов.

    • Code Cache — для скомпилированного JIT-компилятором.

    • ThreadStackArea.

    • Direct buffers.

    • Garbage collection.

    • Symbol tables и прочие, на чем подробно останавливаться не будем.

Итого:

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:

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

Видно, что для 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:

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

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

Итоги

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

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

Комментарии (4)


  1. evilrussian
    05.10.2022 10:17
    -1

    Имхо удобнее использовать -XX:+UseContainerSupport -XX:MaxRAMPercentage=60.0 вместо Xms/Xmx. Автоматом от лимитов пода (лимитов cgroup) будет вычислятся heap.


    1. olegklimenko52 Автор
      05.10.2022 16:28
      +1

      -XX:+UseContainerSupport вроде с java 10 вроде включен по умолчанию, согласен в этом есть удобство но с -XX:MaxRAMPercentage был ньюанс, насколько я помню он сильно зависит от того с какими параметрами куб создает контейнер и соотвественно задаются cgroups, у нас поэтому эта настройка не взлетела так как MaxRam определялся не совсем корректно, причины были с кубом сходу не вспомню. Но спасибо за уточнение, при корректном определении способ для хипа рабочий


      1. evilrussian
        05.10.2022 17:41

        В java 8 появилось с версии 1.8.0_191-b12 

        https://www.oracle.com/java/technologies/javase/8u191-relnotes.html

        Сам я ни разу не сталкивался с проблемами определения лимитов cgroup у java в контейнере в k8s. Что с cgroupDriver "cgroupfs" что с "systemd"


  1. Keirichs
    05.10.2022 13:28
    -4

    Смысл статьи в том, чтобы показать,
    что Java запускается в jvm и оказывается,
    саму jvm необходимо настраивать?

    моё почтение.