sbt-assembly vs sbt-native-image: Выбор для DiceChess ботов

Цель: Объяснить, почему в некоторых репозиториях DiceChess используется sbt-assembly, а в других — sbt-native-image, и помочь выбрать правильный подход для новых проектов.


📌 Краткое резюме

ПлатформаРекомендуемый плагинТип артефактаПричина
Google Cloud Runsbt-assemblyFat JARПростая интеграция, полная совместимость с JVM, автоматическая сборка образа Cloud Run.
Azure Functionssbt-native-imageNative binaryТребуется для кастомных хендлеров (custom handlers).
Cloudflare WorkersИспользуется WebAssembly или JavaScript (не применимо для JVM-ботов).
Локальный запускsbt-assemblyFat JARПростота запуска (java -jar), отладка, совместимость.

🔍 Подробное сравнение

1. sbt-assembly (Fat JAR)

Что это?

Плагин собирает один JAR-файл, который включает:

  • Ваш скомпилированный код.
  • Все зависимости (включая transitive dependencies).
  • Манифест с указанием главного класса (Main-Class).

Плюсы

Простота развёртывания:

  • Один файл для запуска: java -jar my-bot.jar.
  • Cloud Run автоматически собирает Docker-образ из JAR.

Полная совместимость:

  • Работает на любой JVM (OpenJDK, GraalVM, и т.д.).
  • Нет проблем с библиотеками, использующими рефлексию или динамическую загрузку классов.

Быстрая сборка:

  • Не требует компиляции в native код (как в GraalVM Native Image).

Легкая отладка:

  • Можно запускать локально с отладочными флагами JVM (-Xdebug, -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005).

Поддержка всех библиотек:

  • Нет ограничений на используемые зависимости.

Минусы

Больший размер:

  • Fat JAR может весить 10-50 МБ (в зависимости от зависимостей).

Дольше запуск (JVM startup):

  • Требуется время на запуск JVM и загрузку классов.

Больше потребление памяти:

  • JVM + heap может занимать больше памяти, чем native binary.

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

// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.4.1")
 
// build.sbt
lazy val root = (project in file("."))
  .settings(
    // Настройки для sbt-assembly
    assemblyMergeStrategy in assembly := {
      case PathList("META-INF", xs @ _*) => MergeStrategy.discard
      case x => MergeStrategy.first
    }
  )

Запуск:

sbt assembly
java -jar target/scala-3.8.4/my-bot-assembly.jar

2. sbt-native-image (GraalVM Native Image)

Что это?

Плагин собирает native binary (исполняемый файл) с помощью GraalVM Native Image. Результат:

  • Один бинарный файл (например, my-bot).
  • Не требует JVM для запуска.

Плюсы

Мгновенный запуск:

  • Нет задержки на запуск JVM (cold start быстрее).

Меньше потребление памяти:

  • Native код потребляет меньше RAM, чем JVM.

Меньший размер артефакта:

  • Бинарник может быть меньше, чем Fat JAR (если оптимизирован).

Простота развёртывания (для некоторых платформ):

  • Один бинарник для запуска (например, в Docker-контейнере).

Минусы

Дольше сборка:

  • Компиляция в native код занимает больше времени и ресурсов.

Ограниченная совместимость:

  • Не все библиотеки совместимы с GraalVM (проблемы с рефлексией, динамической загрузкой классов, JNI).
  • Может потребоваться настройка reflect-config.json или resource-config.json.

Сложнее отладка:

  • Отладка native binary сложнее, чем JAR.

Требует GraalVM:

  • Нужно устанавливать GraalVM отдельно (не работает с обычной JDK).

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

// project/plugins.sbt
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.5.0")
 
// build.sbt
lazy val root = (project in file("."))
  .enablePlugins(NativeImagePlugin)
  .settings(
    nativeImageInstalled := true,
    nativeImageOptions ++= List("--no-fallback", "--install-exit-handlers"),
    nativeImageOutput := target.value / "native-image" / "my-bot"
  )

Запуск:

sbt nativeImage
./target/native-image/my-bot

🎯 Почему в DiceChess используются разные подходы?

1. dicechess-bot-gcp и dicechess-bot-gcp-onnxsbt-assembly

  • Платформа: Google Cloud Run.
  • Причина:
    • Cloud Run оптимизирован для работы с JAR-файлами.
    • Автоматически собирает Docker-образ из JAR (не нужно настраивать Dockerfile).
    • Проще развёртывать и отлаживать.
    • Нет проблем с совместимостью библиотек.
    • Cold start в Cloud Run не так критичен (боты не требуют мгновенного отклика).

