Тестируем микросервисную архитектуру

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Для начала немного o себе: меня зовут Кирилл, я глава отдела QA в компании Slotegrator.

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

Истоки

Когда я только начинал работать в Slotegrator, это была команда из 30-ти человек, работающая на 5–6 клиентов. Платформа была написана на PHP-монолите, другими словами была построена как единое целое, где вся логика по обработке запросов помещается внутрь одного процесса. И в начале это вполне отвечало нашим нуждам.

За годы мы сильно выросли, клиентов стало значительно больше, и мало помалу начались проблемы. Каждый клиент, естественно, хочет чего-то своего.

Разумеется, монолит имел модульную структуру — содержал отдельные классы, функции, namespace, но связи между этими модулями настолько сильны, что изменение каждого из них неизбежно отражается на работе всего приложения. Поэтому любое обновление требовало участия множества команд, и самое главное, не позволяло проводить обновления быстро.

Архитектуру нашего проекта, совсем простыми словами, можно обобщить следующим образом, мы имеем:

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

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

Монолит также не позволял масштабировать части приложения, и приходилось делать это для всего приложения целиком, даже ради одного отдельно взятого компонента с наименьшей производительностью.

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

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

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

Почему микросервисы

Оценив текущий проблемы и посмотрев на перспективу, наша компания обратилась к микросервисной архитектуре.

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

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

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

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

Тестирование микросервисов

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

Главным преимуществом и одновременно трудностью тестирования является то, что они располагаются на различных серверах и написаны на разных языках программирования, таких как Java и .Net. Фактически разработчики определённого микросервиса не знают, что делают остальные микросервисы, что усложняет процесс тестирования.

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

Само тестирование можно разделить на следующие виды:

  • unit тестирование;
  • контрактное тестирование;
  • интеграционное тестирование;
  • end-to-end тестирование;
  • нагрузочное тестирование;
  • UI или функциональное тестирование.

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

Unit тестирование

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

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

Unit тестами у нас покрыто 70% функционала, и так как мы применяем CI/CD, пока они не пройдены, приложение не задеплоится.

Контрактное тестирование

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

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

Например, бэкэнд-разработчик написал код, проставил аннотации и сделал swagger-документацию. Но если swagger не провалидируется фронтендом, а QA его уже протестируют ­— мы просто зря потратим время. Поэтому и создается контракт: например, у сервиса 8 эндпоинтов, и мы знаем, в каком формате он отдаёт и принимает данные.

Контрактное тестирование необходимо для того, чтобы убедится, что все действительно так и работает. По сути, это тестирование черного ящика: как происходят процессы внутри сервиса неважно, если какая-то схема работает не по пакту — это баг.

У нас оно работает следующим образом: с самой ранней стадии есть техническое задание, согласованное со всеми стейкхолдерами. На основе ТЗ проходит оценка задачи и создается схема, согласно которой все будут работать.

Так QA может писать первичные сценарии проверки бэкэнда, который еще даже не существует. Контракт помогает понять, что ожидать. Конечно, на практике не все всегда работает гладко, не по всем микросервисам удалось создать контракт до начала разработки. Фронтенд-разработчиков у нас меньше, чем бэкенд, и им приходится подтягиваться уже после разработки.

Интеграционное тестирование

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

Так как процесс тестирования был внедрен на ранних этапах разработки, каждый отдельный сервис пришлось поднимать локально, а все зависимости других модулей — мокать. В качестве языка была выбрана Java (с момента основания компании этот язык используется для написания тестов).

Что касается сборщика, то все тоже максимально просто: наша текущая архитектура изначально использует Gradle.

Если рассмотреть нашу инфраструктуру автоматизации, то это по большей части кастомный проект, в основе которого Java и Gradle, плюс куча библиотек, таких как Junit5, Feign, Rest Assured и т.д.

Feign и Rest Assured используется вместе потому, что до перехода на микросервисную архитектуру наш проект прекрасно жил на Feign. Как библиотека для покрытия API это было лучшее решение. Но после перехода на новую архитектуру по факту вся платформа была переписана на микросервисы, которые нужно покрыть верхнеуровневыми тестами в короткий срок для возможности дальнейшего проведения интеграционного тестирования. Тут мы и подключили вторую библиотеку Rest Assured, что позволило быстро покрывать огромные куски функционала (для многих данное решение будет спорным, но на тот момент большинство новых QA работало именно с Rest Assured, что и стало решающим фактором при выборе новой библиотеки).

Для развертывания окружения в docker-контейнерах локально или на CI-сервере используется Java-библиотека TestContainers, которая позволяет оркестрировать docker-контейнерами непосредственно из кода тестов (testcontainers.org). При развертывании окружения поднимаются сам тестируемый сервис, а также используемые сервисом базы данных, брокер сообщений и эмулятор, который и мокает все внешние сервисы.

Так как все контейнеры мы поднимаем локально и на ранних этапах разработки, то потребовалось очень много времени на то, чтобы настроить перманентное окружение. Например, у нас есть сервис Settings, которому для работы нужны Сервисы 1 и 2, и какие-то данные с Kafka и MINO. Все это берется из переменных окружения, и за счет огромного количества зависимостей тяжело контролировать процесс поднятия одного сервиса.

Тестирование формально делится на автоматизированное и ручное. Мануальные тестировщики проводят тесты руками, не поднимая среды, и пишут тест-кейсы для автоматизаторов, упрощая им задачу. У нас два мануальщика покрывают пять автоматизаторов — очень удобно.

End-to-end тестирование

По сути своей E2E тестирует бизнес-логику, так же, как и в интеграционном, но уже не изолированно, а в масштабе всей системы.

