Решение одной задачи балансировки запросов к веб-приложениям
Как-то раз к нам обратилась одна компания с просьбой предложить решение по балансировке запросов к корпоративному веб-приложению. Проблема была в том, что сервер приложения начинал «тормозить» (читайте, не мог обеспечить приемлемое время отклика) в моменты, когда на приложение «наседало» много пользователей. И поскольку карта вертикального масштабирования сервера приложения уже была разыграна, в компании было принято решение запустить несколько экземпляров сервера приложения и распределить запросы пользователей между ними (см. рисунок ниже). Таким образом, в конечном итоге было запущено 3 сервера приложения, а на рабочем столе пользователя появились 3 ярлыка: каждый ярлык запускал сеанс пользователя на соответствующем сервере приложения.
Для решения описанных проблем нами были рассмотрены наиболее популярные подходы к балансировке нагрузки (следует отметить, что в каждом подходе используется свой инструмент балансировки, о чем можно догадаться по названию подхода):
1) DNS
2) Anycast
3) Load balancer
Подход №1. DNS
Подход с использованием сервиса DNS заключается в том, что DNS-сервер возвращает несколько DNS-записей, где каждая запись – это IP-адрес одного из серверов приложения. При этом, поскольку клиент (браузер), как правило, выбирает первую DNS-запись, для распределения запросов между серверами приложения DNS-сервер «перемешивает» DNS-записи в ответах. В случае недоступности одного из серверов приложения (например, отказ коммутатора подключения сервера приложения, или программная ошибка сервера приложения) соответствующая DNS-запись должна исключаться из списка возвращаемых DNS-сервером (эта функция может быть выполнена средствами скрипта).
Однако за простотой реализации подхода кроются нюансы. DNS-записи кэшируются на нескольких уровнях, включая операционную систему и браузер пользователя. Для каждой записи DNS-сервер указывает время «жизни» в кэше, но это время, как правило, игнорируется браузерами (к примеру, время «жизни» записи в кэше Internet Explorer версии 11 составляет 30 минут). Таким образом, браузер «узнает» измененный состав серверов, для которых выполняется балансировка, только при последующем DNS-запросе и при условии устаревания неактуальной DNS-записи в кэше.
Означает ли это, что переключение на работоспособный сервер занимает десятки минут? И да, и нет. Время переключения зависит как минимум от двух факторов: тип отказа и тип (реализация) браузера. Рассмотрим ситуацию отказа коммутатора, к которому подключен один из серверов приложения. Современные версии браузеров контролируют состояние TCP-сессий: если соединение к серверу неактивно (не может быть установлено), браузер устанавливает соединение к другому серверу в соответствии с известными ему DNS-записями. По нашим тестам на детектирование отказа и переключения на другой сервер приложения Internet Explorer версии 11 затрачивает ~21 секунду, Google Chrome версии 45 – ~100 секунд.
Но как поведет себя браузер, если сбой произошел на уровне сервера бизнес-логики (например, ошибка «500 Internal Server Error»)? Поскольку ошибки на уровне приложения никак не обрабатываются браузером, в данном случае переключение на работоспособный сервер произойдет только после получения браузером актуальной DNS-записи (напомню, в случае IE – 30 минут).
Подход №2. Anycast
Подход заключается в том, что все экземпляры приложения используют один и тот же IP-адрес на Loopback-интерфейсе, который анонсируется в сеть для каждого сервера приложения. Анонсирование IP-адреса Loopback-интерфейса выполняет программный маршрутизатор – виртуальная машина vRouter (пример реализации – Quagga). Альтернативный вариант: вместо отдельностоящего vRouter запустить процесс маршрутизации на сервере приложения, но на практике вариант с внесением измнений в серверы приложений не всегда доступен. Задачей vRouter также является контроль доступности экземпляра сервера приложения: в случае недоступности экземпляра сервера приложения (кстати, в отличие от подхода DNS механизм контроля доступности на vRouter может обнаруживать и ошибки уровня приложения путем пробных запросов к приложению) vRouter прекращает анонсировать IP-адрес сервера. Функцию распределения запросов выполняет механизм ECMP физического маршрутизатора (см. рисунок ниже).
Но у подхода Anycast есть свои нюансы. Для определения, по какому из маршрутов равной стоимости направить пакет, маршрутизатор в общем случае использует следующий алгоритм: hash(SRC_IP, DST_IP) mod NUM_OF_ECMP_ROUTES – результат операции (остаток от деления) является условным номером маршрута. Как можно заметить, все пакеты в рамках одной TCP-сессии будут направляться по одному маршруту… пока текущее количество маршрутов неизменно! Как только доступных маршрутов станет не 3, а 2 (например, произошел программный сбой одного из серверов приложения), 2/3 всех установленных TCP-сессий будут перераспределены маршрутизатором по новым маршрутам (подробнее, см. RFC 2992). Это означает переустановку подключения к приложению для 2/3 пользователей.
Другой нюанс подхода заключается в том, что алгоритм распределения нагрузки – равными долями между серверами приложения – является «врожденной» характеристикой ECMP и не может быть изменен. На практике производительность нового сервера пула может превосходить производительность «старичка» на порядок, и для того, чтобы соразмерно производительности серверов распределить запросы, требуется использовать или алгоритм с учетом «веса» сервера (алгоритм weighted round robin), или, более интеллектуально, направлять запросы к серверу с наилучшим временем отклика.
Подход №3. Load balancer (балансировщик нагрузки)
Подход характеризуется тем, что все запросы пользователей направляются на виртуальный IP-адрес балансировщика, который перенаправляет запрос к одному из экземпляров сервера приложения. Несомненным преимуществом данного подхода является то, что алгоритм распределения запросов может быть выбран администратором, например: равными долями (round robin), с учетом «веса» сервера (weighted round robin), сервер с наилучшим временем отклика – перечень алгоритмов ограничен лишь конкретной реализацией балансировщика.
В зависимости от того, какой механизм используется балансировщиком для направления запросов на серверы приложений, выделяют следующие сценарии работы балансировщика:
1) L2/ Direct Server Return
2) L3/ NAT
Все экземпляры приложения используют один и тот же IP-адрес на Loopback-интерфейсе, и такой же виртуальный IP-адрес настраивается на балансировщике (см. рисунок ниже). Для того чтобы избежать конфликта копий IP-адреса в одном широковещательном сегменте, используется следующий трюк: серверы приложения должны быть сконфигурированы таким образом, чтобы игнорировать ARP-запросы к IP-адресу приложения, которые генерирует маршрутизатор, на эти ARP-запросы должен отвечать балансировщик. Благодаря такому трюку в ARP-кэш маршрутизатора помещается следующая запись: «IP-адресу приложения соответствует MAC-адрес балансировщика». Таким образом, все запросы пользователей к приложению маршрутизируются на балансировщик, после чего балансировщик перенаправляет запросы пользователей к конкретному серверу, выполняя подмену MAC-адреса назначения фреймов на MAC-адрес сервера (содержимое IP-пакета при этом никак не изменяется).
Интересный момент в том, что ответный трафик от сервера к пользователю направляется сервером на шлюз по умолчанию (маршрутизатор), минуя балансировщик. В силу того, что балансировщик высвобожден от обработки возвратного трафика от сервера к клиенту, сценарий DSR дает выигрыш в производительности (по сравнению с вариантом обработки и прямого, и возвратного трафика), который в большей степени заметен, когда трафик имеет резко асимметричный характер, а именно, когда размер запросов значительно меньше размера ответов.
Вероятно, самым существенным ограничением при применении сценария является то, что серверы приложения и балансировщик должны принадлежать одному L2 сегменту. Данное требование ограничивает применение сценария в случаях, когда серверы приложения разнесены по разным подсетям, к примеру, расположены на разных площадках (какие последствия у «растягивания» L2 между площадками читайте, например, в нашей библиотеке).
Другим ограничением является необходимость внесения изменений в настройки приложений и стека TCP/IP серверов, что не всегда представляется возможным, например, когда сервер не находится под контролем сетевого администратора или не допускает внесения изменений.
Краткий вывод: сценарий L2/ DSR наилучшим ыобразом подходит для балансировки запросов к серверам в том случае, когда акцент делается на обеспечении производительности балансировщика, а серверы приложения должны быть расположены на одной площадке.
В данном сценарии все запросы пользователей направляются на виртуальный IP-адрес балансировщика, который в свою очередь перенаправляет запрос к одному из экземпляров сервера приложения, выполняя преобразование IP-адреса назначения (destination NAT). Тонкий момент заключается в том, что в момент применения NAT балансировщик терминирует TCP-сессию с клиентом (браузером) и устанавливает другую (параллельную) TCP-сессию с сервером приложения. Таким образом, поскольку TCP-сессия установлена между балансировщиком и сервером приложения ответный трафик от сервера должен быть направлен на балансировщик. Существуют следующие варианты решения локальной задачи направления ответного трафика через балансировщик:
1) Балансировщик выступает в качестве шлюза по умолчанию для серверов приложений (см. рисунок ниже). Как можно заметить, поскольку балансировщик используется на месте маршрутизатора по умолчанию, данный сценарий применения балансировщика требует перестроения сетевой топологии.
2) Ответный трафик направляется на балансировщик за счет маршрутизации. В этом варианте не избежать внесения изменений в настройки маршрутизации сети.
3) Full NAT: вместе с подменой IP-адреса назначения, destination NAT, балансировщик выполняет подмену IP-адреса источника запроса – source NAT (см. рисунок ниже). В этом случае не требуется, как в предыдущих вариантах, вносить изменения в сетевую топологию, при этом серверы и балансировщик могут находиться в разных сегментах сети, т. е. быть разделены маршрутизаторами.
С моей точки зрения, сценарий Full NAT применения балансировщика является наиболее предпочтительным, поскольку не требует внесения изменений в существующую сетевую топологию, и поэтому далее именно сценарий Full NAT рассматривался для решения задачи компании.
Хочу отметить, что до этого момента рассуждения о том, какой подход к решению задачи балансировки применить, имели достаточно общий характер и учитывали лишь тот факт, что взаимодействие клиент – сервер происходит по протоколу TCP.Так ли важно, как устроено конкретное приложение? Давайте рассмотрим этот вопрос подробнее.
Рассмотрим взаимодействие клиента с приложением. Каждый запрос пользователя веб-страницы в соответствии с тем, как работает большинство браузеров, приводит к открытию 6 TCP-сессий для каждого домена-источника данных (подробнее), в которых выполняется в среднем ~100 HTTP-запросов. Веб-сервер обрабатывает часть запросов, возвращая клиенту статический контент (как, например, картинки), а другую засть запросов транслирует на уровень бизнес-логики. В силу того, что протокол HTTP не имеет состояния (каждый последующий HTTP-запрос обрабатывается веб-сервером независимо от предыдущих запросов), в общем-то, не важно, как балансировщик распределит HTTP-запросы пользователя между несколькими веб-серверами. Тонкий момент заключается в том, могут ли запросы одного пользователя быть распределены между несколькими серверами бизнес-логики. Здесь могут быть следующие варианты:
1) Серверы бизнес-логики хранят состояние сессии (сеанса) пользователя. Состояние сеанса пользователя можно описать как «память» о предыдущих HTTP-запросах пользователя, в зависимости от которой должен быть обработан текущий HTTP-запрос. Пример: пользователь вносит в приложение данные нового клиента компании, при этом данные клиента вносятся поэтапно, на нескольких веб-страницах, и пока все данные о клиенте не будут внесены и проверены, не имеет смысла отправлять данные на уровень хранения данных – временные данные будут храниться в памяти сервера бизнес-логики как состояние сеанса.
2) Серверы бизнес-логики не хранят состояние сессии пользователя (каждый HTTP-запрос обрабатывается сервером бизнес-логики независимо от предыдущих запросов).
В случае рассматриваемого веб-приложения серверы бизнес-логики хранили состояние сеанса для каждого отдельного пользователя приложения. Как в этом случае должны распределяться запросы между серверами бизнес-логики? И опять варианты:
1) Серверы бизнес-логики синхронизируют между собой состояния сеансов пользователей. В этом случае не важно, на какой из серверов бизнес-логики «приземлится» следующий HTTP-запрос, поскольку каждый сервер бизнес-логики имеет доступ к состоянию сеанса каждого пользователя. Реализацией данного подхода является технология Oracle Application Server Cluster.
2) Каждый сервер бизнес-логики хранит известное ему состояние сессий пользователей, отличное от состояний других серверов бизнес-логики. В этом случае, для того, чтобы сервер бизнес-логики мог корректно обработать последовательные запросы пользователя, все запросы пользователя в течение сеанса должны «приземляться» на один и тот же сервер бизнес-логики – требование постоянства сессии (session persistence).
Поскольку веб-приложение компании не использовало технологии кластеризации Oracle, задача обеспечения требования постоянства сессии ложилась на балансировщик.
Далее требовалось рассмотреть возможные варианты обеспечения постоянства сессии пользователя, которые мог бы использовать балансировщик:
1) «Привязать» пользователя к его сетевому идентификатору – IP-адресу: таким образом все HTTP-запросы, источником которых является отдельный IP-адрес, будут всегда направляться на один и тот же веб-сервер и соответственно сервер бизнес-логики. Простота данного подхода имеет свою цену: если пользователями приложения используется Source NAT, и значительное число пользователей «скрывается» за одним IP-адресом источника, балансировщик запросов будет не в состоянии корректно (в соответствии с установленными долями) распределять запросы между серверами приложения.
2) «Читать» PDU на уровне приложения и распознавать идентификаторы сессий пользователей (cookie). В случае, когда протокол доступа к приложению – HTTPS, для доступа к данным уровня приложения балансировщик должен дешифровать трафик, терминируя TLS сессий пользователей.
Из двух вариантов обеспечения постоянства сессии пользователя я бы рекомендовал второй, поскольку он избавлен от указанного выше недостатка при работе пользователей через NAT.
Надеюсь, данная статья станет хорошим стартом для тех, кто интересуется темой балансировки запросов к веб-приложениям и укажет вектор для дальнейших исследований этой темы.
Комментариев пока нет
Добавить комментарий