Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 11 октября 2007

Практический JS: балансировка на стороне клиента

Примечание: ниже находится перевод статьи "Client Side Load Balancing for Web 2.0 Applications", в которой затрагиваются вопросы балансировки нагрузки между несколькими серверами и рассматривается решение, обеспечивающее балансировку такой нагрузки прямо на компьютере клиента.

Сервер обрабатывает HTTP (HyperText Transfer Protocol) запросы со стороны браузеров. Если вы введете в адресной строке URL, например, http://www.digital-web.com, то ваш компьютер отправит поисковый запрос для определения, какие именно сервера будут обрабатывать ваш запрос и пересылать данные. Техника обработки таких запросов для кластера веб-серверов называется балансировкой нагрузки.

Балансировка нагрузки для веб-приложений

Балансировка нагрузки повышает надежность веб-сайта путем распределения запросов между несколькими (кластером) серверами, если один из них перегружен или отказал. Существует много методов по обеспечению такого поведения, но все они должны удовлетворять следующим требованиям:

  • Распределять нагрузку внутри кластера рабочих серверов.
  • Корректно обрабатывать отказ одного из рабочих серверов.
  • Весь кластер должен существовать для конечного пользователя как одна-единственная машина.

Популярным, хотя и очень простым подходом для балансировки запросов является циклический (round-robin) DNS. Он подразумевает создание нескольких записей в таблице DNS для одного домена. Например, к слову, мы хотим распределять нагрузку для сайта www.myloadbalancedwebsite.com, и у нас есть два сервера с IP адресами 64.13.192.120 и 64.13.192.121, соответственно. Для того, чтобы создать циклический DNS для распределения запросов, можно просто создать следующие записи в DNS:

www.myloadbalancedwebsite.com  64.13.192.120
www.myloadbalancedwebsite.com  64.13.192.121

После каждого пользовательского запроса к таблице DNS для www.myloadbalancedwebsite.com, запись, стоящая первой, меняется. Ваш браузер будет использовать первую запись, поэтому все запросы будут распределяться случайным образом между этими двумя серверами. К несчастью, ключевым недостатком этого подхода является нарушение второго условия, обозначенного выше, а именно: при отказе одного из серверов, сервер DNS все равно будет отправлять на него пользовательские запросы, и половина ваших пользователей окажется за бортом.

Другим популярным подходом при балансировки запросов является создание одного выделенного сервера, который отвечает за распределение запросов. Примерами таких серверов могут быть специальное оборудование или программные решения, например, F5 BIG-IP или Linux Virtual Server Project. Выделенный балансировщик принимает запросы и распределяет их между внутренним кластером веб-серверов. Балансировщик отвечает за обнаружение отказавшего сервера и распределение запросов по остальным. Для повышения надежности в эту схему может быть добавлен дополнительный балансировщик, который включается, когда отказывает основной.

Минусы этого подхода:

  1. Существует предел запросов, которые могут быть приняты самим балансировщиком. Однако, эта проблема решается введением в схему циклического DNS.
  2. Поддержка балансировщика может обходиться в круглую сумму, доходя до десятков тысяч долларов. К тому же запасной балансировщик большую часть времени простаивает в ожидании того, как откажет основной.

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

Существует еще один подход для распределения нагрузки на серверы от современных веб-приложений, который не нуждается в дополнительном балансирующем оборудовании и отказ одного из серверов происходит гораздо более незаметно для клиента, чем в случае циклического DNS. Прежде чем мы углубимся в детали, давайте представим себе настольное приложение, которому требуется установить связь с серверами в интернете для получения данных. Если наше приложение создает больше запросов к удаленному серверу, чем тот может поддерживать при помощи единственной машины, нам потребуется решение для балансировки нагрузки. Мjжем ли мы воспользоваться циклическим DNS или балансировщиком нагрузки, описанным выше? Конечно, но существует и более дешевое и надежное решение.

Вместо того, чтобы сказать клиенту, что у нас единственный сервер, можно сообщить о нескольких серверах — s1.myloadbalancedsite.com, s2.myloadbalancedsite.com и так далее. При этом клиентское приложение может случайным образом выбирать сервер для подключения и пытаться получить данные с него. Если сервер не доступен или не отвечает длительное время, клиент сам выберет другой сервер, и так далее, пока не получит свои данные. В отличие от веб-приложений, которые хранят код (Javascript или Flash SWF) на одном сервере, который обеспечивает доступ к этой информации, клиентское приложение независимо от сервера. Оно может само выбирать между серверами на стороне клиента для обеспечения масштабируемости приложения.

