Сказ о том, как мы нагружаем Ozon в мультиЦОД-архитектуре

Привет, я Таня, и наша команда занимается разработкой инфраструктуры для нагрузочного тестирования (НТ) в Ozon. Наша цель — предоставить разработчикам простой и понятный инструмент для подготовки и самостоятельного запуска нагрузочных тестов — можно сказать, нагрузочное тестирование as a service. У нас НТ широко распространено и поставлено на поток — большинство продуктовых сервисов регулярно тестируется по расписанию, в автоматическом режиме. Кстати, подавляющая часть тестов проводится не на тестовых стендах, а прямо в продакшене. Это связано с определёнными рисками, ведь есть ещё и реальный пользовательский трафик. Обложившись алертами и автостопами (критериями для автоматической остановки тестов), мы сводим эти риски к минимуму.  

Компания растёт, увеличивается число пользователей и сервисов. В один прекрасный день нам стало тесно в рамках одного дата-центра — началось масштабное расширение на три ЦОДа. Каждый сервис обзавёлся дополнительными инстансами — и новыми требованиями к нагрузке. У НТ-разработчиков появилась задача тестировать сервисы, разбросанные по разным ЦОДам, и при этом ничего не уронить (мы ребята высоконагруженные). Кроме того, для уменьшения объёмов трафика между ЦОДами и сетевых задержек сервисы при взаимодействии перешли с серверной на клиентскую балансировку. Так как при НТ требуется максимально точно воспроизводить клиентский трафик, от генераторов нагрузки ожидалось такое же поведение. О том, какие перед нами стояли задачи и как мы с ними справились, читайте под катом.  

f9e63aa51beef2bf82286eb0f4c89f8a.jpeg

Перед инфраструктурой НТ стояло две основные задачи:  

  • локализация нагрузочного трафика внутри ЦОДа, то есть генератор и нагружаемый им инстанс сервиса должны находиться в одном дата-центре в рамках одного кластера Kubernetes;  

  • возможность тестировать сервисы с использованием клиентской балансировки: генератор нагрузки должен уметь отправлять запросы не только на Ingress, но и использовать service discovery для определения IP тестируемого инстанса. 

Исходная архитектура

На момент переезда компании в несколько дата-центров инфраструктура нагрузочного тестирования представляла собой следующее:  

  • В качестве основного инструмента для запуска тестов мы используем Яндекс.Танк. 

  • В качестве генераторов нагрузки — в основном Pandora или JMeter (подробнее про теорию и инструменты НТ).   

  • При запуске теста центральный сервис инфраструктуры НТ поднимал Docker с генератором нагрузки на выделенной виртуальной машине (VM). К слову, на ней же по соседству с генераторами нагрузки базировалась InfluxDB для хранения статистики нагрузочных тестов, что не добавляло радости и ресурсов.  

  • Управление жизненным циклом генератора также происходило посредством центрального сервиса, который отдавал команду на запуск теста и сворачивал Docker после его завершения. 

Упрощённая схема исходной архитектурыУпрощённая схема исходной архитектуры

Соответственно, у такой схемы было множество ограничений. Вот только самые существенные из них:   

  • Так как генератор нагрузки находился на виртуальной машине, а тестируемые сервисы разворачивались в Kubernetes, при наличии инстансов сервиса в нескольких ЦОДах о локализации нагрузочного трафика внутри ЦОД не могло быть и речи. А объём трафика в условиях массового ночного тестирования мог быть существенным.  

  • По той же причине невозможно было использовать клиентскую балансировку, имитируя реальный трафик. Конечно, service discovery позволяет получить все IP тестируемых подов и разделить между ними трафик как душе угодно, но это не совсем честное и безопасное решение. 

  • На VM одновременно могло работать весьма ограниченное число генераторов нагрузки.  

Таким образом, существующее решение не было эффективным в условиях нескольких дата-центров и не удовлетворяло новым требованиям: локализация нагрузочного трафика внутри ЦОДа и поддержка клиентской балансировки.

Варианты решения проблем

Очевидно было, что для решения указанных проблем генераторы нагрузки тоже должны запускаться в Kubernetes. Причём в тех же ЦОДах, что и тестируемый сервис, — для локализации трафика. То есть как минимум один генератор нагрузки на один ЦОД, трафик с которого должен идти только в инстансы тестируемого сервиса, расположенные в том же ЦОДе. Таким образом, инфраструктуру нагрузочного тестирования тоже ожидал переезд. 

