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