Алгоритм расчета память для java-сервисов

Вводная часть

Данный раздел посвящен объяснению работы и возможностью переиспользования утилиты по динамическому расчёту памяти для systemd сервисов.

Теоретическая часть

Общая формула:

Количество доступной RAM = Общее RAM - Резерв операционной системы - Безопасный буфер
Лимит сервисов = (Количество доступной RAM * Приоритет) / Сумму всех приоритетов
Бюджет JVM = Лимит сервиса - NonHeap резерв
Xmx = (Бюджет JVM * HeapRatio) -> округление -> ограничения
MemoryMax = (Xmx / MaxRAMPercentage) -> округляем чтобы была кратна 2-м(128)

Определение доступной памяти

Общий RAM системы - общий RAM КБ / 1024

Автоматический расчёт резервов - чем больше памяти в системе, тем меньше процент резерва (эффект масштаба)

RAM системыРезерв ОСБезопасный буферОбщий резерв
=< 2 ГБ20%10%30%
=< 8 ГБ15%8%23%
=< 32 ГБ10%5%15%
> 32 ГБ8%4%12%
RAM_available = RAM_total - OS_reserve - safety_buffer

Распределение по приоритетам

Приоритет - это относительная доля, а не абсолютное значение. Увеличение приоритета одного сервиса уменьшает долю остальных

Конфигурация сервисов в формате - service_name:priority:java_version, где 1 это низкий, а 5 это высокий

Формула распределения MemoryLimit_service = (RAM_available * priority_service) / All_weights

Пример: RAM_available = 10000 MB и сервисы: pam-wdog(5) + pam-gui(10) + pam-auth(1) = 16

СервисПриоритетРасчётЛимит
pam-gui1010000 * 10 / 166250 MB
pam-wdog510000 * 5 / 163125 MB
pam-auth110000 * 1 / 16625 MB

Расчёт Non-Heap резерва

JVM использует память не только для кучи (Heap). Скрипт автоматически резервирует место под:

  1. Metaspace (метаданные классов)
  2. CodeCache (JIT-компиляция)
  3. Потоки (stack memory)
  4. Direct buffers (NIO)
  5. GC overhead

Базовая формула: NonHeap = 400 MB + JavaAdjustment + PriorityAdjustment

Корректировки:

УсловиеКорректировкаПояснение
По умолчанию+400 MBСтандартный резерв
Java 8+50 MBТребует больше резерва
Java 21+-50 MBБолее эффективное управление памятью
Приоритет ≥ 5+100 MBБольше запаса для критичных сервисов
Приоритет =< 2-50 MBМеньше запаса для лёгких сервисов

Пример:

# pam-wdog: priority=5, Java=8
NonHeap = 400 + 50 (Java8) + 100 (high-priority) = 550 MB

# pam-auth: priority=1, Java=21
NonHeap = 400 - 50 (Java21) - 50 (low-priority) = 300 MB

Расчёт Heap (Xmx)

Формула JVM бюджета: JVM_budget = MemoryLimit_service - NonHeap

Heap Ratio по приоритету. Определяет, какую часть JVM-бюджета отдать под Heap, а сколько оставить на “надстройку”

ПриоритетHeap RatioПояснение
≥ 5 (критичный)0.70Больше запаса на Non-Heap для стабильности
3–4 (средний)0.75Баланс
=< 2 (лёгкий)0.80Максимум памяти под данные, минимум запаса

Финальный расчёт Xmx. Округление до 64 МБ для совместимости с аллокаторами памяти. Максимум 90% от лимита для защиты от выхода за границы cgroup

Xmx_raw = JVM_budget * HeapRatio
Xmx = round_up_to_64MB(Xmx_raw)
Xmx = clamp(Xmx, min=256, max=MemoryLimit * 0.90)

Пример полного расчёта:

Сервис: pam-wdog, priority=5, Java=8
Система: RAM_available = 10000 MB, All_weights = 16

1. MemoryLimit = 10000 * 5 / 16 = 3125 MB
2. NonHeap = 400 + 50 + 100 = 550 MB
3. JVM_budget = 3125 - 550 = 2575 MB
4. HeapRatio = 0.70 (priority ≥ 5)
5. Xmx_raw = 2575 * 0.70 = 1802.5 MB
6. Xmx = round_up_64(1802.5) = 1856 MB
7. Проверка: 1856 < (3125 * 0.90 = 2812)

Итог: -Xms1856m -Xmx1856m

Использование -Xms (начальный размер кучи) и -Xmx (максимальный размер кучи) с одним и тем же значением рекомендуется для высоконагруженных Java-приложений
Это обеспечивает предсказуемость, устраняет паузы при изменении размера кучи JVM и повышает производительность за счет выделения памяти заранее

Расчёт MemoryMax для systemd

Чтобы systemd не убил процесс раньше времени, устанавливаем лимит cgroup

MemoryMax = Xmx / MaxRAMPercentage
MemoryMax = round_up_to_128MB(MemoryMax)

При MaxRAMPercentage=75.0:

MemoryMax = Xmx / 0.75 = Xmx * 1.333...

Пример:

Xmx = 1856 MB
MemoryMax = 1856 / 0.75 = 2474.67 -> round_up_128 = 2560 MB

systemd: MemoryMax=2560M

Сводная таблица параметров

