Будущее наступает. Нам уже очень сложно вообразить, как люди жили без постоянного доступа в интернет, как встречались, не имея телефонов, как ходили в фотосалоны, а потом хранили редкие фото всю жизнь, да даже как каждый день ходить на работу в офис - многие уже забыли. Думаю, очень скоро в прошлое отойдет и разворачивание приложений на физических серверах. Все уйдут в облака.

По дороге с облаками. И Java
По дороге с облаками. И Java

Беда в том, что наша любимая Java не то чтобы сильно приспособлена для размещения в облаке. Благодаря политике «пишем однажды – работает везде» у нас есть сложный многоступенчатый процесс подготовки сервиса, занимающий много памяти и процессорного времени, за что приходится очень серьезно платить облачному сервису.

Благо, как я уже писал, будущее наступило, и у нас есть некоторые технологии, делающие жизнь сервиса в облаке дешевле и приятнее. Сегодня рассмотрим две из них: технологию Native Image с GraalVM и технологию CraC.

Меня зовут Султанов, и я тимлид (тяжелый вздох). Стараюсь делать приложения быстрыми. Иногда даже получается. А еще у меня есть канал, где можно обсудить эту и другие статьи. Подписывайтесь, там интересно.

Зачем всё это изобрели

Можно ли разместить в облаке обычное Java-приложение на какой-то стоковой JDK? Конечно. Для этого нужно всего лишь вложить размещаемый сервис в Docker-контейнер, завести под Kubernetes, и все прекрасно заработает. Ну то есть как прекрасно…

Этапы прекрасной работы стоковой JVM
Этапы прекрасной работы стоковой JVM

Стоковая JDK любит JIT-компиляцию, а значит тратит много CPU (за который нужно платить облачному провайдеру), к тому же хранит эвристики и статистику в памяти, скомпилированный код в памяти, и начинает компиляцию не сразу, а только после сеансов интерпретации, и только когда накопит достаточно статистики. Этапов JIT-компиляции несколько, classloader тоже тащит всё в память, и за этим всем нужно еще и прибраться GarbageCollector’у.

Вобщем, не так уж всё и прекрасно. Если нам нужно развернуть один экземпляр низконагруженного сервиса – вроде ничего страшного и особо дорогого, но если нам необходимо постоянно разворачивать и тушить сервисы, балансируя нагрузку – уже дороговато. Если же наш сервис должен сразу давать быстрый отклик, и это критично для бизнеса, то нас ждет полный провал.

Решений на самом деле было изобретено довольно много, сегодня мы посмотрим на два из них. И первым выступит

GraalVM с технологией Native Image

Согласно информации на официальном сайте, технология мало того что имеет высочайшую производительность, так еще и полиглот (то есть может работать с несколькими языками). Попробуем проникнуть в суть применяемого решения.

Как это работает

Основной фишкой технологии Native Image является АОТ - Ahead of Time compilation. Означает это примерно следующее – вместо долгого процесса сборки приложения на дефолтной JDK GraalVM формирует тот самый Native Image, то есть образ приложения, уже готовый для контейнеризации.

В отличие от режима JIT, в котором компиляция и выполнение происходят одновременно, в режиме AOT компилятор выполняет все операции во время сборки, перед выполнением. Основная идея состоит в том, чтобы перенести всю «тяжелую работу» — дорогостоящие вычисления — на этап создания образа, чтобы это можно было сделать один раз, а затем во время выполнения сгенерированные исполняемые файлы запускаются быстро и готовы с самого начала, потому что все заранее вычисляется и предварительно скомпилировано.

Работа GraalVM
Работа GraalVM

Утилита GraalVM «native-image» принимает байт-код Java в качестве входных данных и выводит собственный исполняемый файл. Это происходит в три этапа:

  • Анализ точек входа (обычно это main метод). GraalVM Native Image определяет, какие классы, методы и поля Java достижимы во время выполнения, и только они будут включены в собственный исполняемый файл. Анализ итеративно обрабатывает все транзитивно достижимые пути кода до тех пор, пока не будет достигнута фиксированная точка и анализ не закончится. Это касается не только кода приложения, но также библиотек и классов JDK — всего, что необходимо для упаковки приложения в самостоятельный двоичный файл.

  • Инициализация во время сборки. GraalVM Native Image по умолчанию инициализирует класс во время выполнения, чтобы обеспечить корректное поведение. Но если класс не связан внешними зависимостями и не обращается к внешним ресурсам, он будет инициализирован во время сборки. Это делает ненужной инициализацию и проверки во время выполнения и повышает производительность.

  • Создание моментальных слепков heap. Все, что инициализировано, сохраняется в слепок heap, и потом просто грузится в память в момент старта приложения, делая ненужным разогрев.

Перечисленные меры по оптимизации старта приложения действительно приводят к хорошим результатам. Приложение стартует на порядок быстрее.

НО!

Есть в этом рациональное зерНО
Есть в этом рациональное зерНО

Чудес не бывает, и любая технология, имеющая прекрасные достоинства, несет с собой отражение этих достоинств – мерзкие недостатки. И у GraalVM они тоже есть.

Представленная технология полностью меняет подход к разработке приложений. Раньше было как? Написал, отладил, и забыл. Всю дальнейшую работу можно было доверить долгому, но очень стабильному и многократно опробованному процессу компиляции, иногда переставляя флаги в JVM.

