«Несколько лет назад Uber для перехвата регрессионных багов в основном полагался на инкрементное развертывание и оповещения на проде. Такой подход поначалу работал, но стал очень затратным с операционной точки зрения, а также провоцировал большое количество утечек. Наличие проблем на поздних стадиях разработки заставляло разработчиков искать и изолировать точное место ошибки, а затем проходить весь процесс заново.
Рисунок 1. Выявление проблем как можно раньше (т. е. смещение влево) снижает операционную нагрузку.
Как показали разборы по результатам инцидентов, во многих случаях тестирование практически не проводилось, помимо нескольких базовых юнит-тестов, которые часто были настолько зависимы от моков, что было трудно понять, насколько они надежны.
Uber известен тем, что полностью внедрил микросервисную архитектуру. Основная бизнес-логика инкапсулирована в больших группах микросервисов, поэтому сложно проверить функциональность одного сервиса без чрезмерного мокинга. В этой архитектуре единственным разумным методом тестирования было сквозное. В то же время внутренние опросы сотрудников постоянно демонстрировали, что сквозное тестирование считалось самой сложной задачей для разработчиков — неудивительно, что они ее нередко не выполняли.
В известном блоге Google говорится, что E2E-тесты сложные, нестабильные, медленные и трудные в отладке. На своем пути в тестировании Uber столкнулась со всеми вышеперечисленными проблемами, и пришлось придумывать креативные подходы к их решению.
Далее мы рассказываем о том, как построили систему, управляющую каждым изменением кода и конфигурации основных бэкэнд-систем (1000+ сервисов). У нас несколько тысяч E2E-тестов, средний процент прохождения которых составляет 90%+. Представьте, что каждый из этих тестов проходит через реальный пользовательский E2E-поток, например, групповой заказ Uber Eats.
Прежние подходы
Docker Compose
В прошлом некоторые команды Uber пытались локально запускать все связанные сервисы с данными из хранилищ, чтобы выполнить на этом стеке набор интеграционных тестов. На определенном этапе сложности микросервисов этот подход становится слишком дорогим из-за количества запускаемых контейнеров.
Рисунок 2. Docker Compose работает на начальном этапе, но не справляется с масштабированием по мере роста количества микросервисов.
Deploy to staging
Рисунок 3: После того как были достигнуты пределы docker-compose на одной машине, некоторые разработчики перешли к развертыванию кода в общей staging- или testing-среде.
Общий стейджинг для тестов стало слишком сложно поддерживать в надежном состоянии, потому что один разработчик, развернувший неудачное изменение, мог сломать всю систему для всех остальных. Такой подход работал только в небольших доменах, где разработчики могли вручную координировать развертывание нескольких команд.
Могут ли сквозные тесты работать в масштабах Uber? Это история о том, как мы добились этого.
Решение: сдвиг влево с помощью BITS
BITS — Стратегия интеграционного тестирования бэкэнда
Чтобы сдвинуть тестирование влево, необходимо обеспечить тестирование изменений без развертывания на проде. Чтобы решить эту проблему, мы запустили общекорпоративную инициативу BITS (Стратегия интеграционного тестирования бэкэнда), которая обеспечивает развертывание по требованию и маршрутизацию кода в песочницы. На практике это означает, что отдельные коммиты могут быть протестированы параллельно, не мешая другим.
Изоляция данных
Чтобы безопасно сдвинуться влево, нужно было обеспечить изоляцию production и тестового трафика. Ожидалось, что production-сервисы будут получать как тестовый, так и production-трафик.
Контексты
Большинство API-функций масштабируются по сущностям, что означает, что доступ к ним ограничивается конкретной сущностью (например, учетной записью пользователя). Использование тестовых пользователей автоматически ограничивает область побочных эффектов этой учетной записью. Небольшое количество межсубъектных взаимодействий (например, алгоритмический подбор или покупки в Eats) используют следующие стратегии изоляции:
Рисунок 4. Запросы отмечаются идентификаторами «аренда», а сервисы обычно следуют одному из описанных выше подходов:
- Клиенты хранилища направляют тестовый трафик на логически разделенное хранилище данных
- Данные сохраняются в БД на проде в колонку tenancy; диапазонные запросы также передают идентификатор аренды для фильтрации
Архитектура
Архитектуру BITS можно описать как серию рабочих процессов, которые взаимодействуют с различными компонентами инфраструктуры, повышая пользу для разработчика. Мы используем Cadence, движок рабочих процессов с открытым исходным кодом, разработанный в Uber, который предоставляет модель программирования повторных попыток, состояний рабочего процесса и таймеров очистки ресурсов.
Наши CI-процессы запускают алгоритмы выбора тестов и сервисов, создают и развертывают тестовые песочницы, планируют тесты, анализируют результаты и предоставляют отчеты. Процессы обеспечения управляют ресурсами контейнеров и планированием нагрузки в очередях CI, оптимизированных для определенных типов нагрузок (например, сильное использование сети или процессора).
Рисунок 5: Архитектурные компоненты BITS
*Мы уже рассказывали о SLATE, проекте на основе инфраструктурных примитивов BITS для ручного санити-тестирования.
Изоляция инфраструктуры
Мы очень старались изолировать dev-контейнеры BITS от побочных эффектов на проде:
- Тестовые песочницы BITS не получают production-трафика.
- Нативный протокол Apache Kafka™ в Uber абстрагируется с помощью consumer proxy, который обрабатывает сообщения по модели push-пересылки через gRPC. Такой дизайн обеспечивает продуманную маршрутизацию сообщений в тестовые песочницы BITS, а также предотвращает случайный опрос production-топиков в песочницах.
- Workflow Cadence, созданные в процессе тестирования, изолированы от тестовой песочницы.
- Метрики и логи помечаются меткой tenancy для фильтрации.
- Тестовые песочницы не участвуют в production-протоколах выбора лидера.
- SPIFFE™ и SPIRE™ обеспечивают надежную идентификацию песочниц и тестового трафика.
- Прокси-переадресатор проверяет запросы и выполняет P2P-перенаправление по условию, при обнаружении переопределений маршрутизации.
Рисунок 6: Мы кодируем заголовки переопределения маршрутизации как контекстный багаж и внедряем протокол OpenTelemetry™ в промежуточные модули RPC и клиентов для распространения между микросервисами.
Тестирование изменений конфигурации
В Uber изменения конфигурации являются причиной до 30 % инцидентов. BITS также обеспечивает тестовое покрытие для развертывания конфигураций в нашей крупнейшей внутренней системе управления конфигурациями, Flipr.
Рисунок 7: Как изменения конфигурации тестируются с помощью BITS.
Управление тестированием
Разработчики управляют своими наборами в режиме реального времени из кастомного интерфейса, выполняя такие действия как пропуск отдельных тестов, отслеживание состояний и контроль покрытия конечных точек своих сервисов.
Рисунок 8: Управление тестами в интерфейсе BITS.
Трассировка
Каждое выполнение теста подвергается принудительной выборке с помощью Jaeger™. После каждого выполнения теста мы фиксируем «след» и используем для построения следующих индексов:
- Какие сервисы и конечные точки были охвачены тестовым потоком
- Какие тесты охватывают каждый сервис и конечную точку
Рисунок 9: Как индекс трассировки используется для настройки тестов.
Эти индексы используются для сбора показателей покрытия конечных точек командами, но что более важно, они позволяют четче определять, какие тесты нужно запускать при изменении данного сервиса.
Рисунок 10: Индекс отслеживания контролирует покрытие конечных точек.
Правильная организация инфраструктуры всегда была самой легкой проблемой тестирования. Проблемы с оборудованием легко решаемы, но сотрудники в Uber очень чувствительны к снижению эффективности и удобства. Ниже описаны некоторые наши стратегии по снижению проблем с обслуживанием тестов и их надежностью.
Челленджи
Проблема №1: Статусы
Допустим, нужно протестить опережающую диспетчеризацию — когда водитель получает запрос нового пассажира до того как высадит предыдущего. Или совместные поездки нескольких пассажиров. Как вы моделировать статусы в поездке в этих ситуациях?
Composable Testing Framework (CTF)
Мы создали CTF, составной тестовый фреймворк, DSL на уровне кода, для моделирования пользовательских действий/потоков и оценки их влияния на главные статусы приложения. CTF первоначально разрабатывался для валидации нашей Fulfillment Re-architecture, а затем был распространен на всю компанию.
Рисунок 11: Как CTF описывает тест потока.
Каталогизация тест-кейсов
Каждый тест-кейс автоматически регистрируется в хранилище данных на месте. Аналитика по историческим показателям прохождения, распространенным причинам неудач и информация о владельцах доступна разработчикам, которые могут отслеживать стабильность своих тестов с течением времени. Похожие случаи агрегируются.
Рисунок 12: Как мы группируем ошибки в тестах.
Проблема №2: Надежность и скорость
Ранее показатель прохождения тестов был около ≥90%, платформа CTF повысила этот показатель до 99,9 %, повторными выполнениями.
В нашем крупнейшем сервисе 300 тестов. Если они по отдельности проходят на 90% на попытку, то есть 26% вероятность того, что один рендомный тест будет нестабилен. В одном случае у нас было 300 тестов с 95% проходимостью, и с помощью повторных попыток (ретраев) увеличили показатель до ≥99 %.
Вышеописанное делаем тысячи раз в день, опровергая заблуждение о том, что сквозные тесты нельзя сделать стабильными.
Латентность
Тесты работают с API Uber напрямую, поэтому большая часть e2e-тестов выполняются менее минуты.
Проблема № 3: Отладка и пригодность
Плацебо-тест
При каждом запуске теста мы запускаем параллельный «плацебо»-тест в песочнице с последней версией кода из main-бранча. Сравнение результатов этих двух тестов позволяет выйти за рамки двоичных сигналов и перейти к таблице истинности:
Рисунок 13: Таблица истинности, полученная от плацебо-теста в рамках BITS-подхода.
Повторным выполнением можно снизить количество недетерминистичных отказов. Либо ваш прод поломан, либо ваш тест плохой, либо у вас в коде реальный баг. Все три варианта предполагают четкие действия.
Автоматический карантин
Процент прохождения наших тестов имеет бимодальное распределение —
- Подавляющее большинство тестов очень надежны.
- Небольшое подмножество тестов постоянно падает.
- Еще меньшее подмножество тестов нестабильные (flaky).
Рисунок 14: Распределение статусов прохождения E2E-тестов в Uber.
Один неработающий тест может заблокировать CI/CD, поэтому мы уделяем особое внимание защите от подобного. Как только уровень прохождения тестов опускается ниже 90% по сравнению с плацебо-запуском, тест автоматически помечается как неблокирующий, а на команду-владельца автоматически подается тикет и предупреждение. Основные тесты пользовательского потока и комплаенса исключены из вышеописанного, учитывая их важность для бизнеса.
Заключение
Тестирование требует баланса между качеством и скоростью. При хорошо отлаженных процессах разработчики тратят меньше времени на откат деплоя и устранение последствий инцидентов. Повышается и скорость, и качество. Мы отслеживали показатель количества инцидентов на 1000 диффов и сократили его на 71% в 2023 г.
Тестирование — это проблема людей в той же степени, что и техническая проблема
Микросервисная архитектура родилась из желания расширить полномочия команд в больших компаниях. В идеальном мире мы бы повсеместно использовали чистые абстракции API, укрепляя эту модель. Команды могли бы билдить и тестировать, не мешая другим командам. В реальности доставка большинства фич в Uber требует интенсивной совместной работы. Поэтому сквозное тестирование является контрактом, обеспечивающим связь между независимыми командами. Связь неизбежно нарушается, потому что мы люди.
Тестирование и архитектура неразрывно связаны
Никто в Uber намеренно не планировал сильно полагаться на сквозное тестирование, но наша архитектура с большим количеством микросервисов и исторические инвестиции сделали E2E решением лучшим из возможных.
Попытки подогнать стратегию тестирования под классическую пирамиду не работали в Uber. Хорошее тестирование требует критического взгляда на то, что вы пытаетесь валидировать. Вы должны пытаться протестировать наименьший набор модулей, отвечающих за какую-то важную пользовательскую функциональность. А в Uber это часто десятки сервисов.»