Что такое docker java
Java и Docker: это должен знать каждый
Постановка проблемы
В демонстрационных целях я создал демон docker в виртуальной машине с 1 Гб ОЗУ, используя такую команду:
Похожий результат получается даже в кластере Kubernetes / OpenShift. Я запустил группу контейнеров Kubernetes с ограничением памяти, используя такую команду:
При этом кластеру было назначено 15 Гб памяти. В итоге общий объём памяти, о котором сообщила система, составил 14 Гб.
Исследование кластера с 15 Гб памяти
Для того, чтобы понять причины происходящего, советую прочесть этот материал об особенностях работы с памятью в контейнерах Linux.
Для того, чтобы воспроизвести ситуацию, в которой система останавливает процесс после превышения заданного лимита памяти, можно запустить WildFly Application Server в контейнере с ограничением памяти в 50 Мб, воспользовавшись такой командой:
Теперь, в процессе работы контейнера, можно выполнить команду docker stats для того, чтобы проверить ограничения.
Данные о контейнере
Через несколько секунд исполнение контейнера WildFly будет прервано, появится сообщение:
Выполним такую команду:
Анализ причины остановки контейнера
Влияние неверной работы с памятью на Java-приложения
Попробуем это сделать:
Конечная точка ответит примерно следующим образом:
Всё это может навести нас, по меньшей мере, на два вопроса:
Увеличение объёма памяти как пример неверного решения проблемы
Разработчики, не понимающие сути происходящего, склонны полагать, что вышеописанная проблема заключается в том, что окружение не даёт достаточно памяти для исполнения JVM. В результате частое решение этой проблемы заключается в увеличении объёма доступной памяти, но такой подход, на самом деле, только ухудшает ситуацию.
Предположим, мы предоставили демону не 1 Гб памяти, а 8 Гб. Для его создания подойдёт такая команда:
Следуя той же идее, ослабим ограничение контейнера, дав ему не 150, а 800 Мб памяти:
Обратите внимание на то, что команда curl http://`docker-machine ip docker8192`:8080/api/memory в таких условиях даже не сможет выполниться, так как вычисленный параметр MaxHeapSize для JVM в окружении с 8 Гб памяти будет равен 2092957696 байт (примерно 2 Гб). Проверить это можно такой командой:
Проверка параметра MaxHeapSize
Приложение попытается выделить более 1.6 Гб памяти, что больше, чем лимит контейнера (800 Мб RAM и столько же в swap-файле), в результате процесс будет остановлен.
Верное решение проблемы
Небольшое изменение в Dockerfile позволяет нам задавать переменную окружения, которая определяет дополнительные параметры для JVM. Взгляните на следующую строку:
Теперь можно использовать переменную окружения JAVA_OPTIONS для того, чтобы сообщать системе о размере кучи JVM. Этому приложению, похоже, хватит 300 Мб. Позже можно взглянуть в логи и найти там значение 314572800 байт (300 МиБ).
В Kubernetes переменную среды можно задать, воспользовавшись ключом –env=Что такое docker java :
Улучшаем верное решение проблемы
Что если размер кучи можно было бы рассчитать автоматически, основываясь на ограничениях контейнера?
Это вполне достижимо, если использовать базовый образ Docker, подготовленный сообществом Fabric8. Образ fabric8/java-jboss-openjdk8-jdk задействует скрипт, который выясняет ограничения контейнера и использует 50% доступной памяти как верхнюю границу. Обратите внимание на то, что вместо 50% можно использовать другое значение. Кроме того, этот образ позволяет включать и отключать отладку, диагностику, и многое другое. Взглянем на то, как выглядит Dockerfile для приложения Spring Boot:
Теперь всё будет работать так, как нужно. Независимо от ограничений памяти контейнера, наше Java-приложение всегда будет настраивать размер кучи в соответствии с параметрами контейнера, не основываясь на параметрах демона.
Использование разработок Fabric8
Итоги
JVM до сих пор не имеет средств, позволяющих определить, что она выполняется в контейнеризированной среде и учесть ограничения некоторых ресурсов, таких, как память и процессор. Поэтому нельзя позволять механизму JVM ergonomics самостоятельно задавать максимальный размер кучи.
Один из способов решения этой проблемы — использование образа Fabric8 Base, который позволяет системе, основываясь на параметрах контейнера, настраивать размер кучи автоматически. Этот параметр можно задать и самостоятельно, но автоматизированный подход удобнее.
В JDK9 включена экспериментальная поддержка JVM ограничений памяти cgroups в контейнерах (в Docker, например). Тут можно найти подробности.
Надо отметить, что здесь мы говорили о JVM и об особенностях использования памяти. Процессор — это отдельная тема, вполне возможно, мы ещё её обсудим.
Уважаемые читатели! Сталкивались ли вы с проблемами при работе с Java-приложениями в контейнерах Linux? Если сталкивались, расскажите пожалуйста о том, как вы с ними справлялись.
Docker. Зачем и как
Есть множество прекрасных публикаций для тех, кто уже пользуется docker-ом. Есть хорошие статьи для тех, кто хочет этому научиться. Я пишу для тех, кто не только не знает, что такое docker, но и не уверен стоит ли ему это знать.
Я сознательно опускаю некоторые технические подробности, а кое где допускаю упрощения. Если вы увидите, что docker – то, что вам нужно, вы легко найдете более полную и точную информацию в других статьях.
Начну я с описания нескольких типичных проблем.
Проблемы
Первая проблема — как передать продукт клиенту.
Предположим у вас есть серверный проект, который вы закончили и теперь его необходимо передать пользователю. Вы готовите много разных файликов, скриптов и пишите инструкцию по установке. А потом тратите уйму времени на решения проблем клиента вроде: «у меня ничего не работает», «ваш скрипт упал на середине — что теперь делать», «я перепутал порядок шагов в инструкции и теперь не могу идти дальше» и т. п.
Всё усугубляется если продукт тиражируемый и вместо одного клиента у вас сотни или тысячи покупателей. И становится еще сложнее, если вспомнить о необходимости установки новых версий продукта.
Вторая проблема — тиражируемость. Пусть вам нужно поднять 5 (или 50) почти одинаковых серверов. Делать это вручную долго, дорого и подвержено ошибкам.
Наконец, третья проблема — переиспользуемость. Предположим у вас есть отдел, который делает браузерные игры. Предположим, что их у вас уже несколько. И все они используют один и тот же технологический стэк (например — java-tomcat-nginx-postgre). Но при этом, чтобы поставить новую игру вы вынуждены заново подготавливать на новом сервере почти одинаковую конфигурацию. Вы не можете просто так взять и сказать — «хочу сервер, как в игре странники но только с другим веб архивом»
Обычные решения
Как обычно решаются эти проблемы.
Установочный скрипт
Первый подход я уже упомянул — вы можете написать скрипт, который установит всё, что вам нужно и запускать его на всех нужных серверах. ( Скрипт может быть как простым sh файлом, так и чем-то сложным, созданным с использованием специальных инструментов).
Недостатки этого подхода — хрупкость и неустойчивость к ошибкам. Как бы хорошо не был написан скрипт, рано или поздно на какой-то машине он упадёт. И после этого падения машина фактически окажется «испорченной» — просто так «откатить» те действия, которые скрипт успел выполнить, у вашего клиента не получится.
Облачные сервисы
Второй подход — использование облачных сервисов. Вы вручную устанавливаете на виртуальный сервер всё, что вам нужно. Затем делаете его image. И далее клонируете его столько раз, сколько вам надо.
Недостатка здесь два. Во-первых, vendor-lock-in. Вы не можете запускать свое решение вне выбранного облака, что не всегда удобно и может привести к потерям несогласных с этим выбором клиентов. Во-вторых, облака медленны. Виртуальные (и даже «bare-metal») сервера предоставляемые облаками на сегодняшний день сильно уступают по производительности dedicated серверам.
Виртуальные машины
Третий подход — использование виртуальных машин. Здесь тоже есть недостатки:
Размер — не всегда удобно качать образ виртуальной машины, который может быть довольно большим. При этом, любое изменение внутри образа виртуальной машины требует скачать весь образ заново.
Сложное управление совместным использованием серверных ресурсов — не все виртуальные машины вообще поддерживают совместное использование памяти или CPU. Те что поддерживают, требуют тонкой настройки.
Подход докера — контейнеризация
И вот тут появляется docker, в котором
Как работает docker
Создание образа
Сначала создается docker image (или образ). Он создается при помощи скрипта, который вы для этого пишете.
Образы наследуются и, обычно, для создания своего первого образа мы берём готовый образ и наследуемся от него.
Чаще всего мы берем образ в котором содержится та или иная версия linux. Скрипт тогда начинается как-то так:
Кроме этого, мы можем копировать в наш образ любые локальные файлы при помощи директивы COPY.
Докер поддерживает гораздо больше различных директив. Например, директива USER roman говорит докеру что все следующие директивы нужно выполнять из под пользователя roman. А директива ENTRYPOINT [“/opt/tomcat/catalina.sh”] задает исполняемый файл, который будет запускаться при старте.
Я не буду перечислять все остальные директивы — в этом нет смысла. Здесь главное — принцип: вы создаёте вот такой скрипт, называете его Dockerfile и запускаете команду docker build, docker выполняет скрипт и создает image.
Если в процессе возникают какие-то ошибки, докер о них сообщает и вы их исправляете. То есть исправление скрипта происходит на этапе создания image. На этапе установки скрипт уже не используется.
Создание контейнера
Когда у вас уже есть docker image вы можете создать из него контейнер на любом физическом сервере, где установлен докер. Если image – это тиражируемый образ некоторой «машины», то container это уже сама «машина», которую можно запускать и останавливать.
Важный момент — при создании контейнера из image, его можно параметризовать. Вы можете передавать докеру переменные окружения, которые он использует при создании контейнера из image. Так вы сможете создавать немного разные машины из одного образа. Например, передать образу web-сервера его доменное имя.
Хорошей практикой в докере считается «упаковка» в один контейнер ровно одного постоянно работающего серверного процесса. Как я уже упоминал, этот процесс работает на уровне физического сервера и честно регулируется установленной там операционной системой. Поэтому, в отличие от виртуальных машин, контейнеры докера не требуют специального управления памятью и процессорами. Использование ресурсов становится простым и эффективным.
Union filesystem
Ок — память и процессор используется эффективно. А как насчёт файловой системы? Ведь если у каждого контейнера докера своя собственная копия операционной системы, то мы получим ту же проблему, что и с виртуальными машинами — тяжеловесные образы, которые содержат одно и тоже.
На самом деле в докере это не так. Если вы используете 100500 контейнеров, основанных на одном и том же образе операционной системы, то файлы этой системы будут скачаны докером ровно один раз. Это достигается за счёт использования докером union file system.
Union file system состоит из слоёв (layers). Слои как бы наложены друг на друга. Некоторые слои защищены от записи. Например, все наши контейнеры используют общие защищенные от записи слои, в которых находятся неизменяемые файлы операционной системы.
Для изменяемых файлов каждый из контейнеров будет иметь собственный слой. Естественно, докер использует такой подход не только для операционной системы, но и для любых общих частей контейнеров, которые были созданы на основе общих «предков» их образов.
Container registry
Получается, что docker image состоит из слоёв. И хорошо было бы уметь скачивать на наш сервер только те слои, которых на нём пока нет. Иначе для установки 100 контейнеров, основанных на Ubuntu мы скачаем Ubuntu внутри их образов 100 раз. Зачем?
Хорошая новость в том, что докер решает эту проблему. Докер предоставляет специальный сервис, называемый docker registry. Docker registry предназначен для хранения и дистрибуции готовых образов. Собрав новый образ (или новую версию образа) вы можете закачать его в docker registry. Соответственно, потом его можно скачать оттуда на любой сервер. Главная фишка здесь в том, что физически качаться будут только те слои, которые нужны.
Например, если вы создали новую версию образа, в котором поменяли несколько файлов, то в registry будут отправлены только слои, содержащие эти файлы.
Аналогично, если сервер качает из registry какой-то образ, скачаны будут только слои, отсутствующие на сервере.
Docker registry существует и как общедоступный сервис и как open source проект, доступный для скачивания и установки на собственной инфрастуктуре.
Использование контейнеров
Созданные контейнеры можно запускать, останавливать, проверять их статус и т д. При создании контейнера можно дополнительно передать докеру некоторые параметры. Например, попросить докер автоматически рестартовать контейнер, если тот упадёт.
Взаимодействие между контейнерами
Если контейнеров на сервере несколько, управлять ими вручную становится проблематично. Для этого есть технология docker compose. Она существует поверх докера и просто позволяет управлять контейнерами на основе единого конфигурационного файла, в котором описаны контейнеры, их параметры и их взаимосвязи (например контейнер A имеет право соединяться с портом 5432 контейнера B)
Выводы
Таким образом докер очень хорошо подходит для решения перечисленных выше задач:
Docker — основы
В данной статье мы поговорим о таком инструменте как Docker. Сегодня мы не будем писать приложения, а попытаемся их запускать.
Если Вы пользовались виртуальной машиной — то поймете быстрее. Поскольку, Докер это приложение, которое позволяет отделить приложения от инфраструктуры. Например, есть у Вас Windows система и Вы хотите попробовать поработать с Ubuntu. Для этого совсем не обязательно удалять свою систему и устанавливать другую. Достаточно поднять виртуальную машину: приложение внутри которого будет полноценная операционная система. Вы сможете туда зайти и посмотреть как оно там устроено. Если интересен этот вопрос — советую погуглить VMware или просто виртуальная машина.
Теперь, что касается непосредственно Docker. Он позволяет создавать мини виртуальные машины и помещать в них все что нужно для выполнения Вашего приложения вместе с самим приложением, запаковать все это в контейнер и использовать сколько угодно раз.
Например: есть у нас простое java приложение. Допустим это веб приложение, которое имеет свою реляционную базу данных. Пусть будет PostgreSQL. И еще наше приложение использует не реляционную базу (NoSql) MongoDB. В принципе, стандартный набор. Вот мы запустили наше приложение локально и полны счастья и радости. Но что если нам нужно показать приложение товарищу? Не вопрос. Мы можем сбросить ему ссылку на гит или вообще перебросить код. Вот он пытается запустить наше приложение а ему нужна база данных. Наш товарищ устанавливает PostgreSQL, потом MongoDB. Приложение запустилось.
Потом мы сбрасываем приложение другому товарищу. У него уже есть PostgreSQL, но только не версии 9.2 как у нас, а 6.4. Он не может удалить старую версию потому что у него другое приложение использует его старую версию базы. Что делать в этом случае? А что если наши товарищи — это не люди, а разные сервера?
Написать приложение — это пол беды. Не менее сложно заставить его работать на сервере именно так как мы того хотим. Особенно сложно когда приложений несколько сотен и все они имеют определенные зависимости будь-то база данных или менеджер настроек. Еще сложнее, когда к существующим системам приходится добавлять новые приложения, которые могут например требовать новые версии баз данных.
Таким образом мы получаем ситуацию, когда было бы удобно использовать некую формальную оболочку, чтобы можно было запускать приложения только в ней. И чтобы можно было запускать сколько угодно таких оболочек на одном физическом сервере и чтобы они не пересекались по зависимостям. Чтобы была возможность на одной оболочке использовать базу данных версии 6.4, на другой версии 7.8, а на третьей версии 9.2.
Для вышеописанных решений и был придуман Докер. Только в контексте Docker, оболочка — это контейнер (container).
Это как аналогия с корабельным контейнером.
Когда нужно перевезти большое количество разного груза — его помещают в контейнер. Таким образом груз не смешивается и его очень удобно транспортировать. Это тоже самое только в разрезе запуска приложений.
С docker container (контейнером) теперь вопросов быть не должно. Это инкапсулированная среда в которой выполняются приложения.
К стати, контейнеры могут общаться как между собой так и с внешней средой.
Сам по себе пустой контейнер вещь бесполезная. Чтобы он стал полезным в него нужно положить наше приложение, а также все что нужно приложению для запуска. Основа для контейнера — образ (image).
Чтобы было понятно, образ — это набор команд которые будут выполняться для запуска контейнера. Docker images — это наполнения наших контейнеров, их содержимое. Чтобы было еще проще — это можно представить как аналогию с репозиторием кода. Например, есть определенный код на гитхаб который автор выложил для общего пользования. Например приложение на спринг бут. Это приложение может скачать и запустить любой желающий. Потому что я выложил его в общий доступ. Вы можете его скачать себе, изменить там любой участок кода и залить на свой гит аккаунт.
В контексте Docker существует Docker Hub, где также есть готовые образы, которые вы сможете себе закачать. Туда же вы сможете залить свои созданные образы. Советую там зарегистрироваться.
Именно с докер хаб будут скачаны базовые образы такие как оболочка операционной системы база данных mysql например. Ведь для того, чтобы запустить java приложение Вам как минимум нужна операционная система и установленная java.
На данном этапе все еще может быть не понятно. Все дело в том, что я не показал Вам ни единого примера.
Перед тем как показывать примеры, докер нужно установить. На этом моменте я останавливаться не буду. Здесь все предельно просто: качаем установщик с официального сайта, запускаем и следуем инструкциям.
Чтобы проверить что все работает можно запустить хело ворлд команду: docker run hello-world в командной строке.
результат работы docker run hello-world
Если Вы видите результат как на скрине выше — значит докер установлен и работает корректно.
Чтобы посмотреть активные контейнеры нужно выполнить комманду docker ps
CONTAINER ID — это уникальный идентификатор контейнера. По этому айди можно будет потом управлять контейнером (удалять, запускать, останавливать).
IMAGE — имя образа который запускался в этом контейнере. Для того чтобы посмотреть все образы нужно выполнить комманду docker images.
Теперь осталось контейнеризировать наше приложение чтобы увидеть результат работы докера. Предлагаю для примера использовать простое Spring Boot приложение. В данном случае, что именно мы будем отправлять в контейнер большого значения иметь не будет. Docker позволяет контейнеризировать любое приложение.
Я захожу на сайт https://start.spring.io/ выбираю зависимости веб и нажимаю сгенерировать:
генерация Spring Boot приложения
Дальше я добавил всего лишь один класс контроллера и метод, который будет возвращать приветствие:
простое REST приложение
Дальше, перед тем как помещать приложение в Docker, я его запустил и убедился что оно работает корректно:
результат работы приложения
Вот теперь мы готовы задеплоить наше приложение на Docker. У нас есть установленный и корректно работающий Докер и корректно работающее java приложение.
Для того, чтобы написать свой образ используется Dockerfile.
Это просто текстовый файл в который нужно прописать команды для Докера. Теперь о хорошем) Список команд не большой и учить много не нужно. Команды интуитивно понятны и состоят фактически из одного-двух слов.
Вот пример докер файла для помещения нашего приложения в Docker:
Синтаксис докер файла не чувствителен к регистру, но команды принято писать в верхнем регистре.
Команды выполняются по порядку сверху вниз. Сначала будет выполнена команда FROM. Она определяет базовый образ контейнера. В нашем примере, за базовый образ у нас openjdk:8-jdk-alpine. Ведь, для того чтобы запускать java приложения нам необходима установлена джава.
Знаком # объявляют комментарии. Это как аналогия // в java.
ARG — используется для объявления аргументов. Если брать аналогию с языком java: String file = «filename»;. В докере это будет ARG JAR_FILE=target/*.jar Где JAR_FILE — это имя переменной, а target/*.jar — ее значение. Именно в папке target находиться сгенерированный jar файл после билда мавеном.
Дальше, идет команда COPY. Она говорит докеру что нужно скопировать из папки target файл с расширением jar в контейнер и дать ему новое имя app.jar.
Чтобы сгенерировать образ нужно воспользоваться командой:
Запускать ее нужно находясь в одной директории с Dockerfile. Когда докер начнет выполнять ваш файл он скачает в контейнер либы джавы, скопирует jar-ник с папки target и збилдит контейнер.
Запустите команду docker images чтобы увидеть вновь созданный образ:
созданный образ
Мы не запушили наш образ в репозиторий поэтому в строке REPOSITORY пусто. Строка TAG тоже пуста поскольку мы не задавали тегов нашему образу. Нас сейчас больше всего интересует IMAGE ID. С помощью этого айди мы можем запустить контейнер.
Для запуска контейнера существует команда docker run айди_образа. Таким образом, чтобы запустить наш контейнер нужно выполнить команду docker run 6c8a153530ff
результат запуска команды docker run 6c8a153530ff
Теперь, когда Вы выполните команду docker ps, то увидите свой запущенный контейнер:
Чтобы остановить запущенный контейнер нужно воспользоваться командой: docker stop контейнер_айди.
Вот таким образом мы подняли наше java приложение в Docker. Тема докера очень обширна и многообразна. Уместить все в одну статью просто не получиться. Но, что касается основ докера — этого достаточно.