7 советов, как оптимизировать производительность Java в Kubernetes

7 советов, как оптимизировать производительность Java в Kubernetes


Июнь 14, 2024


Оптимизировать производительность Java — уже непростая задача. А Kubernetes, который сам по себе — сложный инструмент с кучей тонкостей и нюансов настройки, делает эту задачу ещё сложнее. Современные технологии контейнеризации и Kubernetes настроены по умолчанию так, чтобы пользователи с минимальным уровнем знаний и навыков начали пользоваться облачными технологиями. Тем не менее, использование этих настроек на постоянной основе приводит к проблемам с контролем над инстансами.

И что делать? В этой статье вы узнаете о проверенных методах, которые помогут улучшить ключевые показатели производительности Java-приложения в Kubernetes.

1. Ограничьте потребление CPU и RAM

Помимо оптимизации объёма памяти JVM с помощью таких опций, как -Xmx, -XX:MaxRAM, -XX:+UseStringDeduplication и других, задайте запросы и ограничения ресурсов CPU и RAM, используемые подами (Pods) и контейнерами в Kubernetes.

Подобрать значения для ограничения памяти приложения — это как пройти меж двух огней: финансовый департамент требует сократить использование ресурсов, чтобы уменьшить счета за облако, а бизнес требует больше ресурсов, формулируя SLA (Service Level Agreement — соглашение об уровне обслуживания).

Найти решение помогут такие рекомендации:

  • Проведите нагрузочное и стресс-тестирование. Определите поведение приложения в нормальных условиях и при пиковых нагрузках, затем настройте параметры соответствующим образом. Не устанавливайте слишком низкие значения, даже если приложение потребляет мало ресурсов при обычной нагрузке. Оно может потребовать больше CPU для прогрева и при пиковых нагрузках.
  • Определите минимальные требования для удовлетворения SLO (Service Level Objectives — цели уровня обслуживания). Эти требования должны выполняться разработчиками для соблюдения SLA.
  • Используйте инструменты для изучения соответствующих метрик, таких как потребление RAM. Например, с помощью kubectl top pod вы получите данные об использовании памяти подом, а с jcmd GC.heap_info — информацию об использовании кучи (heap).
  • Используйте Native Memory Tracking и логи GC (Garbage Collector — сборщик мусора). Так вы поймёте, сколько памяти реально использует ваше приложение.
  • Учитывайте потребляемые ресурсы Kubernetes. Потребляемые ресурсы пода — это ресурсы, которые он использует при работе на узле (Node).

Если вы неправильно подберете значения для ограничения памяти, это приведёт к следующему:

  • Переиспользование ресурсов. Когда сервис потребляет всю доступную память, приложение постоянно выполняет сборку мусора, и требуется добавлять больше инстансов, чтобы возобновить нормальную работу.
  • Недоиспользование ресурсов. Когда приложение не использует всю доступную память, и приходится запускать больше инстансов и тратить ресурсы впустую.

2. Разделяйте типы сервисов

При оптимизации учитывайте тип сервиса. Некоторые из них менее важны, другие — более, поэтому требования к производительности приложений различаются.

Менее важные сервисы:

  • Имеют умеренные требования к RTO (Recovery time objective — целевое время восстановления).
  • Относятся к классу Burstable QoS, что означает меньшие гарантии ресурсов и отсутствие конкретных ограничений памяти. Хотя бы один контейнер в поде должен иметь запрос ресурсов памяти процессора или ограничение по памяти процессора.
  • Могут использоваться с спотовыми инстансами (spot instances), которые на 90% дешевле инстансов по требованию (on-demand instances) и запускаются при наличии свободных мощностей.

Более важные сервисы:

  • Имеют строгие требования к RTO
  • Гибкие и рассчитаны на экспоненциальный рост.
  • Принадлежат к классу Guaranteed Quality of Service (QoS). Это значит, что у подов строгие ограничения ресурсов и эти поды будут работать, пока не превысят свои ограничения. У всех контейнеров в таких подах должны быть: ограничение или запрос ресурсов процессора и ограничение или запрос ресурсов памяти.