Пример балансировки нагрузки и масштабируемости

Пример балансировки нагрузки и масштабируемости

Итак, можно ли эту технику применить к веб-приложениям? Прежде чем ответить на этот вопрос, нужно установить характерные особенности самого веб-приложения.

Веб-приложения самой своей сутью размывают границу между клиентской и серверной частью любого стандартного приложения. Веб-приложения, написанные на PHP, часто смешивают серверный и клиентский код в одном документе. Даже при использования паттерна MVC (модель-вид-контроллер, model-view-controller), когда код, который генерирует уровень представления (presentation layer) (HTML), отделен от серверной логики, все равно сервер создает и доставляет представление страницы. Хотя сервер обеспечивает такие ресурсы, как картинки, в современным веб-технологиях и этот факт существенно меняется (прим.: взять хотя бы технику CSS-sprites, когда одна картинка являются источником для нескольких, и CSS/JavaScript используется для «вытягивания» каждой отдельной картинки из источника). Сейчас многие приложения осуществляют к серверу только AJAX- или Flash-запросы (прим.: а не загружают каждый раз итоговый документ с сервера). Поэтому стандартное настольное и веб-приложение очень похожи в смысле серверных вызовов.

Для обеспечения балансировки на стороне клиента от современного веб-приложения требуется три основных составляющих:

  1. Клиентский код: JavaScript и(ли) SWF (для flash-клиентов)
  2. Ресурсы: картинки, CSS (Каскадные Таблицы Стилей), аудио, видео, и HTML-документы
  3. Серверный код: внутренняя логика для обеспечения нужных клиентам данных

Заметно проще повысить доступность и масштабируемость html-кода страниц и других файлов, требуемых на клиенте, чем осуществить то же самое для серверных приложений: доставка статического содержания требует значительно меньше ресурсов. К тому же существует возможность выложить клиентский код через достаточно проверенный сервисы, например, S3 от Amazon Web Services. Как только у нас есть код и ресурсы, обслуживаемые высоконадежной системой доставки содержания (CDN, Content Delivery Network), мы можем уже подумать над балансировкой нагрузки на серверные мощности.

Мы можем включить список доступных серверов в клиентский код точно так же, как сделали бы это для настольного приложения. У веб-приложения доступен файл servers.xml, в котором находится список текущих серверов. Оно пытается связаться (используя AJAX или Flash) с каждым сервером в списке, пока не получит ответ. Таки образом, весь алгоритм на клиенте выглядит примерно следующим образом:

  1. Загужается файл www.myloadbalancedwebsite.com/servers.xml, который выложен вместе с клиентским кодом и другими ресурсами и содержит список доступных серверов, например, в следующем виде:
    <servers>
        <server>s1.myloadbalancedwebsite.com</server>
        <server>s2.myloadbalancedwebsite.com</server>
        <server>s3.myloadbalancedwebsite.com</server>
        <server>s4.myloadbalancedwebsite.com</server>
    </servers>
  2. Выбирается случайным образом сервер из списка и попытаться с ним соединиться. Во всех последующих запросах используется этот сервер.
  3. На клиенте существует заранее установленное время ожидания запроса, если оно превышено, то исполняется шаг 2.

Осуществляем кросс-доменные запросы

Если вы хоть сколько-нибудь работали с AJAX, то, наверное, подумали: «Это не будет работать по причине кросс-доменной безопасности» (прим.: для предотвращения XSS-атак). Давайте рассмотрим и этот вопрос.

Для обеспечения безопасности пользователей веб-браузеры и Flash-клиенты блокируют пользовательские вызовы к другим доменам. Например, если клиентский код хочет обратиться к серверу s1.myloadbalancedwebsite.com, он должен быть загружен только с того же домена, s1.myloadbalancedwebsite.com. Запросы от клиентов на другие домены будут заблокированы. Для того чтобы обеспечить работоспособность описанной выше схемы балансировки, из клиентского кода на www.myloadbalancedwebsite.com требуется совершать обращения к серверам в с другими доменами (например, к s1.myloadbalancedwebsite.com).

Для Flash-клиентов можно просто создать файл crossdomain.xml для разрешения запросов на *.myloadbalancedwebsite.com:

