Сегодня расскажем, как переводили на микросервисы монолитное решение. Через наше приложение круглосуточно проходит от 20 до 120 тысяч транзакций в сутки. Пользователи работают в 12 часовых поясах. В то же время функционал добавлялся много и часто, что довольно сложно делать на монолите. Вот почему системе требовались устойчивая работа в режиме 24/7, то есть HighLoad, High Availability и Fault Tolerance.
Мы развиваем этот продукт по модели MVP. Архитектура менялась в несколько этапов вслед за требованиями бизнеса. Первоначально не было возможности сделать всё и сразу, потому что никто не знал, как должно выглядеть решение. Мы двигались по модели Agile, итерациями добавляя и расширяя функциональность.
Первоначально архитектура выглядела следующим образом: у нас был MySql c единственным war, Tomcat и Nginx для проксирования запросов от пользователей.
Окружения (& минимальный CI/CD):
- Dev — деплой по Push в develop,
- QA — раз в сутки с develop,
- Prod — по кнопке с master,
- Запуск интеграционных тестов вручную,
- Всё работает на Jenkins.
Разработка была построена на основе пользовательских сценариев. Уже на старте проекта большая часть сценариев укладывалась в некий workflow. Но все же не все, и это обстоятельство усложняло нам разработку и не позволяло провести «глубокое проектирование».
В 2015 году наше приложение увидело продакшн. Промышленная эксплуатация показала, что нам не хватает гибкости в работе приложения, его разработке и в отправке изменений на prod-сервера. Мы хотели добиться High Availability (HA), Continuous Delivery (CD) и Continuous Integration (CI).
Вот проблемы, которые необходимо было решить для того, чтобы прийти к HI, CD, CI:
- простой при выкатывании новых версий — cлишком долго происходил deploy приложения,
- проблема с меняющимися требованиями к продукту и новые user cases — слишком много времени уходило на тестирование и проверки даже при небольших фиксах,
- проблема с восстановлением сессий у Tomcat: session management для системы бронирования и сторонних сервисов, при перезапуске приложения сессия не восстанавливалась силами Tomcat,
- проблемы с высвобождением ресурсов: приходилось рано или поздно перезагружать Tomcat, происходили memory leak.
Мы стали решать все эти проблемы поочередно. И первое, за что взялись — это меняющиеся требования к продукту.
Первый микросервис
Вызов: Изменяющиеся требования к продукту и новые use cases.
Технологический ответ: Появился первый микросервис — вынесли часть бизнес-логики в отдельный war-файл и положили в Tomcat.
К нам пришла очередная задача вида: до конца недели обновить бизнес-логику в сервисе и мы приняли решение вынести эту часть в отдельный war-файл и положить в тот же Tomcat. Мы использовали Spring Boot для скорости конфигурирования и разработки.
Мы сделали небольшую бизнес-функцию, которая решала проблему с периодически изменяющимися параметрами пользователей. В случае изменения бизнес-логики нам бы не пришлось перезапускать весь Tomcat, терять наших пользователей на полчаса и перезагрузить только небольшую его часть.
После удачного вынесения логики по такому же принципу мы продолжили вносить изменения в приложение. И с того момента, когда к нам приходили задачи, которые кардинально меняли что-то внутри системы, мы выносили эти части отдельно. Таким образом, у нас постоянно копились новые микросервисы.
Основной подход, по которому мы начали выделять микросервисы — это выделение бизнес-функции или бизнес-услуга целиком.
Так у нас быстро отделились сервисы, интегрированные со сторонними системами, такими как 1C.
Первая проблема — типизация
Вызов: Микросервисов уже 15. Проблема типизации.
Технический ответ: Spring Cloud Feign.
Проблемы не решились сами собой лишь потому, что мы начали разрезать наши решения на микросервисы. Более того, стали возникать новые проблемы:
- проблема типизации и версионирования в Dto между модулями,
- как задеплоить не один war-файл в Tomcat, а множество.
Новые проблемы увеличили время перезапуска всего Tomcat при технических работах. Получается, мы усложнили себе работу.
Проблема с типизацией, конечно, возникла не сама собой. Скорее всего, в течение нескольких релизов мы её просто игнорировали, потому что находили эти ошибки ещё на этапах тестирования или во время разработки и успевали что-то предпринять. Но когда несколько ошибок были обнаружены уже совсем на полпути в продакшн и потребовали срочного исправления, мы ввели регламенты или начали использовать инструменты, которые решают эту проблему. Мы обратили внимание на Spring Cloud Feign — это клиентская библиотека для http-запросов.
github.com/OpenFeign/feign
cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html
Мы выбрали его, поскольку
— мало накладных расходов на внедрение в проект,
— сам генерировал клиента,
— можно использовать один интерфейс и на сервере, и на клиенте.
Он решил наши проблемы с типизацией тем, что мы формировали клиентов. И для контролеров наших сервисов мы использовали те же интерфейсы, что и для формирования клиентов. Так ушли проблемы с типизацией.
Простои. Бой первый. Работоспособность
Вызов бизнеса: 18 микросервисов, теперь простои в работе системы недопустимы.
Технический ответ: изменение архитектуры, увеличение серверов.
У нас осталась проблема с простоем в работе и выкатыванием новых версий, осталась проблема с восстановлением сессии Tomcat и с освобождением ресурсов. Количество микросервисов же продолжало расти.
Процесс деплоя всех микросервисов занимал около часа. Периодически приходилось перезагружать приложение из-за проблемы с высвобождением ресурсов у tomcat. Не было простых способов делать это более оперативно.
Мы стали продумывать, как изменить архитектуру. Вместе с отделом инфраструктурных решений мы построили новое решение на основе того, что у нас уже было.
Архитектура изменила свой вид следующим образом:
- горизонтально разделили наше приложение на несколько data-центров,
- добавили Filebeat на каждый сервер,
- добавили отдельный сервер для ELK, поскольку росло количество транзакций и логов,
- несколько серверов haproxy + Tomcat + Nginx + MySQL (так мы обеспечивали High Availability).
Используемые технологии были такими:
- Haproxy занимается роутингом и балансировкой между серверами,
- Nginx отвечает за раздачу статики, tomcat был сервером приложений,
- Особенностью решения стало то, что MySQL на каждом из серверов не знает о существовании своих других MySQL’ей,
- Из-за проблемы latency между датацентрами репликация на уровне MySQL была невозможна. Поэтому мы решили реализовать шардинг на уровне микросервисов.
Соответственно, когда приходил запрос от пользователя до сервисов в Tomcat, они просто запрашивали данные у MySQL. Те данные, которые требовали целостности, собирались со всех серверов и склеивались (все запросы были через API).
Применив этот подход, мы немного потеряли в консистентности данных, зато решили текущую задачу. Пользователь мог работать с нашим приложением в любых ситуациях.
- Даже если один из серверов падал, у нас оставалось ещё 3-4, которые, поддерживали работоспособность всей системы.
- Мы хранили бекапы не на серверах в том же дата-центре, в котором они делались, а в соседних. Это помогало нам с disaster recovery.
- Fault tolerance решалась также за счет нескольких серверов.
Так были решены крупные проблемы. Ушёл простой в работе пользователей. Теперь они не чувствовали, когда мы накатывали обновление.
Простои. Бой второй. Полноценность
Вызов бизнеса: 23 микросервиса. Проблемы с консистентностью данных.
Техническое решение: запуск сервисов отдельно друг от друга. Улучшение мониторинга. Zuul и Eureka. Упростили разработку отдельных сервисов и их поставку.
Проблемы продолжали появляться. Так выглядел наш редеплой:
- У нас не было консистентности данных при редиплое, поэтому часть функционала (не самого важного) отходила на второй план. К примеру, при накатывании нового приложения статистика работала неполноценно.
- Нам приходилось выгонять пользователей с одного сервера на другой, чтобы перезапустить приложение. Это тоже занимало порядка 15-20 минут. Вдобавок ко всему, пользователям приходилось перелогиниваться при переходе с сервера на сервер.
- Также мы всё чаще перезапускали Tomcat из-за роста количества сервисов. И теперь приходилось следить за большим количеством новых микросервисов.
- Время редиплоя выросло пропорционально количеству сервисов и серверов.
Подумав, мы решили, что нашу проблему решит запуск сервисов отдельно друг от друга — если мы будем запускать сервисы не в одном Tomcat, а каждый в своём на одном сервере.
Но появились другие вопросы: как сервисам теперь общаться между собой, какие порты должны быть открыты наружу?
Мы выбрали ряд портов и раздали их нашим модулям. Чтобы не было необходимости держать всю эту информацию о портах где-то в pom-файле или общей конфигурации, мы выбрали для решения этих задач Zuul и Eureka.
Eureka — service discovery
Zuul — proxy (для сохранения контекстных урлов, что были в Tomcat)
Eureka также улучшила наши показатели в High Availability /Fault Tolerance, поскольку теперь стало возможным общение между сервисами. Мы настроили так, что если в текущем дата-центре нет нужного сервиса, идти в другой.
Для улучшения мониторинга добавили из имеющегося стека Spring Boot Admin для понимания того, что на каком сервисе происходит.
Также мы начали переводить наши выделенные сервисы к stateless-архитектуре, чтобы избавиться от проблем деплоя нескольких одинаковых сервисов на одном сервере. Это дало нам горизонтальное масштабирование в рамках одного data-центра. Внутри одного сервера мы запускали разные версии одного приложения при обновлении, чтобы даже на нём не было никакого простоя.
Получилось, что мы приблизились к Continuous Delivery / Continuous Integration тем, что упростили разработку отдельных сервисов и их поставку. Теперь не нужно было опасаться, что поставка одного сервиса вызовет утечку ресурсов и придётся перезапускать весь сервис целиком.
Простой при выкатывании новых версий всё ещё остался, но не целиком. Когда мы обновляли поочерёдно несколько jar на сервере, это происходило быстро. И на сервере не возникало никаких проблем при обновлении большого количества модулей. Но перезапуск всех 25 микросервисов во время обновления занимал очень много времени. Хоть и быстрее, чем внутри Tomcat, который делает это последовательно.
Проблему с освобождением ресурсов мы решили также тем, что запускали всё с jar, а утечками или проблемами занимался системный Out of memory killer.
Бой третий, управление информацией
Вызов бизнеса: 28 микросервисов. Очень много информации, которой нужно управлять.
Техническое решение: Hazelcast.
Мы продолжили реализовывать свою архитектуру и поняли, что наша базовая бизнес-транзакция охватывает сразу несколько серверов. Нам было неудобно слать запрос в десяток систем. Поэтому мы решили использовать Hazelcast для event-мессаджинга и для системной работы с пользователями. Так же для последующих сервисов использовали его как прослойку между сервисом и базой данных.
Мы, наконец, избавились от проблемы с consistency наших данных. Теперь мы могли сохранять любые данные во все базы одновременно, не делая никаких лишних действий. Мы сказали Hazelcast, в какие базы данных он должен сохранять приходящую информацию. Он делал это на каждом сервере, что упростило нашу работу и позволило избавиться от шардинга. И тем самым мы перешли к репликации на уровне приложения.
Также теперь мы стали хранить сессию в Hazelcast и использовали его для авторизации. Это позволило переливать пользователей между серверами незаметно для них.
От микросервисов к CI/CD
Вызов бизнеса: нужно ускорить выход обновлений в продакшн.
Техническое решение: конвейер развертывания нашего приложения, GitFlow для работы с кодом.
Вместе с количеством микросервисов развивалась и внутренняя инфраструктура. Мы хотели ускорить поставку наших сервисов до продакшна. Для этого мы внедрили новый конвейер развертывания нашего приложения и перешли к GitFlow для работы с кодом. СI начал собирать и прогонять тесты по каждому комиту, прогонять unit-тесты, интеграционные, складывать артефакты с поставкой приложения.
Чтобы делать это быстро и динамически, мы развернули несколько GitLab-раннеров, которые запускали все эти задачи по пушу разработчиков. Благодаря подходу GitLab Flow, у нас появилось несколько серверов: Develop, QA, Release-candidate и Production.
Разработка происходит следующим образом. Разработчик добавляет новый функционал в отдельной ветке (feature branch). После того, как разработчик закончил, он создает запрос на слияние его ветки с магистральной веткой разработки (Merge Request to Develop branch). Запрос на слияние смотрят другие разработчики и принимают его или не принимают, после чего происходит исправление замечаний. После слияния в магистральную ветку разворачивается специальное окружение, на котором выполняются тесты на поднятие окружения.
Когда все эти этапы закончены, QA инженер забирает изменения к себе в ветку “QA” и проводит тестирование по ранее написанным тест-кейсам на фичу и исследовательское тестирование.
Если QA инженер одобряет проделанную работу, тогда изменения переходят в ветку Release-Candidate и разворачиваются на окружении, которое доступно для внешних пользователей. На этом окружении заказчик производит приемку и сверку наших технологий. Затем мы перегоняем всё это в Production.
Если на каком-то этапе находятся баги, то именно в этих ветках мы решаем эти проблемы и их мёрджим в Develop. Также сделали небольшой плагин, чтобы Redmine мог сообщать нам, на каком этапе находится фича.
Это помогает тестировщикам смотреть, на каком этапе нужно подключаться к задаче, а разработчикам — править баги, потому что они видят, на каком этапе произошла ошибка, могут пойти в определенную ветку и воспроизвести её там.
Дальнейшее развитие
Вызов бизнеса: переключение между серверами без простоя.
Техническое решение: Упаковка в Kubernetes.
Сейчас по окончанию деплоймента технические специалисты докладывают jer-ки на PROD-сервера и перезапускают их. Это не очень удобно. Мы хотим автоматизировать работу системы и дальше, внедрив Kubernetes и связав его с data-центром, обновляя их и разом накатывая.
Чтобы перейти к этой модели, нам необходимо закончить следующие работы.
- Привести наши текущие решения к stateless-архитектуре, чтобы пользователь мог отправлять запросы на все сервера без разбора. Некоторые из наших сервисов ещё поддерживают какие-то сессионные данные. Эта работа касается и репликации данных базы данных.
- Также мы должны распилить последний маленький монолит, который содержит в себе несколько бизнес-процессов. Это и приведёт нас к последнему главному шагу — Continuous Delivery.
P.S. Что изменилось с переходом на микросервисы
- Мы избавились от проблемы меняющихся требований.
- Избавились от проблемы восстановления сессий у Tomcat тем, что перенесли их в Hazelcast.
- При перебрасывании пользователей с одного сервера на другой им не приходится перелогиниваться.
- Решили все проблемы с высвобождением ресурсов, переложив их на плечи операционной системы.
- Проблемы типизации и версионирования решились благодаря Feign.
- Уверенно движемся в сторону Continuous Delivery c помощью Gitlab Pipelines.
Автор: EastBanc Technologies