ПараметрФормулаЗначение
RAM_availableRAM_total - OS_reserve - safety10000 MB
MemoryLimit(RAM_avail * priority) / All_weights3125 MB
NonHeap400 + java_adj + priority_adj550 MB
JVM_budgetMemoryLimit - NonHeap2575 MB
HeapRatio0.70 / 0.75 / 0.800.70
Xmxclamp(round64(JVM_budget * ratio), 256, 90%)1856 MB
MemoryMaxround128(Xmx / 0.75)2560 MB

Визуализация распределения памяти

Система: 16 ГБ (16384 MB)
├─ Резерв ОС (15%)        : 2458 MB
├─ Буфер безопасности (8%): 1311 MB
└─ Доступно сервисам      : 12615 MB
  │
  ├─ pam-gui (pri=10, 62.5%)
  │  ├─ Лимит             : 7884 MB
  │  ├─ NonHeap           : 550 MB
  │  ├─ Heap (70%)        : ~5136 MB -> -Xmx5120m
  │  └─ MemoryMax         : 6912M
  │
  ├─ pam-wdog (pri=5, 31.25%)
  │  ├─ Лимит             : 3942 MB
  │  ├─ NonHeap           : 550 MB
  │  ├─ Heap (70%)        : ~2374 MB -> -Xmx2400m
  │  └─ MemoryMax         : 3200M
  │
  └─ pam-auth (pri=1, 6.25%)
      ├─ Лимит            : 788 MB
      ├─ NonHeap          : 300 MB
      ├─ Heap (80%)       : ~390 MB -> -Xmx384m
      └─ MemoryMax        : 512M

Рекомендации по настройке приоритетов

Сумма приоритетов не ограничена, но разумный диапазон - 1..10. Слишком большой разрыв (1-100) лишает лёгкие сервисы памяти

ПриоритетНазначениеПримеры
1–2 (низкий)Фоновые задачи, health-check, утилитыhealth-check, log-shipper
3–4 (средний)Обычные микросервисы, не критичные к просадкамnotification, reporting
5–7 (высокий)Критичные сервисы, APIauth, order-service
8–10 (критичный)Ядро системы, шлюзыpayment, api-gateway

Практическая часть

В данном блоке будут рассмотрены частные случаи использования утилиты как отдельного компонента для динамического перерасчёта памяти для сервисов

Пример получения справки

→ sudo ./auto-java-memory.sh --help
auto-java-memory.sh - automatic memory calculation for Java services
Usage: auto-java-memory.sh --config <services.conf> --unit-dir <path> [--dry-run] [--verbose]
Conf file format parse style: service_name:priority:java_version
Example: pam-gui:5:8 pam-auth:3:17 pam-ssh:1:21
Parameters:
  --config - path to the file containing the list of services
  --unit-dir - directory with systemd unit files (default: /etc/systemd/system)
  --dry-run - show calculations without application
  --verbose - verbose output
  --help - show help
Usage example: auto-java-memory.sh --config /etc/app/services.conf --unit-dir /etc/systemd/system --dry-run
Version: v1.0.0

Пример использования в инсталляторе

sudo bash "./tools/auto-java-memory.sh" \
  --config "./tools/upgrade-services-unit.conf" \
  --unit-dir /usr/lib/systemd/system
systemctl daemon-reload
SERVICES=(
  "pam-gui.service" "log.io-harvester.service" "log.io-server.service"
  "pam-ssh.service" "pam-sftp.service" "pam-sql.service"
  "pam-http.service" "pam-rdpgw.service" "pam-radius.service"
  "pam-wdog.service" "pam-diag.timer" "pam-diag.service"
  "pam-auth.service" "pam-tacacs.service"
)
for i in "${SERVICES[@]}"; do
  log_line "info" "Activating '${i}' with the latest configuration..."
  systemctl enable --now "${i}"
  sleep 5
  systemctl -q is-active "${i}" || log_line "warning" "service '${i}' is not active at now"
done

Пример использования при перерасчёте памяти

# Запуская проверку, чтобы убудиться о том, какие параметры в соотвествии с весами будут применены
sudo ./auto-java-memory.sh \
  --config ./upgrade-services-unit.conf \
  --unit-dir /usr/lib/systemd/system \
  --dry-run

# Запуск "боевого" режима
sudo ./auto-java-memory.sh \
  --config ./upgrade-services-unit.conf \
  --unit-dir /usr/lib/systemd/system
systemctl daemon-reload
iuda -r all

Дополнительные хистрости

Данный раздел будет посвящен больше не утилите, а о работе с systemd сервисами. Бывают ситуации, когда 1 сервис начинает потреблять большое количество CPU/IO ресурсов, что, конечно, не приятно. Об этом нужно сообщать разработчикам, чтобы вышел соотвествующий патч, но что делать ДО этого момента? Systemd умеет работать напрямую с Cgroupsv2 - механизм ограничения сервисов, при котором напрямую сообщается о том скролько ресурсов выделять ему в данный момент. Пример содержимого:

Пример ограничения юнита systemd

[Service]
MemoryMax=256M
MemorySwapMax=0
CPUQuota=50%
IOReadBandwidthMax=/dev/sda 10M
IOWriteBandwidthMax=/dev/sda 5M
TasksMax=100

Цикл статей по этой тематике с практическим применением “временных заплаток до выхода патча”:

Как ограничить ресурсы процессов в Linux с помощью cgroups и systemd

Зачем администратору разбираться в cgroups v2, даже если «есть контейнеры»