Оптимизируем микросервисы на Java™ с помощью самого маленького в мире Docker-контейнера

Оптимизируем микросервисы на Java™ с помощью самого маленького в мире Docker-контейнера


Март 26, 2021


Компания БЕЛЛСОФТ вносит последние изменения для интеграции Alpine Linux как базового образа контейнера OpenJDK

Предложение по улучшению JDK JEP 386 нацелено на интеграцию проекта Portola в апстрим OpenJDK. В результате можно будет собирать версию JDK на Alpine Linux/x64 и других дистрибутивах Linux, которые используют musl в качестве стандартной библиотеки libc, без слоя переносимости приложений glibc. Наша команда технической разработки выполнила слияние репозитория Portola с основной веткой JDK, скорректировала ряд решений, предложенных предыдущими контрибьюторами, и провела тщательное тестирование созданного патча.

Alpine — это дистрибутив Linux, построенный на базе библиотеки musl и набора утилит BusyBox. Главное преимущество этой системы — её легковесность.

Alpine — идеальный дистрибутив для контейнеров

В 2016 году образы Alpine Linux стали основными для создания контейнеров на Docker Hub, вытеснив Ubuntu. Тогда же система была признана рядом источников «новой ОС по умолчанию» и «предпочтительным дистрибутивом»1. Благодаря небольшому размеру образа Alpine Linux широко применяется в облачных развёртываниях, микросервисной архитектуре и контейнерных средах. Для наглядности: образ Alpine musl Liberica JDK (с 2022 года - Axiom JDK) как минимум на 200 Мб меньше других доступных дистрибутивов и занимает всего 43,5 Мб на диске (в сравнении с Debian — 228 Мб или CentOS — 319 Мб). А благодаря миниатюрному базовому Docker-образу весом до 6 Мб и встроенному процессу сборки можно значительно сэкономить ценные облачные ресурсы.

Ещё один плюс: базовые контейнеры Alpine Linux по умолчанию не включают в себя подсистему управления сервисами. В отличие от CentOS, где имеется целый ряд всегда активных сервисов, такой решение освобождает оперативную память, упрощает диагностику и настройку системы, а также уменьшает время запуска. Наконец, образы Alpine потенциально безопаснее — чем меньше компонентов в дистрибутиве, тем меньше поверхность атаки.

Контейнеры повышают бизнес-ценность: чем меньше, тем лучше

Почему разработчики стремятся максимально уменьшить размер образов?

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

Infographic

Во-первых, исправление ошибок и внедрение изменений проходит оперативно и без перебоев. Возможно, вы развёртываете ряд независимых копий приложения одновременно, например, если оно динамически масштабируется. Основная проблема, которая может здесь возникнуть, — большие расходы на трафик, которые сильно сократятся благодаря маленькому размеру образа. Кроме того, чем меньше вы загружаете в облако, тем быстрее ваш проект начнёт работать на организацию.

Во-вторых, решается извечная проблема с ограниченным пространством для хранения данных. Размер контейнеров в изолированной среде исчисляется десятками мегабайт. Для сравнения: виртуальный том с полнофункциональной ОС может занимать несколько гигабайт. А теперь представьте, сколько вы сэкономите, если компании не придётся докупать пространство на облачных сервисах. Эти средства можно с успехом вложить в увеличение общей производительности.

В-третьих, как правило, процесс деплоя отнимает у программистов массу времени. Контейнеры же развёртываются почти мгновенно — выгода от такого решения очевидна.

Возьмём гипотетическую организацию, которая только что перевела свой проект на контейнеры. Накладные расходы сокращаются, не нужно настраивать гипервизор и запускать виртуальные машины… Идеально. Всё, что осталось, — развернуть новенькие контейнеры на чистом целевом устройстве. Так почему бы не сэкономить еще больше, выбрав меньший по размеру образ операционной системы? Так мы возвращаемся к идее контейнеров с JDK на базе легковесного Alpine Linux.

Прежде чем раскрыть детали портирования JDK на Alpine Linux, напомним, что сборки JDK от БЕЛЛСОФТ не только самые маленькие в отрасли, но и совместимы с большинством известных системных конфигураций. Заполните форму по кнопке ниже и свяжитесь с одним из наших инженеров-разработчиков, чтобы узнать все преимущества перехода на оптимизированные контейнеры Java™ с Liberica JDK.

