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

Друзья, всем привет! Как известно, в 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’а и есть запас на прочие расходы.

Итоги

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

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


ссылка на оригинал статьи https://habr.com/ru/company/domclick/blog/691240/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *