В мире разработки ПО поддержка высокого уровня наблюдаемости (observability) для приложений с архитектурой, управляемой событиями (event-driven architecture, EDA), – критически важный аспект для качественной работы системы. Суть в том, что сложность таких систем, связанных с обработкой огромных объемов данных в режиме реального времени, требует надежных инструментов для мониторинга, отладки и анализа. Однако традиционные методы, использующие логи и метрики, часто оказываются недостаточными, когда необходимо глубоко понять взаимодействие между различными компонентами системы и выявить узкие места.
Именно с этой проблемой мы столкнулись в нашей команде, поэтому я, Дмитрий Титаренко (QA-инженер в компании TAGES), решил поделится найденным решением в статье на Хабр. Надеюсь, что будет полезно!
Введение
Первым делом стоит разобраться, что же такое Observability, и почему мы сочли это важным в рамках EDA-системы.
Observability — это способность системы давать полное представление о своем внутреннем состоянии на основе данных, полученных от самой системы. В контексте микросервисной архитектуры и, особенно, архитектуры, управляемой событиями, observability включает три ключевых компонента:
-
Логи — текстовые записи о работе системы, содержащие данные о событиях, ошибках и других значимых моментах.
-
Метрики — числовые данные, которые позволяют отслеживать производительность и состояние системы в реальном времени.
-
Трейсы — информация о путях запросов и событий через различные компоненты системы.
Специфика EDA
В EDA взаимодействие между компонентами осуществляется через асинхронные сообщения (события). Это порождает следующие сложности:
-
Асинхронность — трудности в отслеживании последовательности событий и их зависимости.
-
Динамичность — частые изменения в конфигурации системы и топологии взаимодействий между микросервисами.
-
Разнообразие событий — различные типы и форматы сообщений, что усложняет их мониторинг и анализ.
Роль Observability
Представим, что у нас есть система из 16 микросервисов, каждый из которых обрабатывает свои специфические события. Взаимодействие между этими сервисами может происходить через брокеры сообщений, такие как Kafka или RabbitMQ.
Наши микросервисы:
-
Пользовательский интерфейс
-
Сервис обработки платежей
-
Сервис проверки мошенничества
-
Сервис уведомлений
-
Сервис отчетности
-
И др.
Условная покупка будет совершаться в рамках следующего процесса:
Шаг 1: Пользовательский интерфейс отправляет событие «OrderPlaced».
Шаг 2: Сервис обработки платежей получает событие «OrderPlaced» и обрабатывает платеж.
Шаг 3: Сервис проверки мошенничества получает событие «PaymentProcessed» и выполняет проверку.
Шаг 4: Сервис уведомлений получает событие «FraudCheckCompleted» и отправляет уведомление пользователю.
Шаг 5: Сервис отчетности получает все события и обновляет отчеты.
В описанном процессе нам помогает Observability. Делается это за счет логов, метрик и трейсов.
Логи |
Централизованное логирование: cбор всех логов в одном месте для упрощения анализа. Например, собираем в OpenTelemetry и ходим смотреть в Jaeger. Корреляция событий: использование уникальных идентификаторов (trace IDs) для отслеживания цепочки событий. |
Метрики |
Мониторинг производительности: Сбор метрик, таких как время обработки платежей, частота событий «OrderPlaced» и т.д. Анализ узких мест: Выявление микросервисов, которые могут быть узким местом в системе, путем анализа времени их ответа. |
Трейсы |
Детализированные трейсы: Визуализация пути события через все микросервисы, от «OrderPlaced» до «FraudCheckCompleted». Отладка проблем: Легкость в выявлении проблемных точек и задержек на различных этапах обработки событий. |
Критическая важность Observability
Как можно было заметить из ранее приведенного примера, Observability в EDA является критически важным элементом сразу по нескольким причинам:
-
Повышение надежности: возможность быстрого выявления и устранения проблем.
-
Улучшение производительности: выявление и устранение узких мест.
-
Гибкость и масштабируемость: легкость в адаптации к изменениям и масштабированию системы.
-
Снижение времени на отладку: быстрое нахождение причин ошибок и сбоев.
Observability в контексте EDA обеспечивает полную прозрачность системы. Это особенно актуально для сложных систем с множеством микросервисов, где любое узкое место или проблема могут значительно влиять на общую производительность и надежность системы.
Наша система
У нас было 16 микросервисов, связанных между собой Kafka, монолитная база данных и свои целевые базы для каждого микросервиса (PostgreSQL). Логирование велось на основе OpenTelemetry, с использованием Jaeger. Имелись автотесты на Jest и Playwright, и перед релизами мы локально запускали тесты с помощью k6.
Проблема
Нашей целью было увеличение скорости доставки фич. Здесь все просто – чем быстрее компания создаёт и запускает продукт, тем быстрее начинает извлекать из него выгоду. Для этого нам было необходимо получать метрики о работоспособности нашей релизной сборки (или нет, а именно при нахождении багов максимально точно локализовать место ошибки для скорейшего его исправления), чтобы сократить скорость регрессионного тестирования.
Для решения этой задачи, конечно, можно было бы написать огромное количество тестов с помощью связки Jest + Allure и Playwright (больше тестов Богу тестов), но нам требовалось менее ресурсозатратное решение. В дополнение к этому, необходимо было учитывать, что у нас уже существуют некоторые тесты, которые не хотелось оставлять без внимания.
Тогда мы решили изучить, как тестируют коллеги на мировом рынке, а также какие существуют «best practices». К нашему удивлению оказалось, что готовых решений практически нет (будем рады, если поделитесь своим опытом в комментариях).
Решение
На просторах интернета обнаружился относительно новый open-source инструмент, под названием Tracetest.io. Нашим задачам он подошел идеально.
Тут и возможность использовать существующие тесты (Cypress, Playwright, k6, Postman и другие), и просматривать весь процесс от web до backend через трассировку, захваченную при каждом запуске теста, и охватить всю систему одним набором тестов.
Время создания тестов согласно, их презентации сокращалось на 98%: с 12 часов до 15 минут. Мы сразу понимали, что таких чудес ждать не стоит, но обойти стороной не смогли.
Так и началось увлекательное приключение на 20 минут под названием «R&D».
Внедрение Tracetest
Что же за зверь этот ваш Tracetest?
С учетом масштаба системы, о котором мы писали ранее, ручное тестирование потребовало бы вовлеченность всего отдела. Разумеется, такой сценарий нам неудобен. Поэтому мы и выбрали Tracetest — инструмент с открытым исходным кодом, который автоматизирует тестирование и наблюдение за микросервисами, используя трейсы из OpenTelemetry. Благодаря этому можно быстро покрывать тестами все микросервисы, автоматизируя рутинные задачи.
Для внедрения инструмента нам понадобились всего два QA-инженера, а поддерживать данные тесты смогли специалисты, занимающиеся преимущественно ручным тестированием (после небольшого онбординга).
Можно предположить, что у многих после прочтения этой части сразу возникла мысль: «Неужели все настолько просто?!».
Давайте разберем процесс подробнее.
Первым делом мы выгрузили логи по всем микросервисам за предыдущий день. Они представляли собой JSON, хранящийся в OpenSearch, что позволило нам работать с их ключами и значениями. На основе всего этого был написан скрипт для генерации тестов в формате YAML для дальнейшего их использования в Tracetest.
Подробно останавливаться на этой теме мы не будем, так как для этого понадобилась бы отдельная статья. Однако наглядный пример вы можете увидеть в официальной документации.
Пример теста
Ниже мы представим пример самой сущности Test в Tracetest вместе с теми проверками (Testspecs), которые планировали охватить:
type: Test
spec:
name: DEMO Import - Import an Entity
description: "Import an entity"
trigger:
type: http
httpRequest:
url: http://demo-api.demo/entity/import
method: POST
headers:
- key: Content-Type
- value: application/json
body: '{ "id": 52 }'
specs:
- selector: span[name = "POST /entity/import"]
assertions:
- attr:tracetest.span.duration <= 500ms
- attr:http.status_code = 200
- selector: span[name = "send message to queue"]
assertions:
- attr:messaging.message.payload contains 52
- selector: span[name = "consume message from queue"]:last
assertions:
- attr:messaging.message.payload contains 52
- selector: span[name = "consume message from queue"]:last span[name = "import entity from externalapi"]
assertions:
- attr:http.status_code = 200
- selector: span[name = "consume message from queue"]:last span[name = "save entity
on database"]
assertions:
- attr:db.repository.operation = "create"
- attr:tracetest.span.duration <= 500ms
outputs:
- name: ENTITY_ID
selector: span[name = "POST /entity/import"]
value: attr:http.response.body | json_path '.id'
Мы сгенерировали файлы с тестами, а затем интегрировали их запуск в CI/CD для всех 16 микросервисов (а это больше 500 тестов с более чем 20 тысячами проверок). Возможно, в тексте это выглядит пугающе, однако все не так страшно, поскольку Tracetest легко интегрируется в пайплайны GitLab, что упрощает работу.
Наши проверки были разбиты на четыре категории:
-
Запросы в SQL (текст и тип запроса)
-
REST-запросы (headers, body и status)
-
Все свитчи (true/false)
-
Прочее
Все это было здорово, однако, у нас также была задача интеграции с Jest. Конечно же, нативной интеграции у Tracetest с Jest не оказалось….
Нам нужен был какой-то API для взаимодействия между Tracetest и Jest.
Интеграция с Jest
Мы пришли к тому, что нам требуется забирать x-request-id из уже вызванных запросов, после чего находить по нему нужный трейс в Jaeger и уже там парсить trace-id, выступающего триггером в Tracetest. Реализовано это было в виде кастомного матчера, «под капотом» которого все и происходило. Таким образом, мы успешно встроили тесты Tracetest в виде дополнительных проверок в уже готовые тесты Jest.
Теперь о том, что именно происходило «под капотом»:
С помощью swagger-typescript-api мы сгенерировали набор API (JS-классы с методами нашего API и все интерфейсы, которые мы используем) из документации OpenAPI 3.0 Swagger. Затем подружили Jest и Tracetest с помощью Axios.
Теперь у нас запускались интеграционные тесты Jest, внутри которых были исключения (exceptions) с нашим кастомным матчером. В итоге, при успешной проверке Tracetest, Jest стал выводить статус «passed», а в случае ошибки — «failed» с последующим парсингом ошибки с местом проблемы в Tracetest.
Заключение
Почему же мы выбрали именно Tracetest:
-
Open Source решение. Обеспечивает технологическую независимость разработки, отсутствие обязательной платы за право использовать продукт, возможность вносить запросы на фичи для Tracetest через issue на Github.
-
Основывается на международных стандартах (например, таких как OpenTelemetry, что ценно в нашем сценарии).
-
Наличие интеграций с инструментами, которые мы уже используем в рамках проекта (Playwright, k6).
-
Отсутствие подходящих альтернатив.
-
Это свежий продукт, в котором не придётся разбираться с устаревшими модулями и интеграциями.
Внедрение Tracetest помогло нам значительно улучшить наблюдаемость и автоматизировать процесс тестирования. Было сокращено время регрессионного тестирования и увеличена скорость доставки новых фич. Если раньше мы выпускали 1 релиз раз месяц (а то и дольше), то спустя полгода смогли наладить поставку 4 релиз-кандидатов в прод за месяц (в среднем мы могли тестировать по 2 релиз-кандидата за двухнедельный спринт). Tracetest оправдал себя гибкостью и опцией масштабирования.
Автор: Dimonioi4