
Всем привет! На связи Николай Мезинов из команды Тинькофф. Предлагаю разобрать интересную задачу — тестирование приложений с помощью микросервиса. Прочитав предыдущее предложение, можно подумать: «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; };
Кратко повторим, что мы сделали:
-
Написали тестовый сценарий аутентификации на playwright.
-
В процессе тестового сценария запоминали, сколько времени длился каждый этап.
-
Сделали сценарий аутентификации в виде воркера, чтобы запускать отдельно от основного треда.
-
В основном треде с помощью шедулера вызывали воркер, ждали от него результатов по тестовому сценарию.
-
После получения результатов обновляли 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.
Заключение
Мы рассмотрели тестирование с помощью микросервиса, и оказалось, что Microservice for testing — это совсем не страшно. На выходе получаем много преимуществ, которых не даст классическое e2e-тестирование.
Делитесь опытом и задавайте вопросы в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/757618/
Добавить комментарий