JEP 386: предпосылки, спрос и концепция

Микаэль Видстедт (Mikael Vidstedt), действующий директор подразделения JVM Software Engineering компании Oracle, был первым, кто предложил поддержку альтернативных реализаций стандартных библиотек libc для OpenJDK. Очевидная проблема заключалась в сложности реализации, например, невозможности запуска виртуальной машины Java на OpenWrt маршрутизаторах. В феврале 2017 года Видстедт решил изменить положение дел и занялся разработкой проекта Portola. Он мог бы стать тем самым человеком, кто внёс бы порт в основную ветку JDK. Однако его команда не провела полноценного анализа изменений, чтобы определить их назначение и выявить другие возможные решения. Не был выполнен и достаточный объём тестов на Alpine Linux, а затем и на всех доступных платформах, в том числе Linux, macOS X, Windows, ARM и AArch64 — ведь реализация патча повлияла бы на общий код JDK.

Мы, специалисты БЕЛЛСОФТ, приступили к работе над JEP 386, когда ощутили рост пользовательского спроса на контейнеры с JDK 9. Liberica JDK всегда поддерживала множество платформ. Для удобства мы предусмотрели несколько каналов доставки дистрибутивов. Например, можно скачать с сайта бинарную сборку, добавить репозиторий в Linux, воспользоваться файлом установщика, найти сборки в открытом доступе с помощью менеджеров пакетов (например, SDKMAN) или выбрать готовый образ в контейнере.

В БЕЛЛСОФТ видели потенциал поддержки таких сборок в коммерческой среде. Так мы создали контейнеры Alpine Linux со слоем совместимости glibc. Впервые Docker-образы на базе Alpine появились в версии Axiom 9.0.1 с загрузочным ISO-образом armhf (для Raspberry Pi). О проекте Portola тогда ещё не шло и речи — он возник во времена JDK 10.

Пара слов о проекте: его суть в прямой связи OpenJDK с musl. Если в системе на базе musl отсутствует слой совместимости, программы, связанные с glibc, не запустятся (включая обычные сборки JDK). Сборки с альтернативной стандартной библиотекой выступают в качестве своеобразного инструмента портирования. Необходимо учитывать и другие особенности целевых систем, поскольку проведенные тесты охватывают лишь некоторые аспекты.

Шло время, наши контейнеры с OpenJDK быстро обрели популярность. Согласно статистике Axiom OpenJDK на сайте Docker Hub, образы на базе Alpine скачаны более 100 тысяч раз, при этом решение обладает пользовательским рейтингом 10 звезд. Популярность обусловлена легковесностью Alpine и тем, что обычные образы JVM не работают под Alpine Linux.

Успех контейнеров Axiom со_ _слоем совместимости glibc подал нам идею стать непосредственными участниками проекта Portola и создать сборку JDK исключительно под Alpine. Используемые в комплекте процедуры должны связываться со стандартной библиотекой без необходимости эмулировать glibc.

Предполагалось, что отсутствие слоя glibc уменьшит общий размер контейнера в среднем на 24 Мб. Для нашей компании, которая стремится свести расходы своих клиентов к минимуму, это было достаточным основанием, чтобы поддержать проект Portola. На сегодняшний день мы выпустили образы Axiom-openjdk-alpine-musl всех версий, начиная с JDK 8.

Вклад БЕЛЛСОФТ в порт Portola

Возникают закономерные вопросы: почему JDK не работал с библиотекой musl libc? И какие изменения мы внесли в свои контейнеры под Alpine Linux?

Сперва наша команда подготовила слияние основной ветки JDK с репозиторием Portola. Ввиду того, что изменения в неё вносятся постоянно, прежде чем утвердить и запустить проект, необходимо было обеспечить непрерывную синхронизацию. Патч был разбит на категории; каждая из них подверглась индивидуальной проверке и получила кодовое название в соответствии с вносимыми изменениями. Некоторые улучшения оказались незначительны, например, определение libc в файле конфигурации:

До: `ldd_version=`ldd --version 2>&1 | head -1 | cut -f1 -d' '``

После: `libc_vendor=`ldd --version 2>&1 | sed -n '1s/.*\(musl\).*/\1/p'``

