В мире разработки ПО поддержка высокого уровня наблюдаемости (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 оправдал себя гибкостью и опцией масштабирования.