Рассматривалось несколько вариантов новой конфигурации НТ:  

1. Пул постоянно запущенных генераторов нагрузки в Kubernetes во всех ЦОДах

Идея состояла в том, чтобы один раз запустить некоторое число деплойментов с генераторами нагрузки в каждом ДЦ и переиспользовать их по мере необходимости. В каждый момент времени на одном генераторе нагрузки может быть запущен только один тест. Такая конфигурация близка к исходной, за исключением местоположения генераторов — в Kubernetes, а не на виртуальной машине.

Вариант с пулом постоянно запущенных генераторовВариант с пулом постоянно запущенных генераторов

Плюсы такого подхода:  

  • решает основные поставленные задачи: можно организовать локализацию трафика и использовать клиентскую балансировку (при доработке генератора);  

  • не нужно тратить время на деплой генератора нагрузки перед каждым тестом;  

  • меньше точек отказа — генератор нагрузки заранее запущен и работает;  

  • известны адреса генераторов. 

Минусы тоже присутствуют:  

  • всегда постоянное и ограниченное количество генераторов;  

  • часть генераторов может не использоваться в определённые моменты времени и при этом потреблять общие ресурсы кластера;  

  • невозможность динамически управлять выделяемыми генераторам ресурсами;  

  • необходимость постоянно мониторить состояние генераторов и вести учёт свободных/занятых. 

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

2. Запускать генератор нагрузки как Kubernetes Job или Kubernetes CronJob

Перед каждым тестом предполагалось средствами CI/CD запускать пайплайн, который стартовал бы Kubernetes Job с генератором нагрузки в указанном ЦОДе.  

Вариант с Kubernetes JobВариант с Kubernetes Job

Плюсы:     

  • возможность настраивать параметры содержащего генератор нагрузки Kubernetes pod в зависимости от требований теста;  

  • возможность запускать разлное количество генераторов нагрузки в каждом ЦОДе. Такая необходимость может возникнуть, если тестируемый сервис не имеет инстансов в одном из ЦОДов либо имеет разное количество инстансов в разных ЦОДах. 

Минусы:   

  • дополнительная сложность реализации: при таком варианте запуска генераторов необходимо настраивать и запускать пайплайн для каждого из них перед каждой стрельбой;  

  • усложнённый мониторинг (хотя в компании разработан функционал для мониторинга  Kubernetes CronJob, его использование требует доработок в коде генератора).  

  • дополнительная точка отказа: генератор нагрузки может вовремя не запуститься по каким-либо причинам (проблемы с доступом к GitLab, технические работы, отсутствие свободных раннеров и т. д.).  

3. Kubernetes Operator

Сервис — клиент Kubernetes API, управляющий настройкой и развёртыванием генераторов нагрузки в Kubernetes. Подробнее о Kubernetes Operator.

Плюсы:     

  • возможность гибко настраивать конфигурацию генератора нагрузки в зависимости от требований теста;  

  • возможность запускать разное количество генераторов в разных ЦОДах;  

  • возможность управления жизненным циклом генераторов;  

  • лёгкий мониторинг статуса и логов генератора при помощи Kubernetes API

Минусы:   

  • возникновение сразу двух дополнительных точек отказа:  

    1. деплой самого управляющего сервиса,  

    2. развёртывание генератора нагрузки (например, при отсутствии достаточных ресурсов в кластере).  

Итоговое решение

Взвесив все за и против, мы взялись за написание сервиса — оператора Kubernetes. Решение получилось достаточно гибким, простым и предоставило дополнительный функционал по мониторингу и управлению подами генераторов (об этом ниже).

Упрощённая схема архитектуры с операторомУпрощённая схема архитектуры с оператором

Конфигурация запуска 

Для управления параметрами запуска генераторов конфиг нагрузочного теста обзавёлся дополнительными полями и стал выглядеть примерно так:  

launch: 
  - env: string (dev|stg|prod|infra)   // окружение, в котором должен быть запущен тест 
    tank_config: string                // конфигурация самого теста
    resource_config:                   // выделяемые под генератор в К8s ресурсы
      memory:
        limit: string
        request: string
      cpu:
        limit: string
        request: string
    zone_config:                       // в каких ДЦ и в каком количестве должны быть        
      - zone: string                   // запущены генераторы
        magnitude: int

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

