Друзья, всем привет! Как известно, в 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)
Keirichs
05.10.2022 13:28-4Смысл статьи в том, чтобы показать,
что Java запускается в jvm и оказывается,
саму jvm необходимо настраивать?моё почтение.
evilrussian
Имхо удобнее использовать
-XX:+UseContainerSupport -XX:MaxRAMPercentage=60.0
вместо Xms/Xmx. Автоматом от лимитов пода (лимитов cgroup) будет вычислятся heap.olegklimenko52 Автор
-XX:+UseContainerSupport вроде с java 10 вроде включен по умолчанию, согласен в этом есть удобство но с -XX:MaxRAMPercentage был ньюанс, насколько я помню он сильно зависит от того с какими параметрами куб создает контейнер и соотвественно задаются cgroups, у нас поэтому эта настройка не взлетела так как MaxRam определялся не совсем корректно, причины были с кубом сходу не вспомню. Но спасибо за уточнение, при корректном определении способ для хипа рабочий
evilrussian
В java 8 появилось с версии 1.8.0_191-b12
https://www.oracle.com/java/technologies/javase/8u191-relnotes.html
Сам я ни разу не сталкивался с проблемами определения лимитов cgroup у java в контейнере в k8s. Что с cgroupDriver "cgroupfs" что с "systemd"