Какой опыт работы с микросервисами, где каждый контейнер имеет свою JVM?

Ответ

Это стандартная практика в микросервисной архитектуре на Java: каждый сервис упаковывается в отдельный Docker-контейнер со своей изолированной JVM. Работал с такой моделью развертывания, используя Docker и Kubernetes.

Типичный Dockerfile для Spring Boot приложения:

# Используем многостадийную сборку для уменьшения образа
FROM eclipse-temurin:17-jdk-alpine as builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

# Финальный легковесный образ
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Оптимизированные флаги JVM для контейнеризации
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

Ключевые аспекты и best practices:

Аспект Решение/Practice
Изоляция Каждый сервис (и его JVM) работает в своем контейнере, что обеспечивает независимость сбоев и безопасности.
Управление ресурсами В Kubernetes задаются requests и limits для CPU/памяти контейнера. JVM должна быть настроена соответственно (через -XX:MaxRAMPercentage).
Overhead памяти Каждая JVM имеет свою мета-область (Metaspace) и heap. Для оптимизации можно использовать GraalVM Native Image для компиляции в нативный бинарник, что drastically сокращает потребление памяти и время запуска.
Мониторинг Каждую JVM необходимо мониторить отдельно. Инструменты: JMX-экспортеры для Prometheus, интеграция с APM-системами (Datadog, New Relic).
Логирование Логи выводятся в stdout/stderr контейнера и агрегируются централизованно (через Fluentd, Loki или сторонние решения).

Минусы модели: Основной недостаток — повышенное общее потребление памяти из-за дублирования базовых классов JVM в каждом контейнере. Это компенсируется более простым управлением и оркестрацией.

Ответ 18+ 🔞

А, ну это ж классика жанра, блядь! Каждый микросервис — как отдельная квартира в общаге: своя JVM, свои тараканы в голове, и чтобы соседний алкаш-сервис не спалил тебе проводку, если у него припадок начнётся.

Вот тебе типичный рецепт, как эту лапшу в банку закатать:

# Сначала нажористо собираем
FROM eclipse-temurin:17-jdk-alpine as builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests

# А потом отжимаем всё лишнее, чтоб образ тощий был
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Настраиваем JVM, чтоб в контейнере не выперла за все лимиты, как дурак умный
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

А теперь по полочкам, где что болит и как обычно:

Что за фигня Как с этим жить
Изоляция Каждый сервис в своём аквариуме. Один сдох — остальные даже не чихнули. Красота, ёпта.
Ресурсы В кубере задаёшь, сколько ему можно жрать CPU и памяти. А JVM через -XX:MaxRAMPercentage говоришь: «Слышь, больше 75% от пайки не бери, а то накажут!».
Память А вот тут засада, блядь. Каждая JVM тащит за собой свою мета-вселенную (Metaspace) и кучу (heap). Вместе — овердохуища накладных расходов. Если совсем припёрло, можно GraalVM Native Image запилить — получится нативный бинарник, который память жрёт как птичка и стартует мгновенно. Но это уже высший пилотаж, не без костылей.
Наблюдение За каждой JVM-кой надо глаз да глаз. JMX-экспортеры в Prometheus, APM-системы (Datadog, New Relic) — без этого ты слепой, как крот подземный.
Логи Всё плюёшь в stdout контейнера, а дальше пусть Fluentd или Loki собирают. Главное — не захлебнись в этом потоке сознания.

Минус, который в лоб: Памяти жрётся дофига, потому что каждая JVM — как отдельный чемодан с одним носком внутри. Но зато управлять проще, оркестрировать удобнее. Расплата за порядок, блядь.