Gymterview
senior

Какие особенности запуска JVM-приложений в Kubernetes нужно учитывать?

Запуск Java-приложений в Kubernetes имеет ряд нюансов, связанных с особенностями JVM и контейнерной среды. Незнание этих нюансов приводит к OOMKilled, медленному старту и неэффективному использованию ресурсов.

1. Настройка памяти JVM

JVM должна корректно определять доступную память контейнера, а не всей ноды.

Пример
env:
  - name: JAVA_OPTS
    value: >-
      -XX:+UseContainerSupport
      -XX:MaxRAMPercentage=75.0
      -XX:InitialRAMPercentage=50.0
  • -XX:+UseContainerSupport — включён по умолчанию с JDK 10+. JVM видит лимиты контейнера, а не ресурсы ноды
  • -XX:MaxRAMPercentage=75.0 — heap займёт не более 75% доступной контейнеру памяти. Остальные 25% — для metaspace, thread stacks, native memory, off-heap буферов
  • Не используйте фиксированные -Xmx/-Xms, если хотите гибкости при изменении Limits

2. Определение CPU

Kubernetes ограничивает CPU через CFS-квоты Linux. JVM с UseContainerSupport определяет количество «процессоров» на основе CPU Limits.

Это влияет на:

  • Количество потоков в пулах (ForkJoinPool, GC threads)
  • Параллелизм по умолчанию

Если CPU Limit = 500m (0.5 ядра), JVM увидит 1 процессор. При 2000m — 2 процессора.

3. Graceful Shutdown

Пример
# application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

В Deployment:

Пример
spec:
  terminationGracePeriodSeconds: 60

Значение terminationGracePeriodSeconds должно быть больше, чем timeout-per-shutdown-phase, чтобы Spring Boot успел завершить текущие запросы до принудительного убийства процесса.

4. Долгий старт

Spring Boot приложения могут стартовать 30-120 секунд, особенно с большим количеством бинов и миграциями БД. Используйте Startup Probe, чтобы Liveness Probe не убила контейнер во время запуска.

5. Логирование

  • Пишите логи в stdout/stderr — Kubernetes автоматически их подхватывает
  • Не пишите в файлы внутри контейнера — это затрудняет сбор логов и расходует дисковое пространство
  • Используйте JSON-формат для структурированного логирования

6. Оптимизация Docker-образа

Подход Плохо Хорошо
Базовый образ JDK, полный образ JRE, alpine
Пользователь root non-root (spring, appuser)
Сборка Один этап Multi-stage build
Пример оптимизированного Dockerfile
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
COPY --from=builder /app/target/*.jar app.jar
USER spring
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

7. Stateless-архитектура

Приложение в Kubernetes должно быть stateless:

Что Где хранить
Сессии Redis, БД
Файлы S3, MinIO, PersistentVolume
Кэш Redis, Hazelcast
Конфигурация ConfigMap, Secret

Это позволяет свободно масштабировать и перемещать Pod’ы между нодами.

8. DNS и Service Discovery

В Spring Boot для обращения к другим сервисам используйте DNS-имена Service’ов Kubernetes:

Пример
# application.yml
app:
  user-service-url: http://user-service.production.svc.cluster.local:80
  # Или короткая форма (в пределах одного namespace):
  order-service-url: http://order-service:80

На собеседовании: ключевые моменты — MaxRAMPercentage (не фиксированный Xmx), UseContainerSupport, graceful shutdown, Startup Probe для долгого старта, логи в stdout. Частая ошибка — не учитывать native memory при расчёте Memory Limit и получать OOMKilled, хотя heap не переполнен.