Зураб Белый, руководитель группы, практика Java, рассказывает свою историю работы в проекте для одной крупной компании и делится накопленным опытом.

Как я заселился…


В проект я попал в конце лета 2017 года рядовым разработчиком. Не скажу, что на тот момент мне это сильно нравилось: технологии в проекте использовались старые, коммуникация внутри команды была сведена к минимуму, общение с заказчиком было тяжелым и непродуктивным. Таким встретил меня проект. В то время у меня было только одно желание: побыстрее из него выйти.

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

Хибара, которая покосилась от ветра


Первое время я в основном занимался изучением технического состояния проекта, фиксил несерьезные баги и делал незначительные улучшения. С технической точки зрения приложение выглядело ужасно: монолитная архитектура, строящаяся вокруг устаревшей коммерческой версии dotCMS, код, написанный на Java 6-й версии, когда давно уже вышла девятая, серверный рендеринг клиентской части на фреймворке Velocity, который к тому времени уже несколько лет не поддерживался. Запускался каждый инстанс в JBoss AS и маршрутизировался с помощью Nginx. Утечки памяти приводили к постоянным перезапускам нод, а отсутствие нормального кеширования приводило к росту нагрузки на серверы. Но самой большой занозой были изменения, сделанные в коде CMS. Они исключали возможность безболезненного обновления на более свежую версию. Хорошей иллюстрацией этого был переход с версии 3.2 на 3.7, который как раз заканчивался в то время. Переход на следующую минорную версию занял больше года. Ни о каких популярных решениях, таких как Spring Framework, React.js, микросервисная архитектура, Docker и др., речи и не шло. Вникая глубже в проект, стали видны и последствия такого технического состояния. Самым острым из них была невозможность локального запуска приложения для разработки и отладки. Вся команда из 8 человек работала на одном девелоперском стенде, где была развернута копия продакшен-версии приложения. Соответственно, одновременно дебажить свой код мог только один разработчик, а накатывание обновленного кода блокировало всю команду. Апогеем стала провальная распродажа, во время которой по разным каналам были отправлены миллионы писем, смс- и пуш-уведомлений пользователям — одновременно были открыты десятки тысяч сессий. Серверы не выдерживали, и большую часть времени портал был недоступен. Приложение плохо масштабировалось. Был только один способ это делать: развернуть рядом еще одну копию и сбалансировать нагрузки между ними с помощью Nginx. А каждая поставка кода на продакшен предполагала большое количество ручной работы и занимала несколько часов.

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

Мы строили, строили...


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

Процессуальные изменения были более глобальными. К тому времени разработка уже велась по Agile- + Scrum-методологии, двухнедельными спринтами с поставкой в конце каждой итерации. Но фактически это не только не увеличивало скорость работы, а наоборот, тормозило. Ежедневные митинги затягивались на полтора-два часа и не давали никаких результатов. Планирование и груминг превращались в споры, ругань или простое общение. С этим нужно было что-то делать. Менять что-либо в этом русле изначально было очень сложно — в лице заказчика команда практически потеряла доверие, особенно после неудачной распродажи. Каждое изменение нужно было долго обосновывать, обсуждать и доказывать. Как ни странно, но инициатива пришла от заказчика. С их стороны был привлечен scrum-мастер для контроля правильности применения подходов и методологий, отлаживания процессов и настроя команды на рабочий лад. Хоть его и привлекли всего на несколько спринтов, это помогло нам правильно собрать фундамент. Сильно изменился подход к общению с заказчиком. Мы стали чаще обсуждать проблемы в процессах, ретроспективы начали проходить более продуктивно, разработчики охотнее давали обратную связь, а заказчик со своей стороны шел навстречу и всячески поддерживал процесс перехода.

Но, положа руку на сердце, честно скажу: было немало моментов, когда некоторые изменения внутри команды проводились «втемную», и уже после появления положительных результатов об этом сообщалось заказчику. За полгода отношение изменилось до комфортной рабочей коммуникации. Затем последовали несколько тимбилдингов, одно- и двухдневные встречи всей команды разработки с командой заказчика (маркетолог, аналитик, дизайнер, продукт-оунер, контент-менеджеры и др.), совместные походы в бар. Через год, да и по сей день, общение можно назвать дружеским. Атмосфера стала доброжелательной, расслабленной и комфортной. Без конфликтов, конечно, не обходится, но даже в самой счастливой семье иногда бывают ссоры.

Не менее интересные изменения происходили в этот период в коде приложения, в его архитектуре и используемых решениях. Если вы не подкованы технически, можете смело пропускать весь текст до заключения. А если вам повезло так же, как мне — добро пожаловать! Весь переход можно разбить на несколько этапов. О каждом поподробнее.

Этап 1. Определение критических проблемных мест.

