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

Но для начала надо немного описать платформу на которую мы деплоим наши приложения. В качестве servlet-container’а мы используем Jetty. Есть два основных типа приложений, которые мы пишем и сопровождаем: web-приложения, которые имеют некий web-интерфейс (это может быть как UI, так и REST интерфейс), а также background сервисы, которые как правило работают асинхронно (мы активно используем ActiveMQ). И здесь нужно подметить первую особенность, мы используем Jetty для обоих типов приложений. Это может показатся странным, так как для background-сервисов мы получаем явный overhead связанный с запуском и работой servlet-container’а. Но этот overhead просто мизерный по сравнению с тем какие плюсы мы получаем. А именно:

  • единые утилиты управления lifecycle’ом приложения вне зависимости от его типа (старт, останов, перезапуск);
  • стандартные способы задания конфигурации приложения вне зависимости от его типа;
  • стандартные форматы распространения приложения вне зависимости от его типа (в нашем случае .war).

Вобщем, стандартизация на лицо.

У нас своя сборка Jetty в которой кроме самого Jetty, есть еще несколько часто используемых нами библиотек (например: slf4j, logback, mysql драйвер, библиотеки connection pooling’а и некоторые другие). Сборка представлена в виде rpm пакета, что позволяет очень быстро подготавливать новую машину: yum install jdk jetty, готово.

Вас может удивило почему мы ставим на production машины JDK, а не JRE. Это связано с наличием в JDK очень полезных для диагностики утилит, таких как: jmap, jstack, jvisualvm и других. Это выгодно отличает платформу Java от множества других, – масса утилит для диагностики, которые в большинстве случаев позволяют довольно быстро понять что не так с приложением.

Деплой приложения состоит из следующих шагов:

  • распространение бинарника с приложением, а также конфигурации на целевые машины;
  • перезапуск servlet container’а;
  • deployment тестирование.

Естественно второй и третий шаг выполняются для всех машин по очереди.

Распространение артефакта и конфигурации Link to heading

Тут все просто, scp доставляет файлы на каждую машину по очереди и ложит каждый в нужное место. Доставляются артефакты, а также конфигурационные файлы.

Перезапуск servlet container’а Link to heading

Мы не используем hot redeploy, так как он вызывает ряд проблем. В частности, небезизвестную OutOfMemoryError: PermGen. Для перезапуска используются скрипты входящие в стандартную поставку Jetty с некоторыми несущественными дополнениями.

Deployment тестирование Link to heading

Этот момент стоит описать поподробнее. После того как Jetty запустился было бы неплохо проверить что приложение способно выполнять основные функции. Тот кто деплоит приложение может опечататься в конфигурации, системные администраторы могут забыть “пропилить дырку” в firewall’е для необходимого приложению сервиса, мало ли что еще может произойти.

Задача deployment тестирования проверить что у приложения есть доступ ко всем необходимым сервисам для того чтобы оно выполняло свою работу. Это могут быть базы данных, очереди сообщений, сетевые файловые системы и другое middleware ПО. К счастью для нас, Spring Beans контейнер практически все делает за нас. Если во время инициализации контейнера (частью которого являются подключения к БД и т.д.) происходит ошибка, об этом легко узнать послав GET запрос на любой url, который обрабатывается непосредственно spring’ом. У нас есть соглашение что url /status не занимается приложением и служит для deployment тестирования. Этот url также сообщает какая версия приложения запушена в данный момент. Таким образом, deploy скрипт после того как перезапускает servlet container начинает опрашивать приложение. Если приложение возвращает 200-й статус код, то можно переходить к обновлению следующей машины. Если нет, то процесс деплоя считается неуспешным и прерывается. Тот кто делает deploy может инициировать процедуру отката. Откат мы делаем руками, так как процент “битых” релизов у нас не велик и смысла автоматизировать эту процедуру пока что смысла нет.

Rollout распределенных приложений Link to heading

Когда приложение запущено в нескольких экземплярах (на нескольких машинах), то после запуска приложения на одном ноде имеет смысл немного подождать прежде чем переходить к следующей машине. Здесь играют свою роль как особенности приложения, так и особенности платформы. Java, как русские – “долго запрягает, но быстро едет” (начальная загрузка байт кода, создание пула соединений, JIT компилятор etc). Нас такой tradeoff вполне устраивает, поэтому наш deploy скрипт может быть сконфигурирован таким образом, чтобы давать только что запущенному ноду “разогреться” перед тем как принять на себя нагрузку его еще не обновленных коллег. Как правило, 3-5 секунд достаточно.

