Тестирование в Яндексе. Как сделать отказоустойчивый грид из тысячи браузеров
Любой специалист, причастный к тестированию веб-приложений, знает, что большинство рутинных действий на сервисах умеет делать фреймворк Selenium. В Яндексе в день выполняются миллионы автотестов, использующих Selenium для работы с браузерами, поэтому нам нужны тысячи различных браузеров, доступных одновременно и 24/7. И вот тут начинается самое интересное.
Selenium с большим количеством браузеров имеет много проблем с масштабированием и отказоустойчивостью. После нескольких попыток у нас получилось элегантное и простое в обслуживании решение, и мы хотим поделиться им с вами. Наш проект gridrouter позволяет организовать отказоустойчивый Selenium-грид из любого количества браузеров. Код выложен в open-source и доступен на Github. Под катом я расскажу, на какие недостатки Selenium мы обращали внимание, как пришли к нашему решению, и объясню, как его настроить.
Проблема
Selenium с момента своего создания не раз кардинально менялся, текущая архитектура, называющаяся Selenium Grid, работает так.
Кластер состоит из двух приложений: хаба (hub) и ноды (node). Хаб – это API, принимающее запросы пользователей и отправляющее их на ноды. Нода – исполнитель запросов, запускающий браузеры и выполняющий в них шаги теста. К одному хабу может быть теоретически подключено бесконечное число нод, каждая из которых умеет запускать любой из поддерживаемых браузеров. А что же на практике?
- Есть уязвимое место. Хаб – это единственная точка доступа к браузерам. Если по каким-то причинам процесс хаба перестает отвечать, то все браузеры становятся недоступны. Ясно, что сервис также перестает работать, если у дата-центра, где стоит хаб, происходит отказ по сети или питанию.
- Selenium Grid плохо масштабируется. Наш многолетний опыт эксплуатации Selenium на разном оборудовании показывает, что под нагрузкой один хаб способен работать не более чем с несколькими десятками подключенных нод. Если продолжать добавлять ноды, то при пиковой нагрузке хаб может перестать отвечать по сети или обрабатывает запросы слишком медленно.
- Нет квотирования. Нельзя создать пользователей и указать, какие версии браузеров какой пользователь может использовать.
Решение
Чтобы не страдать при падении одного хаба, можно поднять несколько. Но обычные библиотеки для работы с Selenium рассчитаны на работу только с одним хабом, поэтому придется научить их работать с распределенной системой.
Балансировка на клиенте
Первоначально мы решали проблему работы с несколькими хабами с помощью небольшой библиотеки, которая использовалась в коде тестов и осуществляла балансировку на клиентской стороне.
Вот как это работает:
- Информация о хостах с хабами и доступных на них версиях браузеров сохраняется в файл конфигурации.
- Пользователь подключает библиотеку в свои тесты и запрашивает браузер.
- Из списка случайным образом выбирается хост и делается попытка получить браузер.
- Если попытка удачная, то браузер отдается пользователю и начинаются тесты.
- Если браузер не удалось получить, то опять случайно выбирается следующий хост и т. д. Поскольку разные хабы могут иметь разное количество доступных браузеров, хабам в файле конфигурации можно назначить разные веса, и случайная выборка делается с учетом этих весов. Такой подход позволяет добиться равномерного распределения нагрузки.
- Пользователь получает ошибку только в том случае, если браузер не удалось получить ни на одном из хабов.
Реализация такого алгоритма несложная, но требует интеграции с каждой библиотекой для работы с Selenium. Допустим, в ваших тестах браузер получается таким кодом:
WebDriver driver = new RemoteWebDriver(SELENIUM_SERVER_URL, capabilities);
Здесь RemoteWebDriver – это стандартный класс для работы с Selenium на Java. Для работы в нашей инфраструктуре придется обернуть его в наш собственный код с выбором хаба:
WebDriver driver = SeleniumHubFinder.find(capabilities);
В коде тестов больше нет URL до Selenium, он содержится в конфигурации библиотеки. Также это значит, что код тестов теперь привязан к SeleniumHubFinder и без него не запустится. Кроме того, если у вас есть тесты не только на Java, но и на других языках, то придется писать клиентский балансировщик для них всех, а это может быть затратно. Гораздо проще вынести код балансировки на сервер и указать его адрес в коде тестов.
Балансировка на сервере
При проектировании сервера мы заложили следующие естественные требования:
- Сервер должен реализовывать API Selenium (протокол JsonWire), чтобы тесты работали с ним, как с обычным Selenium-хабом.
- Можно расставить сколько угодно голов сервера в любых дата-центрах и забалансировать их железным или программным балансировщиком (SLB).
- Головы сервера совершенно независимы друг от друга и не хранят общее состояние (shared state).
- Сервер из коробки обеспечивает квотирование, то есть независимую работу нескольких пользователей.
Архитектурно полученное решение выглядит так:
- Балансировщик нагрузки (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.
Рекомендации по настройке хабов и нод
Мы проводили эксперименты с разными конфигурациями хабов и нод и пришли к выводу, что с точки зрения простоты эксплуатации, надежности и легкости масштабирования наиболее практичным является следующий подход. Обычно устанавливают один хаб, к которому подключают много нод, потому что точка входа должна быть одна.
При использовании gridrouter можно поставить сколько угодно хабов, поэтому проще всего настроить на одной машине один хаб и несколько нод, подключенных к localhost:4444. Особенно удобно так делать, если все разворачивается на виртуальных машинах. Например, мы выяснили, что для виртуальной машины с двумя VCPU и 4 Гб памяти оптимальным является сочетание хаба и пяти нод. На одну виртуальную машину мы устанавливаем только одну версию браузера, поскольку в этом случае легко измерять потребление памяти и переводить число виртуальных машин в число имеющихся браузеров.