Тут все было максимально просто и понятно. В первую очередь нужно было избавляться от зависимости стороннего коммерческого продукта, распилить монолит и сделать возможность локальной отладки. Хотелось отделить клиентский и серверный код, разнести его архитектурно и физически. Еще одно проблемное место — квалификация. В проекте напрочь отсутствовало какое-либо автоматическое тестирование. Это немного затрудняло процесс перехода, так как все нужно было проверять вручную. Учитывая, что технических заданий на функционал никогда не было (тут сказывается специфика проекта), была большая вероятность что-то пропустить. Расписав проблемные места, мы еще раз посмотрели на список. Выглядело как план. Пришло время строить небоскреб!

Этап 2. Обновление кодовой базы.

Самый долгоиграющий этап. Все началось с перехода к сервисной архитектуре (не путать с микросервисами). Идея была следующая: разбить приложение на несколько отдельных сервисов, каждый из которых будет заниматься своей определенной задачей. Сервисы должны были быть не «микро», но и засовывать все в один котел тоже не хотелось. Каждый сервис должен был представлять собой Spring Boot-приложение, написанное на Java SE 8, и запускаться на Tomcat.

Первым стал т. н. «контент-сервис», который стал прослойкой между будущим приложением и CMS. Он стал абстракцией на пути к контенту. Предполагалось, что все запросы, которые раньше мы делали напрямую в CMS, будут выполняться через этот сервис, но уже по протоколу HTTP. Такое решение позволило нам уменьшить связность и создало возможность впоследствии заменить dotCMS на более современный аналог или вообще избавить от использования коммерческих продуктов и написать свое решение, заточенное под наши задачи (забегая вперед, скажу, что именно этим путем мы и пошли).

Сразу создали почву для отделения фронтового и бэкендового кода. Создали фронт-сервис, который стал отвечать за раздачу кода, написанного на реакте. Прикрутили npm, настроили ноду и отладили сборку — все как полагается по современным тенденциям клиентской части.

В общем случае выделяли функционал в сервис по следующему алгоритму:

  • создавали новое Spring Boot-приложение со всеми необходимыми зависимостями и настройками;
  • портировали всю базовую логику (часто писали ее с нуля, обращаясь к старому коду, только чтобы убедиться, что не забыли ни о каком нюансе), например, для сервиса кеширования — это возможности добавлять в кеш, читать из него и инвалидировать;
  • весь новый функционал всегда писали с использованием нового сервиса;
  • постепенно переписывали старые куски приложения в новый сервис в порядке важности.

На старте у нас их было немного:

  • Контент-сервис. Выступал в качестве прослойки между приложением и CMS.
  • Кэш-сервис. Простое хранилище на Spring Cache.
  • АА-сервис. На старте он занимался только раздачей информации об авторизованном пользователе. Остальное так и оставалось внутри dotCMS.
  • Фронт-сервис. Отвечал за раздачу клиентского кода.

Этап 3. Автотесты.

Учитывая весь опыт проекта, решили, что наличие функциональных тестов сильно упрощает жизнь и возможное дальнейшее обновление приложения. Пришло время внедрить их в проект. Юнит-тесты кода, как ни прискорбно об этом говорить, заглохли почти сразу. Времени на написание и поддержку они отнимали много, а его было у нас совсем мало, ведь, помимо переписывания кода, на нас висели текущие задачи по новому функционалу, да и баги частенько всплывали. Было решено остановиться только на тестировании интерфейса приложения с помощью Selenium. Это, с одной стороны, упростило нам регрессионное тестирование перед поставками на продакшен, с другой — появилась возможность делать рефакторинг на серверной стороне, контролируя состояние на клиентской. В команде не было автоматизатора, а написание и поддержка актуальности автотестов требуют дополнительных затрат. Переобучать кого-то из тестировщиков не стали, и в команду был добавлен еще один человек.

Этап 4. Автоматизация деплоя.

Теперь, когда у нас появились отдельные сервисы, когда фронтенд отделился от бэкенда, когда основная функциональность начала покрываться автотестами, пришло время открыть баночку пива и автоматизировать всю ручную работу по развертыванию и поддержке приложения локально, на демонстрационных и прод-серверах. Распил монолита на части и использование Spring Boot открыли для нас новые горизонты.

Разработчики смогли отлаживать код локально, запуская только ту часть функционала, которая для этого необходима. Тестовые стенды наконец-то стали использоваться по назначению — туда попадал уже более-менее отлаженный код, готовый для первичного и квалификационного тестирования. Но по-прежнему осталось много ручной работы, отнимающей у нас драгоценное время и силы. После изучения проблемы и перебора вариантов решения мы остановились на связке Gradle + TeamCity. Так как сборщиком проекта был Gradle, добавлять что-то новое смысла не имело, да и написанные скрипты получились платформонезависимыми, их можно запускать на любой ОС, удаленно или локально. И это позволило не только использовать любое решение для CI/CD, но и безболезненно сменить платформу на любую другую. TeamCity было выбрано из-за богатого встроенного функционала, наличия большого комьюнити и длинного списка плагинов на все случаи жизни, а также интеграции со средой разработки Intellij IDEA.

На данный момент существует более 100 скриптов оптимизации и более 300 задач в CI-системе для их запуска с разными параметрами. Это не только деплой на тестовые стенды и поставка в продакшен, но и работа с логами, управление серверами, маршрутизацией и просто решения для рутинных однотипных задач. Часть задач снялась с наших плеч. Контент-менеджеры смогли сами сбрасывать кэш. Ребята из техподдержки получили возможность самостоятельно дергать отдельные сервисы, проводить первичные реанимационные действия. Сон разработчиков стал глубже и спокойнее.

Этап 5. Собственная CMS.

После того как удалось абстрагироваться от коммерческой CMS, появилась возможность получать данные еще и из другого источника, не переписывая код приложения. Откуда брать те или иные данные решал сервис по работе с контентом. После поиска готовых решений и их анализа приняли решение писать собственную систему управления контентом, так как ни одно из них не удовлетворяло наши потребности полностью. Написание собственной CMS — процесс бесконечный, постоянно появляются новые потребности и пожелания. Выбрали несколько основных фич и отправились в прекрасный мир разработки. Для запуска первой версии в прод нам хватило полтора человекомесяца. По мере готовности функционала в новой CMS мы переносим в нее контент из старой. Все новые страницы никакого отношения к dotCMS уже не имеют. Со временем это позволило отказаться от платной версии и перейти на комьюнити-версию, а в дальнейшем и вовсе отказаться от чего-то стороннего.

Этап 6. Докерезация.

Закатав штаны, мы начали наш путь в мир хипстерского программирования. Этот этап для моего проекта стал заключительным в процессе перестройки. Он продолжается до сих пор. Основная сфера, ради которой этот этап вообще появился, — масштабирование. Связка Docker + Kubernetes + Consul позволяет не только облегчить деплой на разные серверы и управлять каждым сервисом отдельно, но и гибко масштабировать все приложение, делать это только в тех местах, где это необходимо, и только на то время, пока это требуется. Более подробно я смогу расписать только когда мы полноценно перейдем на это решение в продакшене.

…и, наконец, построили. Ура!



Прошел год с начала обновления приложения. Сейчас это 26 REST-сервисов, написанных на Spring Boot. Каждый имеет подробную документацию API в Swagger UI. Клиентская часть написана на React.js и отделена от серверной. Все основные страницы портала покрыты тестами. Проведено несколько крупных распродаж. Переход к современным технологиям, избавление от старого кода и стабильная работа серверов сильно мотивируют разработчиков. От «как сказали, так и делаем» проект перешел в русло, где каждый заинтересован в успешности, предлагает свои варианты улучшений и оптимизации. Изменилось отношение заказчика к команде, создана дружественная атмосфера.

Это результат работы всей команды, каждого разработчика и тестировщика, руководителей и заказчика. Это было очень тяжело, нервно и местами на грани фола. Но сплоченность команды, грамотное планирование и осознание результатов позволили преодолеть все трудности.

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


  1. werklop
    25.06.2019 18:45

    Здравствуйте!
    Подскажите, почему выбор пал на Swagger UI, а не Spring REST Docs?


    1. BelyjZ
      26.06.2019 10:16

      Swagger UI мы все знали, и он нас устраивал полностью. Со Spring REST Docs у меня лично опыта работы не было. А так как нового для нас уже было достаточно, и это все-таки не ключевой функционал, оставили привычное решение.


  1. sentyaev
    25.06.2019 20:41

    А как решали вопрос с развитием проекта во время такой миграции?


    1. BelyjZ
      26.06.2019 14:42
      +1

      Проект развивался постоянно. Задачи расширения функционала и переезда решались параллельно. Сначала занимался только я: набросал скелеты основных сервисов, написал базовую логику. Подготовил почву для реализации новых фич, обособленных от основного кода, уже в новой среде. Далее к процессу постепенно подтянулась вся команда. Весь новый функционал создавался сразу в новой версии. Это обсуждалось с заказчиком, на задачи выделялось больше времени, в их рамках создавались общие компоненты и фундаментальные вещи для дальнейшей разработки. В каждый спринт (по крайней мере мы старались брать в каждый) закладывались технические задачи, невидимые для заказчика. Но, так как проект бизнесовый, периодически приходили срочные таски, например, быстро сделать страницу для новой акции, которая стартует на следующей неделе. Тут уже не до красоты, делали в старой версии приложения, выкидывали в прод, а через несколько недель, после окончания акции, просто забывали про этот код. Отсюда и такой срок на переезд. Фактически в единицу времени над ним работали только 1-2 человека. Постепенно это цифра росла, и старый код уже менялся крайне редко. Иногда по несколько спринтов туда не приходилось залезать.