Microservice for testing — are you kidding me?

—

от автора

Всем привет! На связи Николай Мезинов из команды Тинькофф. Предлагаю разобрать интересную задачу — тестирование приложений с помощью микросервиса. Прочитав предыдущее предложение, можно подумать: «Microservice for testing? Are you kidding me?» И я с ухмылкой отвечу: «No, I am not».

Немного контекста

Наша команда техническая, мы делаем внутреннюю платформу разработки @dmit8815:

Мы придерживаемся принципа you build it, you test it, you run it. Мы верим, что этот принцип позволяет снизить размытие ответственности между разными инженерами в команде, сфокусировав ответственность за какую-то фичу в руках одного инженера. Как следствие, у нас нет четкого разделения на разработчиков, тестировщиков, DevOps. 

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

Какую задачу решаем

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

«пробер». Для полноты контекста обозначим, что это такое для нашего конкретного случая:

Пробер — серверное приложение, которое прогоняет тестовый сценарий с некоторым интервалом времени

Пробер аутентификации — серверное приложение, написанное на Node.js. Оно с некоторым интервалом времени запускает headless-браузер, который открывает веб-страницу и прогоняет тестовый сценарий, попутно снимая метрики

Преимущества выбранного подхода:

  • позволяет мониторить любые метрики в реальном времени, при этом тестируемая система также представляет собой black box;

  • можно снимать и мониторить любые метрики в режиме реального времени.

Идея с пробером покрывает все наши требования. На нем и остановимся.

Довольно рассуждений — покажите код

Мы начали с разработки тестового сценария. Условно говоря, пользователь зашел на страницу аутентификации, ввел логин, пароль и успешно зашел на сайт. В качестве фреймворка для написания тестового сценария выбрали playwright. 

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

// check-auth.ts /**  * Исполняет сценарий аутентификации через headless-браузер  */ import { parentPort, workerData } from 'worker_threads'; import { chromium } from 'playwright';   const authenticate = async ({   webAppUrl,   userName,   userPassword,   timeout, }: WorkerParams): Promise<Durations> => {   const browser = await chromium.launch({     headless: true,     args: [       '--no-sandbox',       '--disable-dev-shm-usage',       '--disable-setuid-sandbox',       '--window-size=1920,1080',     ],   });   try {     const page = await browser.newPage(); // Открываем браузер     await page.setDefaultTimeout(timeout);     await page.setDefaultNavigationTimeout(timeout);     // Не приводим исходный код класса AuthTimeDutrations, чтобы не отвлекать от сути. Инстанс класса умеет засекать время прохождения каждого этапа аутентификации     const authSteps = new AuthTimeDurations();     const stopTotalAuthDuration = authSteps.startOverallAuthDuration();     await page.goto(webAppUrl); // Переходим на страницу аутентификации     const loginButton = await page.locator('text=Войти через IdentityProvider');     authSteps.markStep('Devplatform page loaded', DevplatformAuthPageLoadedLabel); // Помечаем пройденный этап, засекаем время     await loginButton.click({ force: true });     const identityProviderTotalDurationStop = authSteps.startIdentityProviderTotalDuration();     const userNameInput = await page.locator('#userNameInput');     authSteps.markStep('IndentityProvider page opened and loaded', IdentityProviderPageLoadedLabel); // Помечаем пройденный этап, засекаем время     await userNameInput.type(userName); // Вводим логин     await page.type('#passwordInput', userPassword); // Вводим пароль     // Кликаем по кнопке сабмита     await Promise.all([       page.waitForNavigation({ waitUntil: 'commit' }),       page.click('#submitButton'),     ]);     identityProviderTotalDurationStop();     authSteps.markStep('Success identityProvider processed and redirected to devplatform', IdentityProviderProceedLabel);       await page.waitForSelector('text=Выбрать проект');     stopTotalAuthDuration();     await browser.close();     return authSteps.getStageDurations();   } catch (e) {     await browser.close();     throw e;   } };   // Отсылаем из воркера сообщение о пройденной аутентификации authenticate(workerData).then((data) => {   parentPort?.postMessage(data); });

Мы договорились в самом начале, что нам необходимо не только знать, была ли аутентификация успешной, но и снимать кастомные prometheus-метрики. Prometheus-метрики будем обновлять в основном треде. Запуск тестового сценария будем планировать с помощью самописного шедулера. 