Примечание: благодаря этому улучшению можно также выполнять кроссплатформенную компиляцию образов OpenJDK Linux-musl на традиционных системах на базе Linux x64 с использованием дистрибутивов GNU/Linux.

    Portola cross-compilation
    clone hg.openjdk.java.net/jdk/jdk repository

    apply Portola patch

    build jdk for the current platform which will be used as boot and build jdk for cross-compilation

    # build boot and build jdk
    mkdir -p build/linux-x86_64-boot-release
    cd build/linux-x86_64-boot-release
    sh ../../configure --with-boot-jdk=<path-to-jdk>/jdk-14.0.1
    make jdk-image
    download cross musl-based toolchain from https://musl.cc

    copy sysroot from target system (for example from Alpine Lunux docker image)

    # configure musl build with devkit and sysroot
    export DEVKIT=<path-to-devkit>/x86_64-linux-musl-cross
    export SYSROOT=<path-to-sysroot>/alpine3.8-sysroot
    export TARGET=x86_64-linux-musl
    export BOOT_JDK=build/linux-x86_64-boot-release/images/jdk
    sh ./configure --with-jvm-variants=server \
    --with-boot-jdk=$BOOT_JDK \
    --with-build-jdk=$BOOT_JDK \
    --openjdk-target=x86_64-unknown-linux-musl \
    --with-devkit=${DEVKIT} \
    --with-sysroot=${SYSROOT}

    # build jdk image
    cd build/linux-x86_64-server-release
    make jdk-image

Или вместо static int sigWakeup = (__SIGRTMAX — 2) использовать #define INTERRUPT_SIGNAL (SIGRTMAX — 2), где SIGRTMAX — функция в musl libc.

Затем идут проблемы, которые толкнули нас на поиск альтернативных решений. Например, в библиотеке musl не реализуется расширение dlvsym, поскольку оно не является частью POSIX. Исходный проект Portola инициирует динамическую загрузку dlvsym и в случае её отсутствия включает режим функционирования, при наличии неисправности. Мы решили заменить его «заглушкой» dlvsym и переписать код следующим образом:


    --- --- ---
    #ifdef MUSL_LIBCstatic void *dlvsym(void *handle, const char *symbol,
    const char *version) {
     return NULL;
    }
    #endif
    --- --- ---

Пока не все решения такого рода вошли в финальную сборку. В частности, функция execvp в библиотеке musl libc. Она некорректно исполняет shell-файлы (отсутствует корректная строка shebang). На самом деле, эта проблема известна довольно давно2.

Наконец, наша команда выбрала части кода для изолированного анализа и корректировки независимо от патча Portola. Работа над упаковщиком jpackage стала одной из основных и самой трудоёмкой задачей. Её можно даже выделить в своеобразный подпроект. Суть ошибки в том, что при определенных обстоятельствах лаунчер Java запускал приложение повторно. Для musl libc это тупиковая ситуация, ведь при загрузке совместно используемых библиотек она не проверяет их по короткому имени. Как выяснилось, проблема с повторным запуском упаковщика jpackage проявляется не только при использовании Alpine Linux; она воспроизводится на всех Linux-системах, которые изменяют LD_LIBRARY_PATH3.

Ещё один масштабный блок работы — это тестирование. Первой задачей было проведение тестов JCK, jtreg, vmTestbase и jvmTestbase. Ситуация была непростой: нам постоянно приходилось тестировать основной репозиторий в режиме интенсивной разработки. Даже без внедрения патча Portola возникали сбои. Было решено провести тестирование основной ветки JDK и использовать результаты успешных тестов в качестве ориентира для сборки Portola, настроенной на быструю отладку (fastdebug). В итоге одни исправления удалось внести в патч до слияния, другие — уже в процессе. Отдельные ошибки были оформлены в отчёте как баги непосредственно musl libc, например, как в случае с execvp выше.

Резюмируя, при портировании JDK на дистрибутивы Linux, основанные на musl, разработчикам гораздо проще обновлять базовые слои и хранить их в частном репозитории. Другие преимущества — сокращение времени развёртывания на новом узле и общего времени передислокации, экономия пространства на диске. Эти факторы играют важную роль в упрощении бизнес-процессов и снижении затрат (особенно актуально, если вы платите за использование ресурсов ЦП и хранилище в облаке).