В end-to-end тестировании мы проверяем взаимодействие всех сервисов c платформой:

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

Нагрузочное тестирование

Процесс нагрузочного тестирования будем формально разделять на 4 небольших этапа:

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

Для тестов мы используем JMeter, а сами нагрузочные скрипты написаны на Groovy.

Мы используем около пяти виртуальных машин, развернутых на AWS и у нас есть 7 физических машин. Последние используем, если нужно создать большую нагрузку — 15,000 RPS и больше. Виртуальные машины, так как они, по сути, являются «откусанными» частями одной большой машины, таких цифр показать не могут — каждый реквест нужно отправлять с подписью шифрования, и это сильно нагружает процессор. Так что VM используем для фоновой или статической нагрузки в районе 2000 RPS.

Статистику собираем в Grafana — анализируем все показатели, нагрузку на CPU, GPU, сеть, диски и т.д.

Сначала сравниваем с эталонными показателями, потом экспериментируем, например, нагружаем какое-то время процессор на 30%, делаем короткий скачок до 90%—100%, и смотрим, сколько битых запросов нам нападает.

UI или функциональное тестирование

Это завершающий этап тестирования. Если в предыдущих тестах фигурировал только API, теперь тестируется и фронт. Проводим как мануальное тестирование, так и автотесты.

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

Мы используем Java, Cucumber и самописную библиотеку для описания логики сценариев (раньше использовали Akitа сценарии, но поддержка библиотеки закончилась на Java 8, нам пришлось написать свою библиотеку для работы с UI-тестами, но в основе лежат методы именно оттуда). Cucumber используем для удобства написания самих тестов.

Так как проект большой, нам необходимо запускать огромное количество скриптов одновременно, для решения этой проблемы мы используем Selenide, который развернут на одном окружении с Jenkins.

В Jenkins создается pipeline, в котором прописываем, сколько контейнеров необходимо поднять для запуска теста.

Например, нужно протестировать email-шаблоны, которых у нас 100–200. В один поток это займет 15–20 минут. Поэтому создаем pipeline в Jenkins, который этот скрипт разбивает на много маленьких контейнеров, которые поднимаются в Selenide.

Можно сказать, что Selenide — это виртуальный браузер, а Selenium — виртуальный пользователь. Одновременно поднимаются 10 контейнеров, и все тесты проходят за пару минут. Все пайплайны тоже написаны на Groovy.

После этого собираем это все в отчеты в зависимости от проекта: UI в Cucumber reports, а API-тесты — в Azure.

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

Стоило ли того

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

👍НравитсяПонравилось12
В избранноеВ избранном6
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А что плохого в монолите?
Судя по:

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

у вас проблема была не в монолите. Монолитную базу тоже порезали? Если после монолита база данных осталась одна и все сервисы ходят туда, то сколько сервисы не масштабируй, база будет узким местом и по итогу получите тот же монолит, только с увеличенным латенси на коммуникацию компонентов друг с другом.

Тут можно попробовать генерить клиентов для апи из свагер схемы на ci и тогда если контракт сломан будет валиться билд.

Но если swagger не провалидируется фронтендом, а QA его уже протестируют ­— мы просто зря потратим время.

Есть ли у вас в проекте ивенты и если есть как тестируете функционал с ними?

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

Лайк за статью.
Вопрос автору — как вам работается в этой «чудесной» компании? О компании можно почитать на одном итальянском сайте https://***.it/?s=slotegrator

Ну, я в Slotegrator уже больше двух лет работаю — если бы не нравилось, ушел бы) Компания классная, на форуме «***ного IT» пишут много — видел, читал, сказать нечего)) кто и для кого писал это мне не понятно.
Сегодня вышло видео с моим участием — youtu.be/1p8jx5_oKq0 Тут более подробно рассказываю о своей работе

Вы перепутали Selenide с Selenoid.

Это опечатка :) Спасибо, что заметили!

это казино

С тем же успехом можешь похвастать как торгуешь опиатами через сеть закладчиков

Типа что-то умное сказал?

Типа умный, а все остальные?

Топ, всьо по поличках
Пару пропозицій ще на пробу:
Спробуйте замість/в додачу підхід API first для контрактних тестів, для опису комунікації сервісів
Та рест ашуред в сирому залишку може бути не достатньо для покриття тестами бекенду, із-за свого примітивного синтаксису, хоча залежить від того як ви його юзаєте

Спасибо, Роман) На текущий момент тест Ашуред полностью покрывает потребности в глубине тестирования, а api first в нашем случае будет избыточен.

Та рест ашуред в сирому залишку може бути не достатньо для покриття тестами бекенду, із-за свого примітивного синтаксису

можете розказати, в яких випадках його недостатньо?
і чим доповнити/замінити

Если разбираться, в каких случаях REST Assured будет недостаточно. Для начала вспомним, что данная библиотека умеет делать только две вещи: посылать запросы и проверять ответы. Если мы пытаемся вынести ее из тестов и убрать всю валидацию, то она превращается в http клиент. Если вам предстоит написать большое количество тестов и в дальнейшем их поддерживать, целесообразность использования http клиента с громоздким синтаксисом, статической конфигурацией, путаницей в порядке применения фильтров и спецификаций и логированием, которое можно легко сломать, лучше использовать такие альтернативы как Retrofit, Feign или Unirest

Ну не знаю, я Retrofit не юзав, так поверхнево почитав і ознайомився, то виглядає значно напряжніше, ніж REST Assured.
За інші клієнти навіть не чув

Подписаться на комментарии