Логичный вопрос: «Зачем писать собственный шедулер, если есть множество готовых решений?» Причина в том, что существующие шедулеры в основном поддерживают cron-синтаксис, но не учитывают контекст предыдущего запуска — следующий запуск возможен только после окончания предыдущего. Приведем основные фрагменты исходного кода.

Начнем с воркера, который запускает тестовый сценарий, описанный выше:

import { Worker } from 'worker_threads';  // Запускает воркер и ждет от него результатов function runProbe(params: WorkerParams): Promise<any> {   return new Promise((resolve, reject) => {     // Запуск воркера, рассмотренного на предыдущем этапе     const worker = new Worker('./build/check-auth.js', {       workerData: params,       // Опция нужна, чтобы при ошибках в асинхронных функциях мы получали корректное сообщение об ошибке и событие error       execArgv: [...process.execArgv, '--unhandled-rejections=strict'],     });     worker.on('message', resolve);     worker.on('error', reject);     worker.on('exit', (code: number) => {       if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));     });   }); }

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

// Шедулер, который запускает проверку аутентификации с некоторым интервалом времени и учитывает предыдущий запуск const schedule = ({   job,   timeoutInSeconds,   onError, }: {   job: () => Promise<void>;   timeoutInSeconds: number;   onError: (e: Error) => void; }) => {   let timerId: NodeJS.Timeout;   let currentPromise = Promise.resolve();   let stopped = false;   function tick() {     currentPromise = Promise.resolve()       .then(job)       .catch(onError)       .finally(() => {         // Возможно, что остановили, когда сценарий уже прошел, но еще не стартовал, — поэтому проверяем переменную и НЕ запускаем по расписанию         if (stopped) return;         timerId = setTimeout(tick, timeoutInSeconds * 1000);       });   }   return {     start: () => {       tick();     },     stop: () => {       stopped = true;       clearTimeout(timerId);       return currentPromise;     },   }; };

И в конце нам потребуется основная функция, которая запускает тестовый сценарий с помощью кастомного шедулера, ждет от него результатов и обновляет prometheus-метрики:

// Основная функция, которая запускает тестовый сценарий с помощью шедулера, ждет результатов и обновляет prometheus-метрики const authCheckerScheduler = ({   userPassword,   userName,   webAppUrl,   probeIntervalInSeconds,   timeout,   proxy, }: Params) => {   const scheduler = schedule({     job: () => {       // Запустили воркер и ждем от него результатов — сколько времени выполнялся каждый шаг при аутентификации       return runProbe({         userPassword,         userName,         webAppUrl,         timeout,         proxy,       }).then((stageDurations: Durations) => {         // После получения информации о том, сколько выполнялся каждый этап аутентификации, обновляем prometheus-метрики в нужном формате         stageDurations.metrics.forEach((m) => {           const duration = m.time / 1000;           authStageDurationMetric.labels(m.prometheusLabel).observe(duration);           labelToMetric(m.prometheusLabel).set(duration);         });           const authWithoutIdentityProviderInSeconds =           (stageDurations.authTotalDuration - stageDurations.identityProviderDuration) / 1000;         const authTotalDurationInSeconds = stageDurations.authTotalDuration / 1000;         authSuccessTimingWithoutIdentityProviderMetricOverall.observe(authWithoutIdentityProviderInSeconds);         authSuccessTimingWithoutIdentityProviderMetricCurrent.set(authWithoutIdentityProviderInSeconds);         authSuccessTimingMetricOverall.observe(authTotalDurationInSeconds);         authSuccessTimingMetricCurrent.set(authTotalDurationInSeconds);         authSuccessMetricTotalNumber.inc();         lastAuthSuccessStateMetric.set(lastAuthStateMetricValue.success);         return undefined;       });     },     timeoutInSeconds: probeIntervalInSeconds,     onError: (err) => { // в случае ошибки помечаем в prometheus, что аутентификация была неуспешной       lastAuthSuccessStateMetric.set(lastAuthStateMetricValue.fail);       logger.error(err);     },   });     return scheduler; };

