2 апреля 2018
Хардкор

Как перевести решение на микросервисную архитектуру, не останавливая продакшн

Мы перевели на микросервисы высоконагруженное монолитное решение. Через него проходит от 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-запросов. 

Мы выбрали его, поскольку 

— мало накладных расходов на внедрение в проект,
— сам генерировал клиента, 
— можно использовать один интерфейс и на сервере, и на клиенте. 

Он решил наши проблемы с типизацией тем, что мы формировали клиентов. И для контролеров наших сервисов мы использовали те же интерфейсы, что и для формирования клиентов. Так ушли проблемы с типизацией.

 

Простои. Бой первый. Работоспособность

 

Вызов бизнеса: 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.


Что изменилось с переходом на микросервисы

  • Мы избавились от проблемы меняющихся требований.
  • Избавились от проблемы восстановления сессий у Tomcat тем, что перенесли их в Hazelcast.
  • При перебрасывании пользователей с одного сервера на другой им не приходится перелогиниваться.
  • Решили все проблемы с высвобождением ресурсов, переложив их на плечи операционной системы.
  • Проблемы типизации и версионирования решились благодаря Feign.
  • Уверенно движемся в сторону Continuous Delivery c помощью Gitlab Pipelines.

Оригинал опубликован на Habr.