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



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

Проблема


Selenium с момента своего создания не раз кардинально менялся, текущая архитектура, называющаяся Selenium Grid, работает так.

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

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

Решение


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

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


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

Вот как это работает:

  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. Сервер из коробки обеспечивает квотирование, то есть независимую работу нескольких пользователей.



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

  • Балансировщик нагрузки (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 Гб памяти оптимальным является сочетание хаба и пяти нод. На одну виртуальную машину мы устанавливаем только одну версию браузера, поскольку в этом случае легко измерять потребление памяти и переводить число виртуальных машин в число имеющихся браузеров.

Комментарии (30)


  1. slonopotamus
    06.10.2015 20:13
    +7

    А какой такой «тяжелой» нагрузкой занимается хаб, что он может обслуживать насколько мало нод?


    1. vaniaPooh
      06.10.2015 20:46

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


      1. slonopotamus
        06.10.2015 23:57
        +5

        Ваш ответ ничего не объясняет. Я понимаю в чем состоит нагрузка на ноду — она занимается парсингом страниц, рендерингом в браузере и обработкой всяких яваскриптов. Но я не понимаю чем занят хаб. Интуитивно кажется, что он должен быть I/O-bound, но вы утверждаете, что он упирается в CPU. Что кажется неправильным и могло бы быть исправлено путем профилирования и устранения бессмысленных занятий, пожирающих проц на хабе.


        1. KlonD90
          07.10.2015 14:24

          Мне кажется Selenium не сильно оптимизирован, поэтому собственно такая история.


    1. lev_sha
      07.10.2015 15:05
      +2

      Тут на самом деле несколько аспектов
      1. Хаб так или иначе держит таблицу доступных нод и постоянно обменивается с ними данными, пишет логи итп. (видимо действительно что-то из этого он делает неоптимально). Наш прокси, при этом, крайне легковесный, и его реализация изначально позволяет бесконечно масштабироваться за балансером.
      2. При этом, как показали наши опыты, действительно есть предел производительности хаба, который, видимо, пропорционален железу. На небольших машинках в облаке 2хядерный хаб плохо держит более 20 нод. (ранее у нас была клиентская балансировка и хабы стояли на железе, тогда было просто 16 хабов и отдельные ноды)
      3. Мы не изучали подробно внутреннюю архитектуру хаба, тк на данном этапе переписывать и оптимизировать хаб своими силами не входило в наши планы. Держать свою кастомную реализацию хаба мы не хотели тем более.
      4. При этом одной из задач, которую мы решали, являлась задача повышения надежности. Один хаб, выходя из строя, тащит за собой весь грид. Два — половину.
      5. Организовав конфигурацию с большим количеством машин, содержащих хаб и 5 изолированных нод, мы решили сразу три задачи — строго лимитировали нагрузку на один хаб, убрали фактор хаба как точки отказа и при этом остались в рамках стандартных непатченных selenium артефактов.


    1. alekciy
      07.10.2015 15:09
      +1

      На сколько я помню сам selenium-server содержит код как хаба, так и ноды (собственно роль банально задается через флаг role). Что наталкивает на мысль о том, что в принципе имеем в итоге большой комбайн который вроде как умеет все. Причины видимо исторические.


  1. SKY_nv
    06.10.2015 21:02
    -13

    То-то в я браузере вкладки падают и падают. Уже смотрю в сторону вивальди, а то задолбало наблюдать сие


    1. tundrawolf_kiba
      06.10.2015 21:41
      +4

      В техподдержку писать не пробовали? Мне вот например отвечали и помогали.


      1. SKY_nv
        06.10.2015 21:44
        -3

        мне тоже, только проблемы были не мои, а браузера. А тут на их новомодной почте (интерфейс) как начало падать пару дне назад, что пришлось вернуть классический интерфейс ну и заодно начать вивальди для себя готовить, за которым слежу с первого билда. Нет, и не собираюсь. Ибо в данном случае, это ни к чему не приведет. Проблема того что внезапно разные вкладки само собой падают в такие сообщения имеется давно, а воз и ныне там.


        1. tundrawolf_kiba
          06.10.2015 21:52
          +3

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


          1. SKY_nv
            07.10.2015 04:26

            У вас много табов открыто?


            1. tundrawolf_kiba
              07.10.2015 09:48

              В среднем — от десятка до двух, но в некоторые моменты бывало 3-4 десятка


              1. SKY_nv
                07.10.2015 11:20

                может дело в этом. у меня не опускается ниже 2ух десятков.


    1. vaniaPooh
      06.10.2015 22:48
      +1

      Отписал лицам, отвечающим за разработку Яндекс.Браузера. В ближайшее время они отреагируют на ваш комментарий.


    1. BarakAdama
      07.10.2015 10:51
      +2

      Напишите, пожалуйста, через «Сообщить о проблеме» в меню браузера (в этом случае передается еще техническая информация о компьютере). Желательно описать сценарии, когда падения происходили.


    1. Andre_487
      10.10.2015 18:46

      Я уверен — виноват именно роутер из статьи, поэтому лучшего места для этот репорта не найти


  1. SlavikF
    07.10.2015 02:07

    Я не понял вот эту фразу:

    мы выяснили, что для виртуальной машины с двумя VCPU и 4 Гб памяти оптимальным является сочетание хаба и пяти нод

    Я всегда думал, что каждая нода ставится на отдельную машину. А вы тут предлагаете на одну машину ставить 5 нод. В чём смысл?


    1. vaniaPooh
      07.10.2015 12:42

      Поскольку мы работаем с большими объемами браузеров, то довольно важный вопрос — об эффективности использования железа. Типичный железный сервер имеет 24 и более ядра и больше 50 Гб памяти. Для того, чтобы не думать об аппаратном уровне мы используем облако. Для экономии ресурсов и обеспечения изоляции браузеров на одной виртуальной машине мы поднимаем несколько нод в разных виртуальных рабочих столах по 1 браузеру в каждой. Если этого не делать, то, например, начинаются проблемы с пропаданием фокуса на окнах браузера.


      1. alekciy
        07.10.2015 15:15

        На одной виртуальной машине живет только какая-то одна версия браузера (допустим там FireFox последней версии), так? И если нужен тест на более старой мажорной версии, то её нужно искать уже на другой ноде?


        1. vaniaPooh
          07.10.2015 15:20
          +1

          Selenium позволяет держать разные мажорные версии браузера на одной ноде, но с точки зрения администрирования делать так — одна большая проблема. Поэтому оказалось удобным ставить разные мажорные версии браузера на разные виртуальные машины: легче настраивать, легче переводить железо в браузеры и так далее. С точки зрения тестов ничего не меняется — указывается название браузера и версия, а gridrouter сам знает (это прописано в XML конфигурации) на какую машину пойти, чтобы отдать нужный браузер.


      1. SlavikF
        07.10.2015 17:42

        А что используете для виртуальных столов?

        Desktops v2.0 от Microsoft?
        technet.microsoft.com/en-us/sysinternals/cc817881.aspx

        Или что-то ешё?


        1. vaniaPooh
          07.10.2015 18:39

          Под Linux: xvfb, под Windows — desktop-utils.exe.


          1. alekciy
            08.10.2015 16:14

            А под linux что-то другое были попытки использовать?


            1. vaniaPooh
              08.10.2015 16:23
              +1

              Например? Поскольку мы запускаем все на серверах без монитора, то xvfb — напрашивающееся решение. Обычный X сервер требует наличия монитора.


  1. estonec
    09.10.2015 11:27

    Уважаемый, пока ещё, Яндекс! А вы расскажете, как на свет вывалился гениальный продукт — обновленный интерфейс Кинопоиска?


    1. vaniaPooh
      09.10.2015 16:52

      К сожалению, я не владею информацией по данному вопросу, но уверен, что реакция последует.


  1. gregox
    09.10.2015 19:50

    Кинопоиск верните.


    1. vaniaPooh
      10.10.2015 15:47

      1. Kinjeiro
        13.10.2015 11:31

        Адекватную систему рейтингов верните


  1. navion
    16.10.2015 11:35

    А тестирование на OS X сюда входит? Как и чем решаете эту задачу?