Тестирование в Яндексе. Как сделать отказоустойчивый грид из тысячи браузеров

Любой специалист, причастный к тестированию веб-приложений, знает, что большинство рутинных действий на сервисах умеет делать фреймворк Selenium. В Яндексе в день выполняются миллионы автотестов, использующих Selenium для работы с браузерами, поэтому нам нужны тысячи различных браузеров, доступных одновременно и 24/7. И вот тут начинается самое интересное.

173930c0ec244ae8afa229966e0453d3.png

Selenium с большим количеством браузеров имеет много проблем с масштабированием и отказоустойчивостью. После нескольких попыток у нас получилось элегантное и простое в обслуживании решение, и мы хотим поделиться им с вами. Наш проект gridrouter позволяет организовать отказоустойчивый Selenium-грид из любого количества браузеров. Код выложен в open-source и доступен на Github. Под катом я расскажу, на какие недостатки Selenium мы обращали внимание, как пришли к нашему решению, и объясню, как его настроить.

Проблема


Selenium с момента своего создания не раз кардинально менялся, текущая архитектура, называющаяся Selenium Grid, работает так.
7ccc5a82857d493d9c2dac705e704e6d.png
Кластер состоит из двух приложений: хаба (hub) и ноды (node). Хаб – это API, принимающее запросы пользователей и отправляющее их на ноды. Нода – исполнитель запросов, запускающий браузеры и выполняющий в них шаги теста. К одному хабу может быть теоретически подключено бесконечное число нод, каждая из которых умеет запускать любой из поддерживаемых браузеров. А что же на практике?

  • Есть уязвимое место. Хаб – это единственная точка доступа к браузерам. Если по каким-то причинам процесс хаба перестает отвечать, то все браузеры становятся недоступны. Ясно, что сервис также перестает работать, если у дата-центра, где стоит хаб, происходит отказ по сети или питанию.
  • Selenium Grid плохо масштабируется. Наш многолетний опыт эксплуатации Selenium на разном оборудовании показывает, что под нагрузкой один хаб способен работать не более чем с несколькими десятками подключенных нод. Если продолжать добавлять ноды, то при пиковой нагрузке хаб может перестать отвечать по сети или обрабатывает запросы слишком медленно.
  • Нет квотирования. Нельзя создать пользователей и указать, какие версии браузеров какой пользователь может использовать.

Решение


Чтобы не страдать при падении одного хаба, можно поднять несколько. Но обычные библиотеки для работы с Selenium рассчитаны на работу только с одним хабом, поэтому придется научить их работать с распределенной системой.

Балансировка на клиенте


Первоначально мы решали проблему работы с несколькими хабами с помощью небольшой библиотеки, которая использовалась в коде тестов и осуществляла балансировку на клиентской стороне.
a325aca11dba405da9ff98aaf5cf3f68.png
Вот как это работает:

  1. Информация о хостах с хабами и доступных на них версиях браузеров сохраняется в файл конфигурации.
  2. Пользователь подключает библиотеку в свои тесты и запрашивает браузер.
  3. Из списка случайным образом выбирается хост и делается попытка получить браузер.
  4. Если попытка удачная, то браузер отдается пользователю и начинаются тесты.
  5. Если браузер не удалось получить, то опять случайно выбирается следующий хост и т. д. Поскольку разные хабы могут иметь разное количество доступных браузеров, хабам в файле конфигурации можно назначить разные веса, и случайная выборка делается с учетом этих весов. Такой подход позволяет добиться равномерного распределения нагрузки.
  6. Пользователь получает ошибку только в том случае, если браузер не удалось получить ни на одном из хабов.


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

WebDriver driver = new RemoteWebDriver(SELENIUM_SERVER_URL, capabilities);


Здесь RemoteWebDriver – это стандартный класс для работы с Selenium на Java. Для работы в нашей инфраструктуре придется обернуть его в наш собственный код с выбором хаба:

WebDriver driver = SeleniumHubFinder.find(capabilities);


В коде тестов больше нет URL до Selenium, он содержится в конфигурации библиотеки. Также это значит, что код тестов теперь привязан к SeleniumHubFinder и без него не запустится. Кроме того, если у вас есть тесты не только на Java, но и на других языках, то придется писать клиентский балансировщик для них всех, а это может быть затратно. Гораздо проще вынести код балансировки на сервер и указать его адрес в коде тестов.

Балансировка на сервере


При проектировании сервера мы заложили следующие естественные требования:

  1. Сервер должен реализовывать API Selenium (протокол JsonWire), чтобы тесты работали с ним, как с обычным Selenium-хабом.
  2. Можно расставить сколько угодно голов сервера в любых дата-центрах и забалансировать их железным или программным балансировщиком (SLB).
  3. Головы сервера совершенно независимы друг от друга и не хранят общее состояние (shared state).
  4. Сервер из коробки обеспечивает квотирование, то есть независимую работу нескольких пользователей.