Поэтому требования к производительности должны определяться для каждого сервиса в отдельности.

Java-приложения ограниченно масштабируются вертикально, но хорошо — горизонтально. Это значит, что запросы ресурсов и ограничения должны быть основаны на данных о пиковой нагрузке. Также стратегия масштабирования не должна основываться только на показателях CPU и RAM. Иногда для приложения важнее задержка или пропускная способность.

3. Используйте Kubernetes probes эффективно

Kubernetes probes, используемые кублетом, крайне важны для сбора информации о состоянии контейнеров. Но имейте в виду, их неправильная настройка может привести к снижению производительности и излишнему масштабированию.

Существует три типа Kubernetes probes:

  • Startup probe определяет, запустилось ли контейнерное приложение. Другие probes отключаются до тех пор, пока startup probe не подтвердит успешный запуск приложения.
  • Liveness probe определяет, запущен ли контейнер. Если нет, он сообщает кублету о необходимости перезапустить его.
  • Readiness probe определяет, когда контейнер готов принимать сетевые запросы.

Probes лучше работают вместе. Например, startup probe идеально подходит для медленно запускающихся контейнеров, потому что без неё liveness probe может преждевременно убить контейнер, если не установить соответствующее значение timeoutSeconds. Readiness probe может сказать, что контейнер работает нормально, в то время как на самом деле приложение находится в deadlock, который может быть определен только liveness probe.

Основные Java-фреймворки, включая Spring Boot, поддерживают настройку и автоматическую конфигурацию probes. В Spring Boot вам нужно добавить зависимость spring-boot-starter-actuator в файл pom.xml. Spring Boot автоматически зарегистрирует liveness и readiness probes, когда в application.properties property management.health.probes.enabled будет установлено в true (или management.endpoint.health.probes.enabled=true, начиная с Spring 2.3.2).

После этого вы можете настроить параметры probes в соответствии с вашими workloads. Правильная настройка позволит вам избежать частых и ненужных перезапусков контейнеров и других проблем. Например, предположим, что probes ждут недостаточно времени (например, когда вы устанавливаете слишком низкие ограничения таймаута) и возвращают отрицательные ответы. В этом случае Kubernetes autoscaler может решить, что необходимы дополнительные поды, и выполнит ненужное вертикальное масштабирование, тем самым расходуя ресурсы.

4. Обновляйте версию Java

Даже если вы используете поддерживаемые образы JDK с регулярно выходящими обновлениями безопасности, работа ещё не закончена. Обновление версии Java не менее важно, поскольку общая производительность JVM становится все выше с каждым выпуском JDK. Например, начиная с JDK 9, Java все больше ориентируется на контейнеры:

  • JDK версий 10+ имеют лучший алгоритм обнаружения контейнеров Docker и позволяют лучше использовать конфигурацию ресурсов, а также более гибко регулировать процент heap в зависимости от доступной оперативной памяти;
  • Версии 11+ собирают и используют данные cgroups v1;
  • Версии 17+ и 11u имеют поддержку cgroups v2;

И так далее. Кроме того, свежие версии содержат множество улучшений в сборке мусора, что сильно влияет на KPI. И несмотря на то, что некоторые исправления переносятся в устаревшие версии Java, с каждым новым выпуском все меньше и меньше улучшений попадает в старые версии LTS. Поэтому обновление JDK имеет решающее значение для оптимальной производительности Java, причем не только в Kubernetes.

Но что делать, если вы не можете перейти на более новую версию Java прямо сейчас? Ведь переход требует решения проблем совместимости, а иногда и значительного переписывания кода.

Вы можете использовать мощь JVM 17 в своих проектах на базе JDK 11 с помощью Axiom JDK Express. Повысьте скорость запуска, задержки и пропускную способность сразу же, практически не внося изменений в код, и переходите на новую версию в удобном для вас темпе!

5. Выбирайте правильный Garbage Collector