<cross-domain-policy>
    <allow-access-from domain="*.myloadbalancedwebsite.com"/>
</cross-domain-policy>

Для клиентского кода на AJAX существуют жесткие ограничения на передачу данных между доменами, которые зависят от используемых для серверных вызовов методов. Применение техники Dynamic script Tag для осуществления запросов позволяет обойти ограничения по безопасности, ибо разрешает кросс-доменные вызовы. (Однако, стоит проверять заголовок referrer, чтобы убедиться, что именно ваш клиент осуществляет такие запросы, для безопасности вашего сайта.)

Но что, если на клиенте используется XMLHttpRequest? XHR попросту запрещает клиенту запрашивать отличный от исходного домена сервер. Однако, существует небольшая лазейка: если клиент и сервер использует одинаковый домен верхнего уровня, для нашего примера это www.myloadbalancedwebsite.com и s1.myloadbalancedsite.com, то можно осуществлять AJAX-вызовы с использованием iframe, уже через который загружать документы с сервера. Браузеры позволяют скриптам обращаться к такому iframe'у как к «родному», таким образом, становится возможным доставлять данные с помощью серверного вызова через iframe, если скрипты были загружены с того же домена верхнего уровня. Проблема оказывается решенной.

Преимущества балансировки на стороне клиента

Итак, как только мы обговорили технику, позволяющую осуществлять кросс-доменные вызовы, можно обсудить, собственно, как работает сам балансировщик и как он удовлетворяет требованиям, изложенным в начале статьи.

  1. Распределение нагрузки между кластером веб-серверов. Так как клиент выбирает сервер, с которого принимает запросы, случайным образом, загрузка будет распределена случайно (прим.: практически равномерно) между всеми имеющимися серверами.
  2. Незаметное выключение неработающего сервера из кластера. У клиента всегда его возможность подключиться к другому серверу, если первый не отвечает больше заранее определенного времени. Таким образом, подключение клиента «мягко» передается от одного сервера другому.
  3. Работающий кластер доступен для клиента как один сервер. В нашем примере, пользователь просто открывает в браузере http://www.myloadbalancedwebsite.com/, который и является для клиента единственным доступным сервером, использованием всех остальных «зеркал» происходит абсолютно незаметно.

Подведем итог, какие же преимущества балансировки на стороне клиента перед балансировкой на стороне сервера? Наиболее очевидное заключается в том, что не требуется специальное балансирующее оборудование (прим.: оборудование-то не требуется, но сам клиентский код будет достаточно сложным и являться полноценным веб-приложением), и не будет никакой необходимости настраивать аппаратную часть или проверять зеркальность вторичного балансировщика для страховки основного. Если сервер не доступен, его можно просто исключить из файла servers.xml.

Другим преимуществом является то, что все серверы не обязаны быть расположенными в одном месте. Клиент сам выбирает, к какому серверу ему лучше подключиться, а не балансирующий сервер рассматривает его запрос и выбирает один из кластерных серверов для его обработки. Расположение серверов ничем не ограничено. Они могут находиться в различных датацентрах на тот случай, если один из датацентров окажется не доступен. Если приложению требуется база данных, расположенная в локальной сети, второй датацентр может быть по-прежнему использован как запасной, если откажет основной. Переключение с одного датацентра на другой заключается просто в обновлении файла servers.xml вместо того, чтобы ждать рапространения изменений в таблице DNS.

Voxlite — веб-приложение для балансировки на стороне клиента

Voxlite является веб 2.0 приложением, которое позволяет пользователям обмениваться видео-сообщениям, имея под рукой лишь браузер и веб-камеру. Оно использует балансировку на стороне клиента для обеспечения повышенной доступности и масштабируемости. Также Voxlite использует сервисы Simple Storage Service (S3) и Elastic Computing Cloud (EC2) от Amazon Web Services.