c4c050ebce704b60abf224e1e4fd5730.png
Архитектурно полученное решение выглядит так:

  • Балансировщик нагрузки (SLB) раскидывает запросы от пользователей на одну из N голов с сервером, слушающих на стандартном порту (4444).
  • Каждая из голов хранит в виде конфигурации информацию обо всех имеющихся Selenium-хабах.
  • При поступлении запроса на браузер сервер использует алгоритм балансировки, описанный в предыдущем разделе, и получает браузер.
  • Каждый запущенный браузер в стандартном Selenium получает свой уникальный идентификатор, называемый ID сессии. Это значение передается клиентом хабу при любом запросе. При получении браузера сервер подменяет настоящий ID сессии на новый, дополнительно содержащий информацию о хабе, на котором была получена данная сессия. Полученная сессия с расширенным ID отдается клиенту.
  • При следующих запросах сервер извлекает адрес хоста с хабом из ID сессии и проксирует запросы на этот хост. Поскольку вся нужная серверу информация есть в самом запросе, не надо синхронизировать состояние голов – каждая из них может работать независимо.

Gridrouter


Сервер мы назвали gridrouter и решили поделиться его кодом со всеми. Сервер написан на Java с использованием Spring Framework. Исходники проекта можно посмотреть по ссылке. Мы также подготовили Debian-пакеты, устанавливающие сервер.
В настоящий момент gridrouter установлен в качестве боевого сервера, используемого разными командами Яндекса. Общее количество доступных браузеров в этом гриде более трех тысяч. В пиках нагрузки мы обслуживаем примерно такое же количество пользовательских сессий.

Как настраивать gridrouter


Для того чтобы настроить gridouter, нужно задать список пользователей и квоты для каждого пользователя. Мы не ставили цель сделать супербезопасную аутентификацию с хэш-функциями и солью, поэтому используем обычную basic HTTP-аутентификацию, а логины и пароли храним в открытом виде в текстовом файле /etc/grid-router/users.properties вида:

user:password, user
user2:password2, user


Каждая строчка содержит логин и пароль через двоеточие, а также роль, которая на данный момент одна и та же, – user. Что касается квот, то здесь все тоже очень просто. Каждая квота представляет собой отдельный XML-файл /etc/grid-router/quota/<login>.xml, где <login> – имя пользователя. Внутри файл выглядит так:

<qa:browsers xmlns:qa="urn:config.gridrouter.qatools.ru">
    <browser name="firefox" defaultVersion="33.0">
        <version number="33.0">
            <region name="us-west">
                <host name="my-firefox33-host-1.example.com" port="4444" count="5"/>
            </region>
            <region name="us-east">
                <host name="my-firefox33-host-2.example.com" port="4444" count="5"/>
            </region>
        </version>
        <version number="38.0">
            <region name="us-west">
                <host name="my-firefox38-host-1.example.com" port="4444" count="4"/>
                <host name="my-firefox38-host-2.example.com" port="4444" count="4"/>
            </region>
            <region name="us-east">
                <host name="my-firefox38-host-3.example.com" port="4444" count="4"/>
            </region>
        </version>
    </browser>
    <browser name="chrome" defaultVersion="42.0">
        <version number="42.0">
            <region name="us-west">
                <host name="my-chrome42-host-1.example.com" port="4444" count="1"/>
            </region>
            <region name="us-east">
                <host name="my-chrome42-host-2.example.com" port="4444" count="4"/>
                <host name="my-chrome42-host-3.example.com" port="4444" count="3"/>
            </region>
        </version>
    </browser>
</qa:browsers>


Видно, что определяются имена и версии доступных браузеров, которые должны точно совпадать с теми, что указаны на хабах. Для каждой версии браузера определяется один или несколько регионов, то есть разных дата-центров, в каждый из которых записываются хост, порт и количество доступных браузеров (это и есть вес). Имя региона может быть произвольным. Информация о регионах нужна в тех случаях, когда один из дата-центров становится недоступен. В этом случае gridrouter после одной неудачной попытки получения браузера в некотором регионе пытается получить браузер из другого региона. Такой алгоритм значительно повышает вероятность быстрого получения браузера.

Как запустить тесты


Хотя в основном мы пишем на Java, мы проверяли наш сервер с Selenium тестами на других языках программирования. Обычно в тестах URL хаба указывается примерно так:

http://example.com:4444/wd/hub


Поскольку мы используем basic HTTP-аутентификацию, при работе с gridrouter следует использовать такие ссылки:

http://username:password@example.com:4444/wd/hub


Если у вас возникнут проблемы с настройкой, обращайтесь к нам, заводите issue на Github.

Рекомендации по настройке хабов и нод


Мы проводили эксперименты с разными конфигурациями хабов и нод и пришли к выводу, что с точки зрения простоты эксплуатации, надежности и легкости масштабирования наиболее практичным является следующий подход. Обычно устанавливают один хаб, к которому подключают много нод, потому что точка входа должна быть одна.
803d24285c214cd6a19d6dab81429b95.png
При использовании gridrouter можно поставить сколько угодно хабов, поэтому проще всего настроить на одной машине один хаб и несколько нод, подключенных к localhost:4444. Особенно удобно так делать, если все разворачивается на виртуальных машинах. Например, мы выяснили, что для виртуальной машины с двумя VCPU и 4 Гб памяти оптимальным является сочетание хаба и пяти нод. На одну виртуальную машину мы устанавливаем только одну версию браузера, поскольку в этом случае легко измерять потребление памяти и переводить число виртуальных машин в число имеющихся браузеров.

© Habrahabr.ru