Деплой на несколько машин (особенно учитывая их “разогрев”) порождает интересную проблему. Во время обновления кластера, на нем работает две версии приложения. Некоторых людей этот факт ставит в ступор. Они не могут ужиться с тем, что у приложения в любой момент времени нет строго определенной версии.

Суровая реальность заключается в том, что требование работать одновременно в “нескольких версиях” порождается природой web-приложений. Мы стремимся к тому чтобы наше система всегда была online. Это имеет одно очень важное последствие для процесса deploy’я:

Мы всегда должны иметь возможность откатиться на предыдущую версию системы, так как пусть редко, но бывает что ошибка все же проходит сквозь все рубежи тестирования и попадает на production. Причем rollback должен осуществлятся не медленнее чем rollout. Это приводит нас к следующему утверждению. Cледующая версия системы всегда должна быть обратно-совместима с предыдущей. Это касается не только кода, но и схемы БД, данных в кеше и т.д.

Оказывается, что когда новая версия системы обратно совместима с текущей, нет ничего страшного в том чтобы некоторое время они поработали вместе.

На практике Link to heading

Итак, как это все происходит на практике. Разработчик заходит через ssh на машину-координатор, куда автоматически доставляются артефакты приложений. У каждого приложения есть отдельная директория содержимое которой выглядит примерно следующим образом:

-rw-rw-r-- 1 tech tech      101 Jun 24 16:42 .config
-rw-rw-r-- 1 tech tech     1134 Jun  9 13:05 config.properties
-rw-r--r-- 1 tech tech      163 May 16 16:11 .jettyrc
-rw-r--r-- 1 tech tech 30298960 Jun  3 19:41 search-web-frontend-1.0.118.war
-rw-r--r-- 1 tech tech 30299011 Jun  3 21:25 search-web-frontend-1.0.119.war
-rw-r--r-- 1 tech tech 30298949 Jun 13 21:51 search-web-frontend-1.0.120.war
-rw-r--r-- 1 tech tech 30297647 Jun 14 12:38 search-web-frontend-1.0.121.war
-rw-r--r-- 1 tech tech 30297689 Jun 15 11:50 search-web-frontend-1.0.122.war
-rw-r--r-- 1 tech tech 30298356 Jun 15 12:13 search-web-frontend-1.0.123.war
-rw-r--r-- 1 tech tech 30298678 Jun 15 12:32 search-web-frontend-1.0.124.war
-rw-r--r-- 1 tech tech 30732203 Jun 23 11:45 search-web-frontend-1.0.125.war
-rw-r--r-- 1 tech tech 30732204 Jun 23 11:55 search-web-frontend-1.0.126.war

Здесь есть несколько файлов о которых стоит рассказать поподробнее. Файл .config это обыкновенный ini-файл хранящий имена всех машин на которых установлено приложение, а также некоторые другие настройки развертывания. config.properties — это properties-файл, который содержит настройки приложения. .jettyrc содержит startup опции виртуальной машины на которой стартует Jetty.

За довольно длительное время мы перепробовали различные способы работы с конфигурационными файлами приложений. Мы хранили их в development системе контроля версий. Мы создавали для них отдельный репозиторий в зоне production. Текущая схема нам нравится больше всего. Она позволяет автоматически бекапить все конфигурационные файлы приложений, что безусловно необходимо, а также не связываться с излишей сложностью VCS систем для управления “двумя файлами из 10 строчек каждый”.

Если запустить команду deploy без аргументов, то она выведет текущее состояние нодов:

$ deploy 
http://search-service1:8080 - Ok com.farpost.search:search-web-frontend 1.0.126
http://search-service2:8080 - Ok com.farpost.search:search-web-frontend 1.0.126
http://search-service3:8080 - Ok com.farpost.search:search-web-frontend 1.0.126

Благодаря maven во все наши сборки автоматически попадает информация о версии, а также номере билда.

Если же передать команде deploy имя артефакта, то начнется его развертывание на production машинах.

Дальнейшие соображения Link to heading

Текущий процесс нас вполне устраивает, тем не менее у нас есть идеи как сделать его еще лучше.

Runtime обновление конфигурации логгирования Link to heading

Мы используем logback в качестве библиотеки логгирования. Существенным ее плюсом является то что она позволяет менять конфигурацию логгирования на лету. Достаточно просто поменять XML файл с конфигурацией. Было бы неплохо иметь возможность распространять конфигурацию логгирования на машины без перезагрузки самого приложения.

Дифференцирование конфигурации различных нодов Link to heading

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

Partial deploy Link to heading

Иногда бывает необходимо обновить не весь кластер, а только один нод из кластера. В будущем, я думаю мы сделаем такую возможность.

Вот пожалуй и все. А как деплоите приложения вы? ;)