С помощью отладчика jlink опытный пользователь сократит размер JRE и сделает ещё более компактный образ для запуска приложения. Однако из-за дополнительного шага в процедуре развёртывания или динамических зависимостей процесс может показаться слишком сложным. Поэтому наши Docker-образы alpine-musl доступны для сборки контейнеров с дополнительными опциями. Вы можете взять среду исполнения, которая содержит только java.base (для запуска простейших приложений), или лишь некоторые стандартные модули для веб-приложений, или же все модули сразу. Подобная подготовка образов намного проще полноценной работы с jlink. При этом вам необязательно пересчитывать фактические зависимости, достаточно просто выбрать одну из трёх легковесных сборок JDK: базовую Base (43,5 Мб), облегченную Lite (111 Мб) или полную Full (192 Мб). Все размеры представлены для версии JDK 14.0.1.

JEP 386 также обеспечит поддержку сред исполнения Java™ не только на Alpine, но и на других Linux-системах с библиотекой musl. В этом направлении необходимы дополнительные тесты, однако мы уже можем с уверенностью утверждать, что порт работает на OpenWrt — популярном дистрибутиве Linux, который также основан на musl и часто служит для прошивки маршрутизаторов.

БЕЛЛСОФТ гарантирует качество своих образов Alpine. Мы заверяем, что каждая сборка проходит верификацию на соответствие стандарту Java SE сюитой TCK. Сочетание технологий не всегда проходит успешно: в частности, в более старых версиях Alpine Linux JVM отключался прямо при запуске из-за экспериментальных патчей безопасности. Но в контейнерах Liberica JDK исключены такие проблемы благодаря многостороннему тестированию и тщательным проверкам. Наша команда инженеров уделяет этому патчу большое внимание, обеспечивает поддержку сборок, оперативно решает выявленные проблемы и планирует дальнейшее развитие OpenJDK в контексте musl.

Ожидания и перспективы

Мы уверены, что сборки на базе Portola несут в себе множество преимуществ для Java-разработки и обеспечат стабильность работы. Помня о стабильно высоком спросе на JDK 8, ещё летом 2020 года мы предоставили поддержку Linux-musl для самой популярной версии («восьмёрка» до сих пор признаётся лучшим релизом), ведь именно JDK 8u содержит множество бэкпортированных функций, связанных с контейнеризацией4.

Каковы дальнейшие планы БЕЛЛСОФТ в этой области? Наша команда активно участвует в адаптации musl libc для GraalVM в рамках проекта Graal Open Source. Библиотека musl, а в частности те проблемы, которые она ставит перед разработчиками сред исполнения Java™, остаются для нас долгосрочным приоритетом. Если раньше большинство ресурсов уходило на корректировку версий Liberica JDK, теперь же мы фокусируемся на новых технических возможностях. Сообщество JDK больше не рассматривает виртуальные машины как популярное решение. Основным направлением работы и методом применения Java™ стали контейнеры. БЕЛЛСОФТ обещает и в дальнейшем работать в этом направлении, чтобы максимально облегчить труд разработчиков.

А поскольку мы всегда выполняем свои обещания, то ещё в версиях Liberica JDK 8u262, 11.0.8 и 14.0.2 реализовали долгожданную поддержку библиотеки musl libc Alpine Linux, а также множество других полезных функций. Благодаря обширным знаниям об ОС Alpine Linux, наши разработчики создали самый маленький Docker-контейнер среди представленных контрибьюторами OpenJDK.

Наши клиенты уже несколько лет используют контейнеры Liberica JDK с библиотекой Alpine musl. Многие отмечают существенное снижение затрат на передачу и хранение данных и даже сокращение времени проектирования. Планируете перевести свой проект в облачную среду? Хотите достичь максимальной производительности? Стремитесь сэкономить время и средства? Свяжитесь с БЕЛЛСОФТ прямо сейчас!

Ссылки:

  1. Review: Alpine Linux is made for Docker
  2. https://www.openwall.com/lists/musl/2020/02/12/9
  3. https://mail.openjdk.java.net/pipermail/core-libs-dev/2020-June/067456.html
  4. GeeCON 2019: Dmitry Chuyko - Do not put all eggs in one container
Author image

Олег Чирухин

Директор по коммуникациям с разработчиками (DevRel)

Команда Axiom JDK roman.karpov@axiomjdk.ru Команда Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Команда Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67