Платформа Java предлагает множество сборщиков мусора, предназначенных для конкретных рабочих нагрузок и нацеленных на улучшение соответствующих KPI. Например,

  • ParallelGC подходит для приложений с высокой пропускной способностью;
  • G1GC нацелен на снижение задержки;
  • ShenandoahGC (не входит в Oracle Java, но поставляется с дистрибутивами OpenJDK, включая Axiom JDK) фокусируется на том, чтобы паузы были короткими даже при больших кучах.

Цель состоит в том, чтобы выбрать подходящий коллектор для вашего кластера Kubernetes — в большинстве случаев его будет достаточно для повышения производительности и он не потребует тонкой настройки GC.

Кроме того, разработчикам следует избегать автоматического включения SerialGC. Предположим, вы установили параметр -Xmx на 2 Гб или меньше и приложению доступно только два процессора. В этом случае SerialGC будет включен автоматически (даже явное указание другого коллектора в настройках JVM не изменит ситуацию). SerialGC может быть оптимальным для однопроцессорных машин и приложений, работающих в условиях нехватки памяти, но может привести к значительному снижению производительности в других случаях.

Поэтому не устанавливайте слишком низкие лимиты и не используйте параметр -XX:+AlwaysActAsServerClassMachine, который предотвращает автоматическое использование SerialGC.

6. Используйте маленький базовый образ ОС

Минимизация размера контейнеров играет важную роль в оптимизации потребления ресурсов в кластерах Kubernetes и контроле расходов на облака. Несмотря на то, что разработчики используют несколько методик, позволяющих сделать контейнеры компактными, первым шагом к минимизации должен являться выбор миниатюрного базового образа ОС. Таким образом, вы сразу же уменьшите размер контейнера, не прибегая к сложностям настройки памяти JVM или удаления ненужных пакетов из образа ОС.

Лучшими легковесными дистрибутивами ОС Linux для облака являются Alpine и Axiom Linux. Размер базового образа обоих составляет менее 4 МБ (дополнительные пакеты легко устанавливаются с помощью инструмента APK). Однако Axiom Linux, на 100% совместимая с Alpine, имеет несколько отличительных особенностей, которые делают ее идеальной для корпоративной Java-разработки:

  • Две реализации libc, оптимизированные musl и glibc, для повышения производительности и беспроблемной миграции;
  • Дополнительная защита ядра и регулярные обновления для лучшей безопасности;
  • Инструменты, облегчающие Java разработку, и четыре вида malloc для различных профилей нагрузок;
  • LTS-релизы и круглосуточная поддержка непосредственно инженерами Axiom JDK.

7. Сокращайте время запуска и прогрева

Сокращение времени запуска приложений очень важно при использовании cloud functions в Яндекс.Облако, serverless functions в Cloud ru или аналогичных сервисов, которые являются аналогами AWS Lambda, Azure Functions и др. Это также необходимо, если ваш облачный провайдер взимает плату за процессорное время. Кроме того, во время прогрева JVM потребление памяти увеличивается, поэтому вам придется выделять больше памяти для инстансов своего приложения, которая впоследствии не будет использоваться.

Существует несколько способов ускорить запуск Java-приложений, включая AppCDS, AOT-компиляцию и некоторые совершенно новые решения, такие как метод CRaC (Coordinated Restore at Checkpoint). Но эта тема для другой статьи, посвященной методам сокращения времени запуска и прогрева ваших Java-приложений.

В заключении отметим, что в российской платформе для создания Kubernetes-кластеров можно использовать отечественные Java-контейнеры, при этом весьма эффективно. Тесты подтвердили полную совместимость легковесного Java-контейнера Axiom Runtime Container с Deskhouse Kubernetes Platform.

Подписывайтесь на наш тг-канал, чтобы узнавать о выходе новых статей и быть в курсе новостей по Java в России.

Author image

Александр Дроздов

Инженер по РБПО и информационной безопасности Axiom JDK

Команда Axiom JDK roman.karpov@axiomjdk.ru Команда Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Команда Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67