Иначально, сервис S3 представляет прекрасную возможность для хранения и доставки видео-сообщений, а EC2 был спроектирован именно для работы с S3. Он позволяет для Voxlite простой и выгодный способ расширять свои мощности для поддержки большого количества пользователей. Мощности EC2 могут быть задействованы в любое время путем простого запуска образа виртуальной машины. Каждая такая машина стоит 10 центов в час или 72 доллара в месяц. Но что более всего привлекает в EC2, так это гибкость вычислительных ресурсов: виртуальные машины EC2 могут быть отключены, когда они не используется. Например, если у Voxlite больше трафика в дневное время, чем ночью, то можно подключать больше серверов днем, тем самым сильно повышая денежную эффективность решения в плане хостинга. Однако, большим минусом для EC2 является невозможность проектирования балансировки нагрузки на стороне сервера, у которого не было бы уязвимых мест. Многие веб-приложения размещаются на EC2 используя только одну виртуальную машину с динамическим DNS для балансировки нагрузки запросов к отдельному домену. Если сервер, обеспечивающий балансировку, отказывает, то вся система становится недоступной, пока динамический DNS не подключит домен к другой виртуальной машине.

При использовании балансировки на стороне клиента, описанной выше, становится возможным избежать этого неприятного момента и существенно повысить надежность всего решения. на базе серверов EC2. При построении кластера виртуальных машин EC2 для поддержки балансировки на клиенте Voxlite использует код и другие веб-ресурсы, размещенные и отдаваемые с помощью S3. Как только появляется виртуальная машина EC2, т.е. она полностью настроена и готова принимать запросы от клиентов, тогда Voxlite использует следующий подход для составления списка доступных для клиента серверов.

Чуть раньше я описывал использование файла servers.xml для оповещения клиента о доступных серверах, но для S3 можно использовать более простой способ. При обращении к сегменту (bucket) S3 (сегментом в S3 называют хранимую группу файлов; идея похожа на папки файлов) без каких-либо дополнительных аргументов, сервис просто перечисляет все ключи, соответствующие заданному префиксу. Таким образом, для каждой из виртуальных машин Voxlite на базе EC2 запускается по cron'у скрипт, который регистрирует сервер как часть общего кластера просто создавая пустой файл с ключами servers/{AWS IP адреса} в публично доступном сегменте S3.

Например, если я обращусь по адресу http://s3.amazonaws.com/voxlite/?prefix=servers, то получу примерно следующий ответ:

<ListBucketResult>
    <Name>voxlite</Name>
    <Prefix>servers</Prefix>
    <Marker/>
    <MaxKeys>1000</MaxKeys>
    <IsTruncated>false</IsTruncated>
    <Contents>
	<Key>servers/216.255.255.1</Key>
	<LastModified>2007-07-18T02:01:25.000Z</LastModified>
	<ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
	<Size>0</Size>
	<StorageClass>STANDARD</StorageClass>
    </Contents>
    <Contents>
	<Key>servers/216.255.255.2</Key>
	<LastModified>2007-07-20T16:32:22.000Z</LastModified>
	<ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag>
	<Size>0</Size>
	<StorageClass>STANDARD</StorageClass>
    </Contents>
</ListBucketResult>

В этом примере присутствуют два EC2 сервера в кластере, с IP адресами 216.255.255.1 и 216.255.255.2, соответственно.

Логика для скрипта, запускающегося по cron'у:

  1. Загрузить и разобрать http://s3.amazonaws.com/voxlite/?prefix=servers.
  2. Если текущий сервер отсутствует в списке, создать пустой файл в сегменте с ключом servers/{IP адрес EC2 сервера}.
  3. Проверить, доступны ли остальные сервера, записанные в сегменте, проверив связь до них, используя внутренний AWS IP адрес. Если связь установить не удается, то ключ сервера из сегмента удаляется.

Так как скрипт, запускающийся по cron'у, является частью виртуальной машины EC2, каждая такая машина автоматически регистрируется как доступный сервер в кластере. Клиентский код (AJAX или Flash) разбирает список ключей в сегменте и вычленяет внешнее имя AWS сервера, добавляет его в массив для случайного выбора при соединении, как описано выше при рассмотрении файла servers.xml. Если виртуальная машина EC2 отказывает или выключается, то другие машины самостоятельно убирают ее запись из сегмента: в сегменте остаются только доступные сервера. Дополнительно, клиент сам выбирает другой сервер EC2 в сегменте, если ответ не был получен в течение определенного времени. Если трафик на веб-сайт увеличивается, достаточно просто запустить больше серверов EC2. Если нагрузка уменьшается, можно часть из них отключить. Использование балансировки на стороне клиента при помощи S3 и EC2 позволяет легко создать гибкое, расширяемое и весьма надежное веб-приложение.

Читать дальше

Ссылки по теме и дополнительные статьи

Все комментарии (habrahabr.ru)