Какие особенности запуска 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 не переполнен.