2. dicechess-bot-scala и dicechess-bot-azuresbt-native-image

  • Платформа: Azure Functions.
  • Причина:
    • Azure Functions требует native binary для кастомных хендлеров (custom handlers).
    • JAR не поддерживается для кастомных хендлеров (только для HTTP-триггеров с ограничениями).
    • Native binary проще интегрировать с Azure Functions.

📊 Сравнительная таблица

Критерийsbt-assembly (Fat JAR)sbt-native-image (Native Binary)
Тип артефактаJARNative binary
Запускjava -jar./binary
Требует JVM✅ Да❌ Нет
Время запускаСреднее (JVM startup)Мгновенный
Размер артефактаБольшой (10-50 МБ)Средний (5-30 МБ)
Время сборкиБыстроеМедленное
Совместимость библиотек✅ Полная⚠️ Ограниченная
Отладка✅ Легкая❌ Сложная
Требует GraalVM❌ Нет✅ Да
Cloud Run✅ Оптимально⚠️ Требует Docker
Azure Functions❌ Не поддерживается✅ Оптимально

🔧 Рекомендации для новых проектов

Используйте sbt-assembly, если:

  1. Целевая платформа — Google Cloud Run.
  2. Нужна полная совместимость с JVM (например, если используются библиотеки с рефлексией).
  3. Важна простота развёртывания и отладки.
  4. Cold start не критичен (например, для ботов, которые не требуют мгновенного отклика).

Используйте sbt-native-image, если:

  1. Целевая платформа — Azure Functions (кастомные хендлеры).
  2. Cold start критичен (например, для HTTP-сервисов с высокой нагрузкой).
  3. Нужно минимизировать использование памяти.
  4. Проект использует мало зависимостей (чтобы избежать проблем с совместимостью GraalVM).

Альтернативы

  • Docker + Fat JAR: Если нужно развёртывать в Kubernetes или других платформах, можно использовать Docker с Fat JAR.
  • Docker + Native Image: Если нужно использовать Native Image в Cloud Run или Kubernetes, можно собирать Docker-контейнер с бинарником.

🛠️ Примеры настройки для разных платформ

1. Google Cloud Run + sbt-assembly

// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.4.1")
 
// build.sbt
lazy val root = (project in file("."))
  .settings(
    name := "my-bot",
    Compile / mainClass := Some("com.example.Main"),
    assemblyMergeStrategy in assembly := {
      case PathList("META-INF", xs @ _*) => MergeStrategy.discard
      case x => MergeStrategy.first
    }
  )

Dockerfile (опционально, если нужно кастомизировать образ):

FROM eclipse-temurin:21-jre
COPY target/scala-3.8.4/my-bot-assembly.jar /app/my-bot.jar
CMD ["java", "-jar", "/app/my-bot.jar"]

Развёртывание в Cloud Run:

gcloud run deploy my-bot --source . --region us-central1

2. Azure Functions + sbt-native-image

// project/plugins.sbt
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.5.0")
 
// build.sbt
lazy val root = (project in file("."))
  .enablePlugins(NativeImagePlugin)
  .settings(
    name := "my-bot",
    Compile / mainClass := Some("com.example.Main"),
    nativeImageInstalled := true,
    nativeImageOptions ++= List("--no-fallback", "--install-exit-handlers"),
    nativeImageOutput := target.value / "native-image" / "my-bot"
  )

host.json (для Azure Functions):

{
  "version": "2.0",
  "customHandler": {
    "description": {
      "defaultExecutablePath": "my-bot"
    },
    "enableForwardingHttpRequest": true
  }
}

Развёртывание в Azure Functions:

sbt nativeImage
func azure functionapp publish my-bot-function

📚 Полезные ресурсы


✅ Чеклист выбора

  • Определите целевую платформу (Cloud Run, Azure Functions, и т.д.).
  • Проверьте, поддерживает ли платформа JAR или требует native binary.
  • Оцените важность cold start для вашего проекта.
  • Проверьте совместимость зависимостей с GraalVM (если выбираете sbt-native-image).
  • Выберите плагин на основе анализа выше.

💡 Совет: Если не уверены, начните с sbt-assembly. Это более универсальный и простой вариант, который работает везде, где поддерживается JVM.