add entrypoint to dockerfile and doc

fix resource usage
This commit is contained in:
Savosin Denis
2025-11-06 15:47:56 +07:00
parent 11847af074
commit e6db3360c2
9 changed files with 403 additions and 19 deletions

View File

@@ -1,2 +1,3 @@
*
!build/libs/
!entrypoint.sh

View File

@@ -10,3 +10,15 @@ DB_PASSWORD=postgres
KAFKA_SERVERS=localhost:9095
OTLP_TRACING_HTTP_URL=http://localhost:4318/v1/traces
# jvm tuning
TOMCAT_THREADS_MAX=30
#TOMCAT_THREADS_MIN=
HIKARI_DB_MAXIMUM_POOL_SIZE=4
#HIKARI_DB_MINIMUM_IDLE_SIZE
#JVM_NATIVE_MB=120
#RESERVED_CODE_CACHE_SIZE_MB=64
MAX_METASPACE_SIZE_MB=100
#DIRECT_BYTES_BUFFERS_MB=10
#COMPRESSED_CLASS_SPACE_MB=16
#OVERHEAD_GC_SIZE_PERCENT=5

View File

@@ -1,3 +1,9 @@
FROM openjdk:17-jdk-slim
COPY --chmod=777 build/libs/demo-single-version.jar /application/demo.jar
CMD ["java", "-jar", "/application/demo.jar"]
FROM eclipse-temurin:20-jdk
WORKDIR /app
COPY ./entrypoint.sh .
RUN chmod +x ./entrypoint.sh
COPY ./build/libs/*.jar .
ENTRYPOINT ["/app/entrypoint.sh"]

278
doc/entrypoint.md Normal file
View File

@@ -0,0 +1,278 @@
# Entrypoint: Запуск JAR-файла и конфигурация JVM
_LLM-generated документация._
## Оглавление
- [Обзор](#обзор)
- [Параметры запуска](#параметры-запуска)
- [Алгоритм работы](#алгоритм-работы)
- [1. Определение ограничений памяти контейнера](#1-определение-ограничений-памяти-контейнера)
- [2. Конфигурация компонентов приложения](#2-конфигурация-компонентов-приложения)
- [Переменные окружения со значениями по умолчанию:](#переменные-окружения-со-значениями-по-умолчанию)
- [3. Расчет "складских" ресурсов](#3-расчет-складских-ресурсов)
- [4. Расчет доступной памяти для JVM](#4-расчет-доступной-памяти-для-jvm)
- [5. Настройка Non-Heap областей JVM](#5-настройка-non-heap-областей-jvm)
- [Конфигурация областей памяти:](#конфигурация-областей-памяти)
- [6. Расчет Heap памяти](#6-расчет-heap-памяти)
- [Этап 1: Память доступная для Heap + GC](#этап-1-память-доступная-для-heap--gc)
- [Этап 2: Резерв для GC overhead](#этап-2-резерв-для-gc-overhead)
- [Этап 3: Финальный размер Heap](#этап-3-финальный-размер-heap)
- [Этап 4: Процент от MaxRAM для JVM](#этап-4-процент-от-maxram-для-jvm)
- [JVM флаги и их назначение](#jvm-флаги-и-их-назначение)
- [Контейнерная поддержка](#контейнерная-поддержка)
- [Управление памятью](#управление-памятью)
- [Оптимизация производительности](#оптимизация-производительности)
- [Дополнительные опции](#дополнительные-опции)
- [Экспортируемые переменные](#экспортируемые-переменные)
- [Пример расчета](#пример-расчета)
- [Дано:](#дано)
- [Расчет:](#расчет)
- [Результирующая команда Java:](#результирующая-команда-java)
- [Мониторинг и отладка](#мониторинг-и-отладка)
- [Verbose режим (`-v`)](#verbose-режим--v)
- [Рекомендуемые размеры контейнеров](#рекомендуемые-размеры-контейнеров)
- [Рекомендации по настройке:](#рекомендации-по-настройке)
- [Troubleshooting](#troubleshooting)
- [Проблема: Приложение падает с OOM](#проблема-приложение-падает-с-oom)
- [Проблема: Медленная работа GC](#проблема-медленная-работа-gc)
- [Проблема: Много database connections timeout](#проблема-много-database-connections-timeout)
## Обзор
Скрипт `entrypoint.sh` выполняет интеллектуальную настройку JVM на основе ограничений памяти контейнера. Он автоматически вычисляет оптимальные размеры heap, non-heap областей и других параметров для обеспечения стабильной работы Spring Boot приложения.
### Параметры запуска
- **`-v`** - включает verbose режим с выводом отладочной информации о расчетах памяти
- **`JAR_FILE_NAME`** - имя JAR-файла для запуска (позиционный аргумент)
**Пример использования:**
```bash
./entrypoint.sh -v app.jar # С отладочной информацией
./entrypoint.sh app.jar # Без отладочной информации
```
## Алгоритм работы
### 1. Определение ограничений памяти контейнера
```bash
# Проверка версии cgroups и получение лимита памяти
if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
# Cgroups v1
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
elif [ -f /sys/fs/cgroup/memory.max ]; then
# Cgroups v2
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory.max)
else
# Ошибка: нет доступа к информации о памяти
exit 1
fi
```
**Результат:** Получение лимита памяти контейнера в байтах.
### 2. Конфигурация компонентов приложения
#### Переменные окружения со значениями по умолчанию:
| Переменная | Значение по умолчанию | Описание |
|-------------------------------|-------------------------------|----------------------------------------|
| `TOMCAT_THREADS_MAX` | 100 | Максимальное количество потоков Tomcat |
| `TOMCAT_THREADS_MIN` | = TOMCAT_THREADS_MAX | Минимальное количество потоков Tomcat |
| `HIKARI_DB_MAXIMUM_POOL_SIZE` | 10 | Максимальный размер пула соединений БД |
| `HIKARI_DB_MINIMUM_IDLE_SIZE` | = HIKARI_DB_MAXIMUM_POOL_SIZE | Минимальное количество idle соединений |
| `JVM_NATIVE_MB` | 120 | Резерв памяти для native кода JVM |
### 3. Расчет "складских" ресурсов
```bash
STOCK_SIZE_MB = (TOMCAT_THREADS_MAX / 2) + HIKARI_DB_MAXIMUM_POOL_SIZE + JVM_NATIVE_MB
```
**Логика расчета:**
- **Tomcat потоки:** каждый поток потребляет ~0.5 MB памяти
- **Пул соединений БД:** каждое соединение ~1 MB
- **Native память JVM:** фиксированный резерв для JNI, сжатия, etc.
**Пример:** При дефолтных значениях: `(100/2) + 10 + 120 = 180 MB`
### 4. Расчет доступной памяти для JVM
```bash
LIMITED_MAXRAM_JAVA_SIZE_MB = LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB - STOCK_SIZE_MB
```
**Назначение:** Память, которую JVM может использовать без риска превышения лимитов контейнера.
### 5. Настройка Non-Heap областей JVM
#### Конфигурация областей памяти:
| Область | Переменная | Значение по умолчанию | Назначение |
|----------------------------|-------------------------------|-----------------------|----------------------------|
| **Code Cache** | `RESERVED_CODE_CACHE_SIZE_MB` | 64 MB | Компилированный JIT код |
| **Metaspace** | `MAX_METASPACE_SIZE_MB` | 80 MB | Метаданные классов |
| **Direct Buffers** | `DIRECT_BYTES_BUFFERS_MB` | 10 MB | NIO буферы |
| **Compressed Class Space** | `COMPRESSED_CLASS_SPACE_MB` | 16 MB | Сжатые указатели на классы |
```bash
NON_HEAP_SIZE_MB = RESERVED_CODE_CACHE_SIZE_MB + MAX_METASPACE_SIZE_MB +
DIRECT_BYTES_BUFFERS_MB + COMPRESSED_CLASS_SPACE_MB
```
### 6. Расчет Heap памяти
#### Этап 1: Память доступная для Heap + GC
```bash
HEAP_GC_SIZE_MB = LIMITED_MAXRAM_JAVA_SIZE_MB - NON_HEAP_SIZE_MB
```
#### Этап 2: Резерв для GC overhead
```bash
OVERHEAD_GC_SIZE_PERCENT = 5 # По умолчанию 5%
OVERHEAD_GC_SIZE_MB = (HEAP_GC_SIZE_MB * OVERHEAD_GC_SIZE_PERCENT) / 100
```
#### Этап 3: Финальный размер Heap
```bash
HEAP_SIZE_MB = HEAP_GC_SIZE_MB - OVERHEAD_GC_SIZE_MB
```
#### Этап 4: Процент от MaxRAM для JVM
```bash
MAX_RAM_PERCENTAGE = (HEAP_SIZE_MB * 100) / LIMITED_MAXRAM_JAVA_SIZE_MB
```
## JVM флаги и их назначение
### Контейнерная поддержка
- **`-XX:+UseContainerSupport`** - Включает автоопределение ресурсов контейнера
- **`-XX:MaxRAM="${LIMITED_MAXRAM_JAVA_SIZE_MB}m"`** - Максимальная память для JVM
- **`-XX:MaxRAMPercentage="$MAX_RAM_PERCENTAGE"`** - Процент MaxRAM для heap
### Управление памятью
- **`-XX:+ExitOnOutOfMemoryError`** - Принудительное завершение при OOM
- **`-XX:MaxMetaspaceSize="${MAX_METASPACE_SIZE_MB}m"`** - Лимит Metaspace
### Оптимизация производительности
- **`-XX:+SegmentedCodeCache`** - Сегментированный code cache для лучшей производительности
- **`-XX:ReservedCodeCacheSize="${RESERVED_CODE_CACHE_SIZE_MB}m"`** - Размер code cache
### Дополнительные опции
- **`$JAVA_OPTS_OVERRIDE`** - Переменная для переопределения опций
## Экспортируемые переменные
Скрипт экспортирует рассчитанные значения для использования приложением:
```bash
export TOMCAT_THREADS_MAX=$TOMCAT_THREADS_MAX
export TOMCAT_THREADS_MIN=$TOMCAT_THREADS_MIN
export HIKARI_DB_MAXIMUM_POOL_SIZE=$HIKARI_DB_MAXIMUM_POOL_SIZE
export HIKARI_DB_MINIMUM_IDLE_SIZE=$HIKARI_DB_MINIMUM_IDLE_SIZE
```
## Пример расчета
### Дано:
- Лимит контейнера: **512 MB**
- Дефолтные значения всех переменных
### Расчет:
1. **Складские ресурсы:**
```
STOCK_SIZE_MB = (100/2) + 10 + 120 = 180 MB
```
2. **Доступная память для JVM:**
```
LIMITED_MAXRAM_JAVA_SIZE_MB = 512 - 180 = 332 MB
```
3. **Non-heap память:**
```
NON_HEAP_SIZE_MB = 64 + 80 + 10 + 16 = 170 MB
```
4. **Память для Heap + GC:**
```
HEAP_GC_SIZE_MB = 332 - 170 = 162 MB
```
5. **GC overhead:**
```
OVERHEAD_GC_SIZE_MB = (162 * 5) / 100 = 8 MB
```
6. **Финальный размер Heap:**
```
HEAP_SIZE_MB = 162 - 8 = 154 MB
```
7. **Процент RAM для heap:**
```
MAX_RAM_PERCENTAGE = (154 * 100) / 332 = 46%
```
### Результирующая команда Java:
```bash
exec java \
-XX:+UseContainerSupport \
-XX:+ExitOnOutOfMemoryError \
-XX:MaxRAM="332m" \
-XX:MaxRAMPercentage="46" \
-XX:+SegmentedCodeCache \
-XX:ReservedCodeCacheSize="64m" \
-XX:MaxMetaspaceSize="80m" \
-jar /app/app.jar
```
## Мониторинг и отладка
### Verbose режим (`-v`)
При запуске с флагом `-v` скрипт выводит отладочную информацию:
```bash
# Версия cgroups
Cgroups v2 are used
# Расчеты памяти
LIMITED_MAXRAM_JAVA_SIZE_MB=332
HEAP_SIZE_MB=154
MAX_RAM_PERCENTAGE=46
```
**Без флага `-v`** - никаких отладочных сообщений не выводится, только стандартные логи приложения.
### Рекомендуемые размеры контейнеров
| Размер контейнера | Heap | Статус |
|-------------------|-----------------|---------------------|
| **≤ 352 MB** | ❌ Отрицательный | Не работает |
| **353 MB** | 🔴 3 MB | Критический минимум |
| **400 MB** | 🟡 47 MB | Базовый |
| **512 MB** | 🟢 154 MB | Рекомендуемый |
| **768 MB** | 🟢 380 MB | Комфортный |
### Рекомендации по настройке:
1. **Для высоко нагруженных приложений** - увеличьте `TOMCAT_THREADS_MAX`
2. **Для БД-интенсивных приложений** - увеличьте `HIKARI_DB_MAXIMUM_POOL_SIZE`
3. **Для приложений с большим количеством классов** - увеличьте `MAX_METASPACE_SIZE_MB`
4. **При частых OOM** - уменьшите `OVERHEAD_GC_SIZE_PERCENT` или увеличьте память контейнера
## Troubleshooting
### Проблема: Приложение падает с OOM
**Решение:** Проверьте соотношение heap/non-heap памяти, возможно нужно увеличить лимит контейнера.
### Проблема: Медленная работа GC
**Решение:** Увеличьте `OVERHEAD_GC_SIZE_PERCENT` или используйте альтернативный GC через `JAVA_OPTS_OVERRIDE`.
### Проблема: Много database connections timeout
**Решение:** Увеличьте `HIKARI_DB_MAXIMUM_POOL_SIZE` и соответственно лимит памяти контейнера.

View File

@@ -5,16 +5,16 @@ services:
build:
dockerfile: Dockerfile
context: .
environment:
SPRING_LOG_LEVEL: $SPRING_LOG_LEVEL
SPRING_ACTIVE_PROFILE: $SPRING_ACTIVE_PROFILE
DB_URL: $DB_URL
DB_NAME: $DB_NAME
DB_SCHEMA: $DB_SCHEMA
DB_USERNAME: $DB_USERNAME
DB_PASSWORD: $DB_PASSWORD
KAFKA_SERVERS: $KAFKA_SERVERS
OTLP_TRACING_HTTP_URL: $OTLP_TRACING_HTTP_URL
env_file:
- .env
expose:
- 8080
- 8081
command:
- app.jar
healthcheck:
test: [ "CMD", "curl", "--fail", "--silent", "http://localhost:8081/health" ]
interval: 30s
timeout: 10s
retries: 5
start_period: 10s

View File

@@ -5,19 +5,21 @@ import com.github.dannecron.demo.edgecontracts.validation.SchemaValidatorImp
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.util.ResourceUtils
import org.springframework.core.io.ResourceLoader
@Configuration
@EnableConfigurationProperties(ValidationProperties::class)
class SchemaValidationConfig(
private val validationProperties: ValidationProperties,
private val resourceLoader: ResourceLoader,
) {
@Bean
fun schemaValidator(): SchemaValidator = SchemaValidatorImp(
schemaMap = validationProperties.schema.mapValues {
schema -> ResourceUtils.getFile("classpath:json-schemas/${schema.value}")
.readText(Charsets.UTF_8)
schema -> resourceLoader.getResource("classpath:json-schemas/${schema.value}")
.takeIf { it.exists() }!!
.getContentAsString(Charsets.UTF_8)
}
)
}

77
entrypoint.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
VERBOSE=false
while getopts "v" arg; do
case $arg in
v )
VERBOSE=true
;;
* )
;;
esac
done
# Сдвигаем позиционные параметры после обработки опций
shift $((OPTIND-1))
JAR_FILE_NAME=$1
if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]
then
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
if [ $VERBOSE = true ]; then echo "Cgroups v1 are used"; fi
elif [ -f /sys/fs/cgroup/memory.max ]
then
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory.max)
if [ $VERBOSE = true ]; then echo "Cgroups v2 are used"; fi
else
echo "No cgroups files with memory limits"; exit 1
fi
#
LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB=$((LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES / 1024 / 1024))
TOMCAT_THREADS_MAX=${TOMCAT_THREADS_MAX:-100}
TOMCAT_THREADS_MIN=${TOMCAT_THREADS_MIN-$TOMCAT_THREADS_MAX}
HIKARI_DB_MAXIMUM_POOL_SIZE=${HIKARI_DB_MAXIMUM_POOL_SIZE:-10}
HIKARI_DB_MINIMUM_IDLE_SIZE=${HIKARI_DB_MINIMUM_IDLE_SIZE:-$HIKARI_DB_MAXIMUM_POOL_SIZE}
JVM_NATIVE_MB=${JVM_NATIVE_MB:-120}
STOCK_SIZE_MB=$((TOMCAT_THREADS_MAX / 2 + HIKARI_DB_MAXIMUM_POOL_SIZE + JVM_NATIVE_MB))
LIMITED_MAXRAM_JAVA_SIZE_MB=$((LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB - STOCK_SIZE_MB))
#
RESERVED_CODE_CACHE_SIZE_MB=${RESERVED_CODE_CACHE_SIZE_MB:-64}
MAX_METASPACE_SIZE_MB=${MAX_METASPACE_SIZE_MB:-80}
DIRECT_BYTES_BUFFERS_MB=${DIRECT_BYTES_BUFFERS_MB:-10}
COMPRESSED_CLASS_SPACE_MB=${COMPRESSED_CLASS_SPACE_MB:-16}
NON_HEAP_SIZE_MB=$((RESERVED_CODE_CACHE_SIZE_MB + MAX_METASPACE_SIZE_MB + DIRECT_BYTES_BUFFERS_MB + COMPRESSED_CLASS_SPACE_MB))
#
HEAP_GC_SIZE_MB=$((LIMITED_MAXRAM_JAVA_SIZE_MB - NON_HEAP_SIZE_MB))
OVERHEAD_GC_SIZE_PERCENT=${OVERHEAD_GC_SIZE_PERCENT:-5}
OVERHEAD_GC_SIZE_MB=$((HEAP_GC_SIZE_MB * OVERHEAD_GC_SIZE_PERCENT / 100))
HEAP_SIZE_MB=$((HEAP_GC_SIZE_MB - OVERHEAD_GC_SIZE_MB))
#
MAX_RAM_PERCENTAGE=$((HEAP_SIZE_MB * 100 / LIMITED_MAXRAM_JAVA_SIZE_MB))
# export calculated environments
export TOMCAT_THREADS_MAX=$TOMCAT_THREADS_MAX
export TOMCAT_THREADS_MIN=$TOMCAT_THREADS_MIN
export HIKARI_DB_MAXIMUM_POOL_SIZE=$HIKARI_DB_MAXIMUM_POOL_SIZE
export HIKARI_DB_MINIMUM_IDLE_SIZE=$HIKARI_DB_MINIMUM_IDLE_SIZE
if [ $VERBOSE = true ]
then
echo "LIMITED_MAXRAM_JAVA_SIZE_MB=$LIMITED_MAXRAM_JAVA_SIZE_MB"
echo "HEAP_SIZE_MB=$HEAP_SIZE_MB"
echo "MAX_RAM_PERCENTAGE=$MAX_RAM_PERCENTAGE"
fi
# shellcheck disable=SC2086
exec java \
-XX:+UseContainerSupport \
-XX:+ExitOnOutOfMemoryError \
-XX:MaxRAM="${LIMITED_MAXRAM_JAVA_SIZE_MB}m" \
-XX:MaxRAMPercentage="$MAX_RAM_PERCENTAGE" \
-XX:+SegmentedCodeCache \
-XX:ReservedCodeCacheSize="${RESERVED_CODE_CACHE_SIZE_MB}m" \
-XX:MaxMetaspaceSize="${MAX_METASPACE_SIZE_MB}m" \
$JAVA_OPTS_OVERRIDE \
-jar /app/"$JAR_FILE_NAME"

View File

@@ -31,7 +31,7 @@ Demo приложение для изучения языка `kotlin` и фре
* убедиться, что все запущенные контейнеры будут видеть контейнер с приложением (например, добавить везде сеть `spring-boot-demo_default`)
* скопировать [.env.example](/.env.example) в [.env](/.env) и изменить конфигурацию
Перед каждым запуском необходимо собрать приложение:
После каждого изменения в исходный код необходимо собрать приложение:
```shell
./gradlew assemble
```

View File

@@ -9,6 +9,8 @@ spring:
driver-class-name: org.postgresql.Driver
hikari:
schema: ${DB_SCHEMA:public}
maximum-pool-size: ${HIKARI_DB_MAXIMUM_POOL_SIZE}
minimum-idle: ${HIKARI_DB_MINIMUM_IDLE_SIZE}
flyway: #flyway automatically uses the datasource from the application to connect to the DB
enabled: true # enables flyway database migration
locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
@@ -98,6 +100,12 @@ management:
sampling:
probability: 1.0
server:
tomcat:
threads:
max: ${TOMCAT_THREADS_MAX}
min-spare: ${TOMCAT_THREADS_MIN}
tracing:
url: ${OTLP_TRACING_HTTP_URL}