Теперь же нужно заранее думать, а что будет при создании образа? Как поведет себя приложение при АОТ? Может ли умная машина вырезать из образа что-то нужное?

Эту проблему вобщем-то признают и сами авторы технологии Native image. Например, чтобы в вашем приложении заработала рефлексия, нужно поддерживать создаваемый вручную (что неприятно) или автоматически (что опасно) конфигурационный файл, разрешающий рефлексию некоторым частям программы. На самом деле выглядит, как костыль.

Так же GraalVM использует не самый продвинутый сборщик мусора (G1), имеет слабо отлаживаемый код, IDE не подсвечивает ошибки, связанные с Native Image, технология Linux-специфична. Со временем это конечно исправят, но мы-то живем и разрабатываем приложения в настоящий момент. Сейчас совершенно непонятно, с какими сложностями придется столкнуться на проде, используя Native image. Но, как говорится, кто не рискует…

CRaC – Coordinated Restore at Checkpoint

Одним из конкурентов Native image в деле ускорения и удешевления старта приложений в облаке является технология CRaC. Вобщем-то вся суть изложена в названии технологии. CRaC – это про создание образов приложения и восстановление из них.

Как это работает?

В ядре линукса (да-да, CRaC тоже Linux-специфичен) очень давно существует другая технология, а именно CRIU – Coordinated Restore in Userspace. Именно её использует внутри себя CRaC. CRUI позволяет заморозить любой процесс в виде набора некоторых файлов, и увидеть полное содержимое памяти, все состояния потоков, которые работали в этот момент, то есть попросту сохраняет слепок состояния процесса. И этот слепок (в виде набора файлов) можно где-то сохранить и потом запустить, через некоторое время или даже на другой физической машине, и он как будто бы ничего не заметит, и начнет работать так же, как он работал до этого, с того же места.

Собственно, CRaC это и есть адаптация технологии CRIU для Java. Вроде просто, и должно прекрасно работать. Как показывают замеры, время старта из образа на два порядка меньше времени традиционного старта приложения на стоковой JVM.

НО!

Как всегда, существуют проблемы. Наше приложение не работает само по себе, у него всегда есть связи, которые тоже сохраняются в слепке приложения. Чтобы подобного не происходило, нужна некая процедура, называемая координацией. На самом примитивном уровне это делается так:

Дополнительный код для использования CRaC
Дополнительный код для использования CRaC

То есть мы должны вручную обработать закрытие всех ресурсов перед созданием образа, а потом открытие этих ресурсов снова. Если этого не сделать, то система просто откажется создавать образ.

Следствием создания образа системы являются проблемы с безопасностью. Сам образ никому нельзя показывать, так как там вся подноготная. Компрометация образа будет катастрофой для безопасности.

Так же проблемой будет являться размер образа, который по сути будет аналогичен размеру разогретого JVM-приложения.

Заключение

На текущий момент обе технологии демонстрируют потенциал, но вместе с тем и проблемы, которые принято называть детскими. Разработчики их решают, но когда решат, и когда появятся достаточные гарантии, чтобы использовать Native image и CRaC на проде, и не поседеть, пока неясно.

Безумству храбрых поём мы песню!
Безумству храбрых поём мы песню!

Комментарии (3)


  1. SlavikF
    16.04.2024 14:27
    +2

    Вот вы говорите, что Java не заточен работать в облаках.

    А какой язык или среда разработке заточена под облака? Go?


    1. dyadyaSerezha
      16.04.2024 14:27

      Любой язык без JIT-а хотя бы библиотек (уже скомпилированы до машиных кодов), а лучше без JIT-а совсем. Но!

      Как часто требуется сервис, время старта и разогрева которого сопоставимо с временем его жизни? Типа старых CGI-программ, которые стартовали и работали ровно на один http-запрос. Как мне кажется, сейчас такое используется гораздо, гораздо реже. Кроме того, есть же всякие мелкие и шустрые JDK для крайних случаев.


  1. Batalmv
    16.04.2024 14:27
    +4

    Ну к примеру, начнем с

    Для этого нужно всего лишь вложить размещаемый сервис в Docker-контейнер, завести под Kubernetes, и все прекрасно заработает. 

    К примеру возьмем Azure AKS либо AWS EKS. Если не нравится, давайте ваш вариантю. Мы платим за (fixme):

    • кластер сам по себе поштучно за час

    • за виртуалки и их размер, также поштучно и за "квант" времени (лениво вспоминать, сикоко там)

    • за диски

    Теперь вопрос, на который у меня нет ответа

    Если нам нужно развернуть один экземпляр низконагруженного сервиса – вроде ничего страшного и особо дорогого, но если нам необходимо постоянно разворачивать и тушить сервисы, балансируя нагрузку – уже дороговато. Если же наш сервис должен сразу давать быстрый отклик, и это критично для бизнеса, то нас ждет полный провал.

    Зачем постоянно разворачивать/тушить сервисы, они же pods (если я вас правильно понял). Это не влияет на стоимость до тех пор пока вам не на добавить одну ноду, ну либо убрать. Запуск виртуалки же - это не секунды, поэтому это надо делать чуток заранее, ну либо прописать критерии чуток с запасом.

    Заниматься "тонким" тюнингом и балансировкой внутри кластера - а зачем? Не, ну можно ... просто это ничего, кроме "гемороя" и нагрузки на сам кластер не добавит. Т.е. понятно, autoscaling нужен, но прям такой супер реактивный - а зачем?

    Но возможно я туплю и не могу понять