Оптимизация образов: безопасность и размер
jlink: минимальная JVM под конкретное приложение
Инструмент JDK для создания настраиваемой JVM, включающей только необходимые модули (например, java.base, java.logging).
Как это работает:
1. Анализ зависимостей через jdeps:
2. Сборка минимальной JVM:
Результат:
- Стандартный образ OpenJDK 17 — 450 МБ.
- Образ с jlink — 120 МБ.
Нюансы:
- Некоторые библиотеки (например, Hibernate) требуют модуля jdk.unsupported.
- Проверяйте совместимость через --dry-run.
Distroless runtime образы (Google)
Образы без операционной системы, содержащие только JVM и ваше приложение. Созданы Google для минимизации attack surface.
Пример:
Как это работает:
- Distroless использует scratch (пустой образ) и добавляет только:
- Мусоросборщик (glibc или musl)
- Базовые сертификаты SSL,
- Минимальный набор библиотек для JVM
- Нет shell (/bin/sh), поэтому docker exec -it bash невозможен.
Преимущества:
- Размер образа — 80 МБ против 450 МБ у OpenJDK.
- Нулевой attack surface: нет curl, bash, apt для эксплуатации.
Отладка:
- Для диагностики используйте sidecar-контейнеры с curl или netcat.
- Логи читайте через docker logs, метрики — через /actuator/prometheus.
Non-root user: запуск Java-приложений безопасно
Запуск процесса Java не от root, чтобы ограничить ущерб при компрометации контейнера.
Как это работает:
Нюансы безопасности:
- UID/GID:
- Используйте фиксированный UID (1001), чтобы избежать конфликтов с томами.
- Если том смонтирован с правами root, пользователь 1001 не сможет писать в него.
- Capabilities:
- По умолчанию контейнер получает capabilities NET_BIND_SERVICE (привязка к портам < 1024).
- Для запуска на порту 80:
Критическая ошибка:
- Если приложение требует записи в /tmp, убедитесь, что директория доступна для записи:
#Java #middle #Docker #Debug
jlink: минимальная JVM под конкретное приложение
Инструмент JDK для создания настраиваемой JVM, включающей только необходимые модули (например, java.base, java.logging).
Как это работает:
1. Анализ зависимостей через jdeps:
jdeps --print-module-deps target/myapp.jar
Вывод: java.base,java.logging,java.xml.
2. Сборка минимальной JVM:
RUN jlink \
--add-modules java.base,java.logging,java.xml \
--output /jlinked \
--strip-debug \ — удаление отладочной информации.
--compress 2 \ — максимальное сжатие (уменьшает размер на 30%).
--no-header-files \
--no-man-pages
Результат:
- Стандартный образ OpenJDK 17 — 450 МБ.
- Образ с jlink — 120 МБ.
Нюансы:
- Некоторые библиотеки (например, Hibernate) требуют модуля jdk.unsupported.
- Проверяйте совместимость через --dry-run.
Distroless runtime образы (Google)
Образы без операционной системы, содержащие только JVM и ваше приложение. Созданы Google для минимизации attack surface.
Пример:
FROM gcr.io/distroless/java17-debian11
COPY --from=builder /app/target/myapp.jar /app.jar
ENTRYPOINT ["/app.jar"]
Как это работает:
- Distroless использует scratch (пустой образ) и добавляет только:
- Мусоросборщик (glibc или musl)
- Базовые сертификаты SSL,
- Минимальный набор библиотек для JVM
- Нет shell (/bin/sh), поэтому docker exec -it bash невозможен.
Преимущества:
- Размер образа — 80 МБ против 450 МБ у OpenJDK.
- Нулевой attack surface: нет curl, bash, apt для эксплуатации.
Отладка:
- Для диагностики используйте sidecar-контейнеры с curl или netcat.
- Логи читайте через docker logs, метрики — через /actuator/prometheus.
Non-root user: запуск Java-приложений безопасно
Запуск процесса Java не от root, чтобы ограничить ущерб при компрометации контейнера.
Как это работает:
# Создаем пользователя с UID 1001
RUN adduser --uid 1001 --disabled-password --gecos "" appuser
USER 1001
COPY --chown=1001:1001 target/myapp.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Нюансы безопасности:
- UID/GID:
- Используйте фиксированный UID (1001), чтобы избежать конфликтов с томами.
- Если том смонтирован с правами root, пользователь 1001 не сможет писать в него.
- Capabilities:
- По умолчанию контейнер получает capabilities NET_BIND_SERVICE (привязка к портам < 1024).
- Для запуска на порту 80:
# docker-compose.yml
app:
cap_add:
- NET_BIND_SERVICE
Критическая ошибка:
- Если приложение требует записи в /tmp, убедитесь, что директория доступна для записи:
RUN mkdir /app/tmp && chown 1001:1001 /app/tmp
ENV JAVA_OPTS="-Djava.io.tmpdir=/app/tmp"
#Java #middle #Docker #Debug
👍3
Минимизация attack surface (удаление ненужных слоёв)
Техники:
1. Многоступенчатая сборка (multi-stage build):
Промежуточные слои (Maven, исходники) не попадают в финальный образ.
2. Очистка кэша в том же слое:
- Если очистка в отдельном слое, размер образа не уменьшится (слой сохранится в истории).
3. Удаление ненужных файлов:
- Некоторые модули (CORBA, RMI) не используются в современных приложениях.
Performance tuning: настройка под контейнеры
CPU/Memory ограничения (--cpus, --memory)
Как это работает:
- Docker использует cgroups v2 для ограничения ресурсов:
- -m 512m — лимит памяти (записывается в /sys/fs/cgroup/memory.max),
- --cpus=1.5 — лимит CPU (эквивалентно --cpu-period=100000 --cpu-quota=150000).
Нюансы для JVM:
- По умолчанию JVM не видит лимиты cgroups и выделяет память, равную объему хоста.
- Решение:
Проверка лимитов:
Thread-pool tuning под контейнеры
Проблема:
- Стандартные пулы потоков (например, ForkJoinPool) используют Runtime.getRuntime().availableProcessors(),
- В контейнере это значение равно количеству CPU хоста, а не лимиту (--cpus).
Решение:
1. Для Spring Boot:
2. Для ванильного Java:
Как получить лимит CPU:
JVM-параметры для контейнеров
Критические параметры:
Объяснение:
- -XX:+UseContainerSupport (включено по умолчанию в JDK 8u191+):
- Позволяет JVM читать лимиты из cgroups.
- Без него -XX:MaxRAMPercentage игнорируется.
- -XX:MaxRAMPercentage=75.0:
- Heap = 75% от лимита памяти контейнера.
- Оставшиеся 25% — для Metaspace, native-памяти, стеков потоков.
- -XX:+UseZGC:
- Низколатентный GC (макс. задержки ~1 мс).
- Альтернативы: Shenandoah (JDK 12+), G1GC (по умолчанию).
- -Djava.security.egd=file:/dev/./urandom:
- Ускоряет старт приложений, заменяя блокирующий /dev/random на неблокирующий /dev/urandom.
Проверка работы GC:
- Столбец ZGC покажет использование памяти и паузы сборки мусора.
#Java #middle #Docker #Debug
Техники:
1. Многоступенчатая сборка (multi-stage build):
# Stage 1: Сборка
FROM maven:3.8.6 AS builder
COPY . .
RUN mvn package
# Stage 2: Финальный образ
FROM openjdk:17-jre-slim
COPY --from=builder target/myapp.jar /app.jar
Промежуточные слои (Maven, исходники) не попадают в финальный образ.
2. Очистка кэша в том же слое:
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
- Если очистка в отдельном слое, размер образа не уменьшится (слой сохранится в истории).
3. Удаление ненужных файлов:
RUN rm -rf $JAVA_HOME/jmods/java.corba.jmod
- Некоторые модули (CORBA, RMI) не используются в современных приложениях.
Performance tuning: настройка под контейнеры
CPU/Memory ограничения (--cpus, --memory)
Как это работает:
- Docker использует cgroups v2 для ограничения ресурсов:
docker run -m 512m --cpus=1.5 myapp
- -m 512m — лимит памяти (записывается в /sys/fs/cgroup/memory.max),
- --cpus=1.5 — лимит CPU (эквивалентно --cpu-period=100000 --cpu-quota=150000).
Нюансы для JVM:
- По умолчанию JVM не видит лимиты cgroups и выделяет память, равную объему хоста.
- Решение:
environment:
JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75.0" //указывает JVM использовать 75% от лимита cgroups.
Проверка лимитов:
docker exec myapp cat /sys/fs/cgroup/memory.max
docker exec myapp cat /sys/fs/cgroup/cpu.max
Thread-pool tuning под контейнеры
Проблема:
- Стандартные пулы потоков (например, ForkJoinPool) используют Runtime.getRuntime().availableProcessors(),
- В контейнере это значение равно количеству CPU хоста, а не лимиту (--cpus).
Решение:
1. Для Spring Boot:
# application.properties
spring.task.execution.pool.core-size=4
spring.task.execution.pool.max-size=8
2. Для ванильного Java:
int availableCpus = (int) Math.min(
Runtime.getRuntime().availableProcessors(),
Double.parseDouble(System.getenv("CPU_LIMIT"))
);
ExecutorService executor = Executors.newFixedThreadPool(availableCpus * 2);
Как получить лимит CPU:
public static int getContainerCpuLimit() {
try (BufferedReader reader = new BufferedReader(
new FileReader("/sys/fs/cgroup/cpu.max"))) {
String line = reader.readLine();
String[] parts = line.split(" ");
long quota = Long.parseLong(parts[0]);
long period = Long.parseLong(parts[1]);
return (int) (quota / period); // Например, 1.5 → 1
}
}
JVM-параметры для контейнеров
Критические параметры:
environment:
JAVA_TOOL_OPTIONS: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:+UseZGC
-Djava.security.egd=file:/dev/./urandom
Объяснение:
- -XX:+UseContainerSupport (включено по умолчанию в JDK 8u191+):
- Позволяет JVM читать лимиты из cgroups.
- Без него -XX:MaxRAMPercentage игнорируется.
- -XX:MaxRAMPercentage=75.0:
- Heap = 75% от лимита памяти контейнера.
- Оставшиеся 25% — для Metaspace, native-памяти, стеков потоков.
- -XX:+UseZGC:
- Низколатентный GC (макс. задержки ~1 мс).
- Альтернативы: Shenandoah (JDK 12+), G1GC (по умолчанию).
- -Djava.security.egd=file:/dev/./urandom:
- Ускоряет старт приложений, заменяя блокирующий /dev/random на неблокирующий /dev/urandom.
Проверка работы GC:
docker exec myapp jstat -gcutil 1 1000
- Столбец ZGC покажет использование памяти и паузы сборки мусора.
#Java #middle #Docker #Debug
👍4
Debug и мониторинг: production-ready диагностика
docker exec, attach к процессу
Стандартные команды:
Нюансы:
- В distroless-образах нет jstack/jstat.
Решение:
- Используйте sidecar-контейнер с OpenJDK:
- Или добавьте инструменты в образ через jlink:
JMX/JFR в контейнере
Настройка JMX:
Проблемы:
- RMI-порт: JMX использует динамические порты для RMI. Чтобы зафиксировать их:
- DNS-имя:
-Djava.rmi.server.hostname=myapp — имя сервиса в Docker Compose.
JFR (Java Flight Recorder):
Интеграция с Prometheus/Grafana
Шаги:
1. Добавьте Micrometer в приложение:
2. Настройте эндпоинт:
3. Docker Compose:
4. prometheus.yml:
Ключевые метрики:
- jvm_memory_used_bytes — использование heap,
- http_server_requests_seconds_count — количество HTTP-запросов,
- jvm_gc_pause_seconds — длительность GC-пауз.
#Java #middle #Docker #Debug
docker exec, attach к процессу
Стандартные команды:
# Стэк-трейс
docker exec myapp jstack 1 > thread_dump.txt
# Мониторинг GC
docker exec myapp jstat -gcutil 1 1000
# Heap-дамп
docker exec myapp jcmd 1 GC.heap_dump /tmp/heap.hprof
Нюансы:
- В distroless-образах нет jstack/jstat.
Решение:
- Используйте sidecar-контейнер с OpenJDK:
debug-tools:
image: openjdk:17
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: ["jstack", "myapp"]
- Или добавьте инструменты в образ через jlink:
RUN jlink \
--add-modules jdk.jfr,jdk.management \
--output /jlinked
JMX/JFR в контейнере
Настройка JMX:
environment:
JAVA_TOOL_OPTIONS: >-
-Dcom.sun.management.jmxremote.port=9090
-Dcom.sun.management.jmxremote.rmi.port=9090
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=myapp
ports:
- "9090:9090"
Проблемы:
- RMI-порт: JMX использует динамические порты для RMI. Чтобы зафиксировать их:
-Dcom.sun.management.jmxremote.rmi.port=9090
- DNS-имя:
-Djava.rmi.server.hostname=myapp — имя сервиса в Docker Compose.
JFR (Java Flight Recorder):
# Запуск записи
docker exec myapp jcmd 1 JFR.start name=recording duration=60s filename=/tmp/recording.jfr
# Экспорт метрик в Prometheus
RUN jcmd 1 JFR.configure stackdepth=128
Интеграция с Prometheus/Grafana
Шаги:
1. Добавьте Micrometer в приложение:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2. Настройте эндпоинт:
management.endpoints.web.exposure.include=prometheus
3. Docker Compose:
services:
app:
ports:
- "8080:8080"
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
4. prometheus.yml:
scrape_configs:
- job_name: 'java'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
Ключевые метрики:
- jvm_memory_used_bytes — использование heap,
- http_server_requests_seconds_count — количество HTTP-запросов,
- jvm_gc_pause_seconds — длительность GC-пауз.
#Java #middle #Docker #Debug
👍4