Особенности

  • Оператор в нашем исполнении представляет собой сервис, написанный на Go с использованием библиотеки для доступа к Kubernetes API.  

  • Для локализации нагрузочного трафика внутри ЦОДа инстансы операторов задеплоены во все дата-центры основных используемых в НТ кластеров Kubernetes (у нас это стейдж и продакшен).  

  • Каждый оператор может создавать генераторы только в своём окружении. 

  • Генераторы запускаются только на специально выделенных для них K8s nodes. Соседство с другими сервисами опасно и для этих сервисов, и для самих генераторов: кому-то может не хватить ресурсов в самый неожиданный момент. 

  • На запуск пода с генератором есть тайм-аут. Тесты выполняются по расписанию, и не все можно запускать параллельно, так как часть трафика идёт на одни и те же сервисы. При длительном запуске генератора время старта теста сдвигается, что может зааффектить соседей.   

  • Для расселения генераторов по нодам используются правила K8s affinity/anti-affinity.

  • Оператор следит за статусами подов с генераторами и удаляет завершившиеся. 

Возможности

Для различных типов тестов и разных сервисов требуются разные ресурсы: выделять под короткий и лёгкий пятиминутный тест столько же памяти и CPU, как и для комплексного тестирования целой системы, — непозволительная роскошь. Следовательно, лимиты и реквесты для генератора хотелось бы уметь ограничивать в зависимости от особенностей теста. И одним из самых приятных плюсов использования оператора является как раз такая возможность настраивать выделение ресурсов под генераторы. 

Кроме того, оператор в любой момент имеет доступ к любому запущенному поду с генератором в своём кластере. Благодаря этому открываются широкие возможности для управления жизненным циклом подов, мониторинга и логирования. Например, можно:  

  • передавать в генератор переменные окружения (например, для использования в кастомных плагинах);  

  • логировать статус подов и причину их завершения;  

  • сохранять логи подов в любом удобном формате. 

Флоу запуска генераторов 

Процесс запуска генератора для нагрузочного теста теперь выглядит так:  

  1. Пользователь настраивает конфиг запуска (выше) в соответствии с требованиями к тестируемому сервису и отправляет его в центральный сервис НТ. 

  1. Если конфиг успешно проходит валидацию, параметры запуска (в каком дата-центре, сколько и с какими ресурсами надо запустить генераторы) передаются в оператор. В противном случае пользователю возвращается ошибка с пояснением, какая часть его конфига настроена неверно. 

  1. Оператор запускает генераторы в указанном количестве на специально выделенных K8s nodes

  1. Генераторы получают конфиг с параметрами теста (куда и какую нагрузку подавать) и начинают тестирование.  

  1. После завершения теста оператор удаляет закончившие работу генераторы, предварительно сохранив их логи и конечный статус для дебага. 

Каждый генератор отправляет запросы только в инстансы тестируемого сервиса, расположенные в одних с ним K8s-кластере и зоне. Таким образом, нагрузочный трафик остаётся в пределах одного дата-центра. 

Заключение

Так из одного дата-центра и виртуальных машин наша инфраструктура для нагрузочного тестирования переехала в Kubernetes и три ЦОДа. Это потребовало существенных изменений в архитектуре запуска тестов. Мы рассмотрели несколько вариантов её модификации, каждый из которых позволял решить основную задачу — локализацию нагрузочного трафика внутри дата-центров —, но обладал и своими недостатками. В конечном итоге был выбран наиболее гибкий — K8s-оператор. Такое решение позволяет:  

  • запускать генераторы нагрузки с динамической конфигурацией в среде Kubernetes в определённых пользователем количестве и дата-центре;  

  • передавать в генератор переменные окружения для использования в кастомных плагинах;  

  • мониторить статус генераторов и сохранять их логи;  

  • удалять генераторы (при необходимости). 

Это далеко не все возможности, предоставляемые оператором как единой точкой управления подами генераторов нагрузки. В планах у нас административный интерфейс для управления генераторами, а также улучшение мониторинга и логирования. Пока же, успешно прожив с оператором неполный год, мы стали запускать в пять раз больше тестов, распределённых по трём дата-центрам. А то, как мы агрегируем данные распределённых тестов, — уже совсем другая история:) 

© Habrahabr.ru