Quarkus и Micronaut запускаются на обычном Java-сервере как обычный jar, и на небольшом тарифе их проще уместить в лимиты, чем многие другие фреймворки: внедрение зависимостей они выполняют на этапе сборки, поэтому стартуют за секунду-две и занимают меньше heap, чем сопоставимый сервис на Spring Boot. Людей подводят три вещи: структура артефакта (Quarkus по умолчанию не создаёт один файл), лимит памяти (JVM охотно запросит больше, чем вы оплатили) и пути health-эндпоинтов, которые отличаются от тех, что использует Spring. В этой статье разобраны все три, а также вопрос, который задают первым: собирать native-исполняемый файл или просто работать на JVM?
Это намеренно дополнение к статье о развёртывании Spring Boot-приложения в продакшене, где описан общий путь для fat jar: версии Java, внешняя конфигурация, HikariCP, заголовки прокси. Здесь мы остановимся на том, чем отличаются Quarkus и Micronaut, и повторим только то, что нужно, чтобы не потерять нить.
Чем это отличается от fat jar в Spring Boot#
Сборка Spring Boot даёт один исполняемый файл. Quarkus и Micronaut дают то же самое в других формах, а форма определяет, что именно загружать и какой командой запускать.
| Фреймворк | Результат сборки по умолчанию | Запуск |
|---|---|---|
| Quarkus | каталог target/quarkus-app/ | java -jar target/quarkus-app/quarkus-run.jar |
| Quarkus (uber-jar) | target/*-runner.jar | java -jar target/*-runner.jar |
| Micronaut (Gradle) | build/libs/*-all.jar | java -jar build/libs/app-0.1-all.jar |
| Micronaut (Maven) | target/*.jar | java -jar target/app-0.1.jar |
Имена файлов Micronaut зависят от названия и версии проекта, а также от применённых плагинов, поэтому считайте приведённые имена примерами и после сборки посмотрите, что лежит в каталоге. В Micronaut с Gradle -all.jar создаёт плагин Shadow и задача shadowJar; с Maven сгенерированный проект использует плагин Shade или Micronaut Maven, чтобы получить запускаемый архив.
Общее у них вот что: оба фреймворка слушают порт, который вы выбираете, читают настройки из файлов и переменных окружения и требуют Java не ниже той версии, под которую они скомпилированы. Линейка Java-приложений RE:NODE запускает любой jar на Java 17, 21 или 25, а это покрывает Quarkus, Micronaut, Spring Boot и Ktor. Актуальные релизы Quarkus 3 и Micronaut 4 требуют Java 17 или новее; перед выбором проверьте версию своего фреймворка в примечаниях к релизу и компилируйте с флагом --release, соответствующим самой старой Java, на которой собираетесь работать. Jar, собранный под 17, работает на 21 и 25, но никогда наоборот.
Упаковка Quarkus: fast-jar, uber-jar и остальное#
Вариант Quarkus по умолчанию называется fast-jar. Это не один файл. Сборка создаёт каталог:
target/quarkus-app/ quarkus-run.jar lib/ boot/ main/ app/ quarkus/quarkus-run.jar - крошечный загрузчик. Зависимости лежат в lib/ в виде отдельных jar-файлов, ваши собственные классы - в app/, а сгенерированные при сборке данные - в quarkus/. Такая структура нужна ради одного: старт быстрее, когда среде выполнения не нужно открывать и сканировать один огромный архив. Она же удешевляет развёртывание, потому что большая часть каталога между релизами не меняется - меняются только app/ и quarkus/, так что инкрементальная загрузка (или слоистый образ контейнера) передаёт несколько сотен килобайт вместо пятидесяти мегабайт.
Для хостинга следствие простое, но легко ошибиться: загружать нужно весь каталог `quarkus-app`, а не только `quarkus-run.jar`. Один загрузчик падает с ошибкой загрузки классов, потому что рядом нет lib/. Заархивируйте каталог в CI, загрузите архив и распакуйте на месте. Файловый менеджер панели распаковывает архивы там, где они лежат, а это именно то, что здесь нужно.
$ ./mvnw -B clean package$ tar -czf quarkus-app.tar.gz -C target quarkus-app$ ls -lh quarkus-app.tar.gzТип упаковки задаётся одним параметром в application.properties или в командной строке через -D:
quarkus.package.jar.type | Результат |
|---|---|
fast-jar | Структура каталога по умолчанию, описанная выше |
uber-jar | Один запускаемый *-runner.jar, внутри которого всё |
legacy-jar | Старый вариант: один jar плюс lib/, оставлен для совместимости |
mutable-jar | fast-jar, который можно заново обработать при старте, для инструментов разработки |
В более старых релизах Quarkus (до 3.9) параметр назывался quarkus.package.type, а значения были те же. Если сборка игнорирует вашу настройку, вы, скорее всего, читаете документацию не той мажорной версии.
quarkus.package.jar.type=uber-jarКогда uber-jar - правильный выбор? Когда ваш способ развёртывания действительно требует один файл: перетащить один jar в файловый менеджер или не менять команду запуска. Цена - более медленный старт и более крупный единый архив, который меняется целиком в каждом релизе. Если вы справляетесь с tar-архивом, оставьте fast-jar. Если нет, uber-jar подойдёт, и многие небольшие сервисы так и работают без проблем.
У Micronaut такого выбора нет. Сборка даёт запускаемый jar, а суффикс -all.jar (Gradle) подсказывает, какой файл загружать. Скорость старта достигается предварительной обработкой в процессоре аннотаций, а не структурой архива.
Native или JVM: что запускать#
Оба фреймворка умеют компилировать приложение в native-исполняемый файл с помощью GraalVM. Цифры, из-за которых это хочется, настоящие: старт за десятки миллисекунд и заметно меньший объём резидентной памяти. Но и издержки настоящие, и для небольшого размещённого сервиса они обычно перевешивают выигрыш.
| Режим JVM | Native-исполняемый файл | |
|---|---|---|
| Старт | Около секунды-двух | Десятки миллисекунд |
| Память в простое | Heap плюс накладные расходы JVM | Заметно ниже |
| Пиковая пропускная способность | Выше после прогрева | Часто ниже, нет JIT |
| Время сборки | От секунд до минуты | Минуты |
| Память при сборке | Скромная | Несколько гигабайт |
| Reflection, прокси | Просто работают | Должны быть известны на этапе сборки |
| Отладка, профилирование | Полный набор инструментов JVM | Ограничены |
| Переносимость | Любая машина с Java | Одна ОС и одна архитектура CPU |
Команды для справки:
# Quarkus: build a native executable (needs GraalVM or Mandrel installed)$ ./mvnw package -Dnative# Quarkus: build it inside a container, so you need no local GraalVM$ ./mvnw package -Dnative -Dquarkus.native.container-build=true# Micronaut, Gradle$ ./gradlew nativeCompile# Micronaut, Maven$ ./mvnw package -Dpackaging=native-imageQuarkus кладёт в target/ бинарный файл *-runner. Он работает только на той операционной системе и архитектуре CPU, под которые собран, - поэтому и существует сборка в контейнере: она даёт Linux-бинарник, даже если вы разрабатываете на другой системе.
Честная рекомендация для размещённого сервера приложений - режим JVM. Причин три. Во-первых, Java-линейка RE:NODE построена вокруг запуска jar на Java 17, 21 или 25; native-исполняемый файл - файл другого рода, и в документации не сказано, что Java-линейка его запускает, так что уточните у поддержки, прежде чем строить на этом планы. Во-вторых, native-сборке на несколько минут нужно несколько гигабайт памяти, а этому не место на продакшен-тарифе, который вы стараетесь держать небольшим, - это работа для CI. В-третьих, режим JVM с разумными флагами heap уже достаточно компактен: сервис на Quarkus или Micronaut со скромным деревом зависимостей помещается в тариф на 1 ГБ, самый дешёвый. Native стоит усилий, когда вы масштабируетесь до нуля и часто стартуете (функции, задания), запускаете очень много крошечных экземпляров или когда память - главное ограничение в более крупном развёртывании. Для одного сервиса на одном тарифе это затраты без отдачи.
Если вы всё же выбираете native, помните: reflection, загрузка ресурсов и динамические прокси должны быть объявлены на этапе сборки. Библиотекам, которые на них полагаются, нужна конфигурация или расширение. Оба фреймворка поставляют расширения для распространённых библиотек, а отсутствующая регистрация проявляется во время работы как class-not-found или недостающий ресурс, хотя в режиме JVM всё работало отлично, - поэтому запускайте интеграционные тесты на native-сборке, а не только на JVM. Для этого в Quarkus есть @QuarkusIntegrationTest.
Порты и привязка#
Порт прослушивания - первое, что ломается в любом фреймворке, а оба этих по умолчанию используют 8080. У вашего сервера есть выделенный порт, и приложение должно слушать именно его, на всех интерфейсах. О том, где задавать переменные, рассказано в статье про переменные окружения и секреты; ключи приведены ниже.
quarkus.http.host=0.0.0.0quarkus.http.port=${SERVER_PORT:8080}micronaut: server: port: ${SERVER_PORT:8080}${SERVER_PORT:8080} означает «используй переменную окружения, если она задана, иначе 8080», и это работает и на ноутбуке, и в продакшене без правок. Прежде чем полагаться на имя SERVER_PORT, загляните на вкладки Startup и Network и проверьте, какую переменную действительно предоставляет ваш сервер; панели на основе Pterodactyl обычно отдают выделенный порт под этим именем, но для своего сервера это нужно проверить.
Оба фреймворка также сопоставляют переменные окружения с ключами конфигурации вообще без кода: ключ пишется в верхнем регистре, а точки и дефисы заменяются подчёркиваниями.
| Ключ | Переменная окружения |
|---|---|
quarkus.http.port | QUARKUS_HTTP_PORT |
quarkus.datasource.jdbc.url | QUARKUS_DATASOURCE_JDBC_URL |
micronaut.server.port | MICRONAUT_SERVER_PORT |
datasources.default.url | DATASOURCES_DEFAULT_URL |
Адрес привязки по умолчанию в Quarkus зависит от режима: в режиме разработки это loopback-адрес, а при обычном запуске в продакшене - все интерфейсы. Явная настройка quarkus.http.host=0.0.0.0 ничего не стоит и снимает сомнения. Сервис, привязанный к 127.0.0.1 внутри своего контейнера, недоступен никому, и прокси сообщает о bad gateway, пока консоль уверяет, что приложение работает.
Профили работают одинаково в обоих. В Quarkus префикс %prod. привязывает ключ к профилю продакшена, а именно его использует java -jar, если не указано иное; остальные - %dev. и %test.. В Micronaut окружения задаются через MICRONAUT_ENVIRONMENTS=prod, что загружает application-prod.yml поверх application.yml.
quarkus.http.port=8080%prod.quarkus.http.port=${SERVER_PORT:8080}%prod.quarkus.log.level=INFO%dev.quarkus.log.level=DEBUGФлаги heap для малого лимита памяти#
Это самая важная часть. У контейнера жёсткий лимит памяти, а размер запрашиваемого heap JVM определяет сама. Подробно это описано в статье про Spring Boot; здесь - рабочая версия.
В RE:NODE, когда процесс достигает лимита памяти, ядро останавливает контейнер, и он чисто перезапускается, а не уходит в swap. Оригинальный Pterodactyl поступает наоборот. Для сервиса это лучшее поведение, но оно меняет то, как читать сбой. Бывает два совершенно разных случая:
- `java.lang.OutOfMemoryError: Java heap space` в консоли. JVM заметила, что heap кончился, и сообщила об этом. Heap слишком мал для данных, либо что-то утекает.
- Перезапуск вообще без ошибки Java. Весь процесс превысил лимит контейнера и был остановлен снаружи. Heap был слишком велик для тарифа, либо выросла память вне heap.
Второй случай встречается чаще, потому что память JVM - это не только heap. Она держит ещё metaspace для метаданных классов, стеки потоков, кэш JIT-кода, прямые буферы и структуры сборщика мусора. Quarkus добавляет Netty и Vert.x, которые выделяют прямые (внекучные) буферы для сетевого ввода-вывода; отчасти поэтому резидентный размер сервиса на Quarkus больше его -Xmx. Micronaut на Netty делает то же самое.
Задавайте heap как долю лимита контейнера, чтобы он следовал за вами при смене тарифа:
$ java -XX:MaxRAMPercentage=60.0 -XX:InitialRAMPercentage=25.0 \ -XX:+ExitOnOutOfMemoryError \ -jar target/quarkus-app/quarkus-run.jarЕсли команду запуска изменить нельзя, положите флаги в окружение: каждая запускаемая JVM читает JAVA_TOOL_OPTIONS и печатает Picked up JAVA_TOOL_OPTIONS: ... в начале консоли, так что видно, что настройка сработала:
JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=60.0 -XX:+ExitOnOutOfMemoryErrorНе путайте это с JAVA_OPTS и JAVA_OPTS_APPEND. Их читают скрипты запуска внутри образов контейнеров Red Hat и Quarkus, а не сам бинарник java, поэтому при обычном запуске java -jar они ничего не делают.
Отправные точки по размеру тарифа - это догадки, которые нужно заменить данными с графика:
| Память тарифа | Настройка heap | Почему |
|---|---|---|
| 1 GB | -Xmx256m to -Xmx384m | Примерно половина уходит на память вне heap и на ОС |
| 2 GB | MaxRAMPercentage=60 | Около 1,2 ГБ heap, остальное на накладные расходы |
| 4 GB | MaxRAMPercentage=65 | Накладные расходы - меньшая доля |
| 6 GB и больше | MaxRAMPercentage=70 | Оставьте запас на буферы и всплески |
Разумный способ найти свою цифру: задайте заведомо щедрый heap на тарифе со свободной памятью, прогоните настоящий трафик в течение суток и сравнивайте график памяти в консоли с лимитом. Небольшому REST-сервису на любом из фреймворков обычно хватает heap в 128-256 МБ; сервису, который кэширует данные или обрабатывает большие запросы, нужно больше. Задавайте heap по наблюдаемому значению плюс запас, а не как круглый процент от тарифа.
Несколько флагов заслуженно нужны на небольших тарифах:
| Флаг | Действие |
|---|---|
-XX:+ExitOnOutOfMemoryError | Завершить процесс при нехватке heap, чтобы платформа перезапустила чистый процесс |
-XX:MaxMetaspaceSize=128m | Ограничить рост метаданных классов |
-XX:MaxDirectMemorySize=128m | Ограничить прямые буферы, которые использует Netty |
-XX:+UseSerialGC | Сборщик с наименьшими накладными расходами для 1 CPU или небольшого heap |
-Xss512k | Стеки потоков меньше, если потоков много |
-XX:+ExitOnOutOfMemoryError - флаг, который большинство пропускает. JVM, выбросившая OOM по heap в одном потоке, часто остаётся живой, но испорченной: часть запросов обслуживает, часть заваливает. Выход даёт вам чистый процесс. Помните, что за перезапусками следят. Наблюдатель за сбоями опрашивает сервер каждые две минуты и ищет аптайм, пошедший назад, или сервер, ушедший в офлайн; перезапуски, которые вы запросили сами, не считаются, три перезапуска за час выводят предупреждение на странице сервера и открывают автоматический тикет, а шесть означают приостановку. Сервис, застрявший в цикле нехватки памяти, - ровно то, что он ищет, так что исправьте размеры, прежде чем оставлять сервис без присмотра.
Важен и CPU. Это жёсткое ограничение до купленной доли, и 100% означает медленную работу, но никогда не повод для приостановки. JVM подбирает размеры пулов потоков по числу CPU, которое видит, а оно может округлить долю в пол-ядра до единицы. Если старт медленный или задержки идут всплесками на самом маленьком тарифе, стоит попробовать -XX:ActiveProcessorCount=1. Общий урок статьи CPU или RAM для игровых серверов применим без изменений: выясните, какой ресурс является ограничивающим, прежде чем покупать больше другого.
Потоки, а не только heap
Оба фреймворка работают на Netty, и оба могут выполнять блокирующий код в пуле рабочих потоков. Quarkus по умолчанию задаёт размер этого пула по числу обнаруженных CPU, а ограничить его можно ключом quarkus.thread-pool.max-threads. Каждый поток стоит стека и некоторого объёма нативной памяти, поэтому пул из 200 рабочих потоков в сервисе, у которого одновременно обрабатывается не больше 10 запросов, лишь раздувает потребление. В Micronaut аналог находится в micronaut.executors, например micronaut.executors.io.type: fixed и micronaut.executors.io.nThreads: 16. Задайте столько, сколько сервис действительно использует.
Подключения к базе данных#
Параметры подключения зависят от того, какую базу вы подключаете. У серверов приложений есть слоты баз данных, которые создаются в панели со сгенерированными хостом, пользователем и паролем; отдельный тариф хостинга баз данных даёт PostgreSQL или MySQL 8.4 LTS с учётными данными, создаваемыми для каждого сервера. Храните значения в переменных окружения, а не внутри jar.
QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://db-host:5432/shopQUARKUS_DATASOURCE_USERNAME=shop_appQUARKUS_DATASOURCE_PASSWORD=change-mequarkus.datasource.db-kind=postgresqlquarkus.datasource.jdbc.max-size=8quarkus.datasource.jdbc.min-size=2Quarkus использует Agroal в качестве пула, а quarkus.datasource.jdbc.max-size - его потолок. В Micronaut источник данных настраивается в datasources.default, а пул по умолчанию - HikariCP:
datasources: default: url: ${DATASOURCES_DEFAULT_URL} username: ${DATASOURCES_DEFAULT_USERNAME} password: ${DATASOURCES_DEFAULT_PASSWORD} maximum-pool-size: 8Каким бы ни был пул, арифметика одна: каждое соединение удерживается базой данных, поэтому два экземпляра с пулом по восемь плюс задача миграции в сумме упираются в собственный лимит базы. Подробно это разобрано в статье о пулах соединений и лимитах. Пула из шести-восьми соединений хватает с запасом для сервиса на одном-двух ядрах.
Health-эндпоинты в Quarkus и Micronaut#
Ни один из фреймворков не использует /actuator/health из Spring. Оба дают health-эндпоинт только после добавления зависимости, и пути у них разные.
Quarkus: SmallRye Health
Добавьте расширение:
$ ./mvnw quarkus:add-extension -Dextensions="smallrye-health"Вы получите четыре эндпоинта с префиксом /q:
| Путь | На какой вопрос отвечает |
|---|---|
/q/health | Всё вместе: live, ready и started |
/q/health/live | Находится ли процесс в состоянии, в котором он может продолжать работу? |
/q/health/ready | Должен ли этот экземпляр получать трафик? |
/q/health/started | Завершился ли старт? |
Ответ имеет вид {"status":"UP","checks":[...]} с HTTP 200 либо DOWN с 503. Расширения добавляют проверки сами - расширение источника данных, например, добавляет проверку готовности базы, так что отсутствующая база окрашивает /q/health/ready в красный без вашего участия. Собственные проверки - это CDI-бины:
@ApplicationScoped@Readinesspublic class CacheWarmCheck implements HealthCheck { @Override public HealthCheckResponse call() { return HealthCheckResponse.up("cache-warm"); }}Префикс /q - это корневой путь Quarkus для неприкладных эндпоинтов, и он настраивается через quarkus.http.non-application-root-path. Если вы его переносите, обновите всё, что следит за сервисом.
Micronaut: модуль management
Добавьте в сборку micronaut-management. Эндпоинты включаются конфигурацией:
endpoints: health: enabled: true details-visible: NEVERБазовый путь - /health, ниже него находятся /health/liveness и /health/readiness. Параметр details-visible принимает NEVER, AUTHENTICATED или ANONYMOUS и определяет, кто видит разбивку по отдельным индикаторам. На публичном адресе оставляйте NEVER: подробности могут раскрыть имена хостов и версии. Чтобы вообще убрать management-эндпоинты с публичного порта, переместите их на отдельный через endpoints.all.port.
Что делает проверку хорошей
Liveness никогда не должна зависеть от базы данных или любого другого сервиса. Если зависит, сбой базы заставит платформу перезапускать совершенно здоровый процесс, и станет только хуже. Зависимостям место в readiness. Делайте каждую проверку дешёвой: индикатор, вызывающий три сторонних API, превращает каждый пинг мониторинга в нагрузку. Подробнее об этом, а также о второй половине истории - остановке, - в статье о плавной остановке и health-проверках. В Quarkus параметр quarkus.shutdown.timeout задаёт, как долго он ждёт завершения обрабатываемых запросов, когда вы нажимаете Restart.
Проверьте из консоли или со своей машины:
$ curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/q/health/ready200И не открывайте на публичном адресе ничего, кроме health. Метрики, dev UI Quarkus и эндпоинты /env и /beans в Micronaut раскрывают конфигурацию. Dev UI Quarkus активен только в режиме разработки, но перепроверьте, что ничего из добавленного вами в application.properties не включает management-пути в продакшене.
За прокси: TLS и реальные адреса клиентов#
Не завершайте TLS внутри приложения. В тарифы приложений входит слот обратного прокси: направьте A-запись на показанный адрес, и сертификат будет выпущен и продлён автоматически, в окне в 21 день. Ваше приложение отдаёт обычный HTTP на выделенном порту, а реальный адрес клиента приходит в X-Forwarded-For.
Ни один из фреймворков по умолчанию не доверяет пересылаемым заголовкам - по веской причине: их мог бы отправить любой клиент. Без этой настройки в качестве клиента вы видите адрес прокси, а перенаправления, построенные по запросу, получаются с http://.
quarkus.http.proxy.proxy-address-forwarding=truequarkus.http.proxy.allow-x-forwarded=truequarkus.http.proxy.enable-forwarded-host=trueВ Micronaut ключ, называющий заголовок с адресом клиента, - micronaut.server.client-address-header:
micronaut: server: client-address-header: X-Forwarded-ForВключайте это только тогда, когда вы действительно за прокси: иначе любой, кто может обратиться к порту приложения напрямую, сможет подделать свой адрес. После настройки отправьте запрос и выведите определённый адрес клиента через отладочный эндпоинт, а не полагайтесь на предположения. Всю цепочку объясняет статья что делает обратный прокси.
Развёртывание и запуск в RE:NODE#
Путь развёртывания тот же, что и для любого jar. Собирайте в CI или локально и там же гоняйте тесты. Сборка Quarkus или Micronaut легче native-сборки, но всё равно требует больше памяти, чем небольшой продакшен-тариф стоит тратить на это, а сборка там, где вы тестируете, делает артефакт воспроизводимым.
- Соберите артефакт:
./mvnw packageдля Quarkus (затем заархивируйтеtarget/quarkus-app) или./gradlew shadowJarдля Micronaut. - Загрузите его через файловый менеджер или SFTP. Оба есть на каждом сервере приложений, с SFTP-учётными данными для каждого сервера. Файловый менеджер распаковывает архивы на месте, что подходит для каталога Quarkus.
- Задайте переменные окружения на вкладке Startup: порт, параметры источника данных,
JAVA_TOOL_OPTIONSи профиль (QUARKUS_PROFILE=prodилиMICRONAUT_ENVIRONMENTS=prod). - Укажите в настройках запуска нужный файл, затем запустите из консоли и дождитесь строки о старте фреймворка.
Альтернатива - развёртывание из GitHub. Панель делает это через GitHub App с короткоживущими токенами, поэтому приватные репозитории работают, и есть два переключателя: pull при каждом запуске и deploy при push, который перезапускает работающий сервер и запускает остановленный. Только GitHub. Это путь «из исходников на сервер», поэтому он подходит, только если на сервере будет лежать результат сборки; практичный вариант - ветка, содержащая только готовый артефакт релиза. Перед привязкой предварительная проверка читает репозиторий и перечисляет то, что сломается, начиная с худшего, например привязку к неверному порту или хосту.
Новый Java-сервер запускается с небольшой рабочей страницей по своему адресу, пока вы не развернёте код, и эта страница никогда не перезаписывает ваши файлы.
Решение проблем#
`Error: Unable to access jarfile quarkus-run.jar` или ошибка об отсутствующем `lib`. Вы загрузили загрузчик без остального каталога quarkus-app. Загрузите и распакуйте весь каталог или переключитесь на uber-jar.
`no main manifest attribute, in app.jar`. Вы загрузили тонкий jar. Для Micronaut это обычный jar вместо -all.jar; для Quarkus это не тот файл в target/. Найдите запускаемый.
`UnsupportedClassVersionError`. Jar скомпилирован под более новую Java, чем на сервере. Компилируйте с --release 17 или выберите для сервера Java 21 или 25.
Сервер перезапускается без трассировки стека. Процесс превысил лимит контейнера. Уменьшите MaxRAMPercentage, ограничьте прямую память через -XX:MaxDirectMemorySize и сравните график памяти с лимитом. Помните, что три перезапуска за час вызывают предупреждение.
`java.lang.OutOfMemoryError: Java heap space`. Heap слишком мал, либо что-то накапливается. Увеличьте его, если на тарифе есть запас, или снимите дамп с помощью -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/container/dump.hprof и затем удалите файл, так как он размером с heap.
`java.lang.OutOfMemoryError: Direct buffer memory`. У Netty кончилась внекучная память, обычно при потоке больших запросов или ответов. Увеличьте -XX:MaxDirectMemorySize, если на тарифе есть запас, и поищите код, который удерживает буферы.
Порт уже занят или bad gateway при работающем приложении. Приложение слушает 8080, а не выделенный порт, либо слушает loopback. Задайте порт из окружения и quarkus.http.host=0.0.0.0.
`/q/health` возвращает 404. Расширения smallrye-health нет в сборке, либо quarkus.http.non-application-root-path перенёс путь.
`/health` возвращает 404 в Micronaut. micronaut-management нет в classpath, либо endpoints.health.enabled равно false.
Native-образ работает локально, но не на сервере. Он собран под другую операционную систему или архитектуру CPU. Соберите его в CI в Linux-контейнере или вернитесь к режиму JVM.
FAQ#
Fast-jar лучше, чем uber-jar?
Для старта и инкрементальных развёртываний - да. Для пути развёртывания, которому нужен один файл, - нет. Fast-jar по умолчанию не зря, но uber-jar на небольшом сервисе не ошибка: задайте quarkus.package.jar.type=uber-jar и загрузите один файл.
Нужен ли native-образ на небольшом тарифе?
Обычно нет. Для сборки нужны несколько гигабайт памяти и минуты работы CPU, в документации Java-линейка описана как запускающая jar, а режим JVM с урезанным heap уже помещается в самый дешёвый тариф. Native окупается при масштабировании до нуля и при очень многих небольших экземплярах, но не для одного сервиса.
Сколько памяти нужно сервису на Quarkus или Micronaut?
Небольшой REST-сервис с базой данных обычно работает с heap в 128-256 МБ и процессом заметно меньше 1 ГБ, так что тариф на 1 ГБ вполне годится. Измеряйте по графику памяти в консоли за сутки реального трафика, а не доверяйте никаким эмпирическим правилам, включая это.
Что нужно: -Xmx или MaxRAMPercentage?
Используйте MaxRAMPercentage, если можете сменить тариф, потому что он следует за лимитом. Используйте -Xmx для фиксированного, измеренного значения. Если заданы оба, побеждает -Xmx.
Почему мой health-эндпоинт отвечает на одном порту, а на другом нет?
Management-интерфейс может быть на собственном порту. В Micronaut это делает endpoints.all.port, а Quarkus при соответствующей настройке умеет отдавать management на отдельном интерфейсе. У тарифа есть те порты, что в нём перечислены, поэтому прежде чем что-то переносить, проверьте вкладку Network.
Можно ли так же запускать Ktor или Spring Boot?
Да. Любой jar работает на Java-линейке, а флаги heap одинаковы. Особенности Spring Boot описаны в руководстве по развёртыванию Spring Boot.