Кратко повторим, что мы сделали:

  1. Написали тестовый сценарий аутентификации на playwright.

  2. В процессе тестового сценария запоминали, сколько времени длился каждый этап.

  3. Сделали сценарий аутентификации в виде воркера, чтобы запускать отдельно от основного треда.

  4. В основном треде с помощью шедулера вызывали воркер, ждали от него результатов по тестовому сценарию.

  5. После получения результатов обновляли prometheus-метрики.

Посмотрим, что же отдает нам теперь эндпоинт /metrics:

... # HELP sla_devplatform_authentication_successful total success auth probe number # TYPE sla_devplatform_authentication_successful counter sla_devplatform_authentication_successful 4   # HELP sla_devplatform_authentication_seconds overall success auth probe duration # TYPE sla_devplatform_authentication_seconds histogram sla_devplatform_authentication_seconds_bucket{le="0.005"} 0 sla_devplatform_authentication_seconds_bucket{le="0.01"} 0 sla_devplatform_authentication_seconds_bucket{le="0.025"} 0 sla_devplatform_authentication_seconds_bucket{le="0.05"} 0 sla_devplatform_authentication_seconds_bucket{le="0.1"} 0 sla_devplatform_authentication_seconds_bucket{le="0.25"} 0 sla_devplatform_authentication_seconds_bucket{le="0.5"} 0 sla_devplatform_authentication_seconds_bucket{le="1"} 0 sla_devplatform_authentication_seconds_bucket{le="2.5"} 4 sla_devplatform_authentication_seconds_bucket{le="5"} 4 sla_devplatform_authentication_seconds_bucket{le="10"} 4 sla_devplatform_authentication_seconds_bucket{le="+Inf"} 4 sla_devplatform_authentication_seconds_sum 8.681718063999899 sla_devplatform_authentication_seconds_count 4   # HELP sla_devplatform_authentication_seconds_current last success auth probe duration # TYPE sla_devplatform_authentication_seconds_current gauge sla_devplatform_authentication_seconds_current 2.1950614569997415   # HELP sla_devplatform_authentication_last_successful last success auth probe state, 1 - successful, 0 - failed # TYPE sla_devplatform_authentication_last_successful gauge sla_devplatform_authentication_last_successful 1   # HELP sla_devplatform_authentication_without_identity_provider_seconds overall success auth probe duration without identity_provider # TYPE sla_devplatform_authentication_without_identity_provider_seconds histogram sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.005"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.01"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.025"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.05"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.1"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.25"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="0.5"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="1"} 0 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="2.5"} 4 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="5"} 4 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="10"} 4 sla_devplatform_authentication_without_identity_provider_seconds_bucket{le="+Inf"} 4 sla_devplatform_authentication_without_identity_provider_seconds_sum 6.257014720998704 sla_devplatform_authentication_without_identity_provider_seconds_count 4   # HELP sla_devplatform_authentication_without_identity_provider_seconds_current last success auth without identity_provider probe duration # TYPE sla_devplatform_authentication_without_identity_provider_seconds_current gauge sla_devplatform_authentication_without_identity_provider_seconds_current 1.6220274379989132 … 

Все наши кастомные метрики экспортируются в /metrics, выходим на финишную прямую. Осталось лишь визуализировать результаты.

Наглядность, больше наглядности

Визуализировать время прохождения аутентификации будем в Grafana. Для построения графиков воспользуемся языком PromQL. Наиболее интересная метрика — время аутентификации. Нас интересует как полное время аутентификации, так и время без корпоративного Identity-провайдера, который является для нас внешней системой. Запросы имеют вид:

sla_devplatform_authentication_seconds_current{env="$env", group=~"devplatform|spirit-probers"} sla_devplatform_authentication_without_identity_provider_seconds_current{env="$env", group=~"devplatform|spirit-probers"}

Введем эти запросы в Grafana и увидим четыре графика, так как у нас работают две реплики в k8s.

Полное время аутентификации меньше 3 секунд — приемлемо

Полное время аутентификации меньше 3 секунд — приемлемо

Заключение

Мы рассмотрели тестирование с помощью микросервиса, и оказалось, что Microservice for testing — это совсем не страшно. На выходе получаем много преимуществ, которых не даст классическое e2e-тестирование.

Делитесь опытом и задавайте вопросы в комментариях!


ссылка на оригинал статьи https://habr.com/ru/articles/757618/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *