Книга: Site Reliability Engineering. Надежность и безотказность как в Google
Назад: 18. Разработка ПО службой SRE
Дальше: 20. Балансировка нагрузки в дата-центре

19. Балансировка нагрузки на уровне фронтенда

Автор — Петр Левандовски

Под редакцией Сары Чевис

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

В этой главе рассматривается вопрос высокоуровневой балансировки нагрузки — как мы балансируем пользовательский трафик между дата-центрами. В следующей главе рассматривается вопрос балансировки нагрузки внутри дата-центра.

Мощность — это не ответ

Чисто теоретически предположим, что мы располагаем невероятно мощной машиной и сетью, которая никогда не дает сбоев. Будет ли достаточно такой конфигурации для удовлетворения потребностей компании Google? Нет. Даже при такой конфигурации нас сдерживали бы физические ограничения, связанные с нашей сетевой инфраструктурой. Например, скорость света — это ограничива­ющий фактор для скорости обмена данными посредством оптоволоконного кабеля, устанавливающего верхнюю границу того, как быстро мы можем выдавать данные в зависимости от расстояния, которое необходимо преодолеть. Даже в идеальном мире полагаться на инфраструктуру, имеющую единственную «точку отказа», — плохая идея.

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

Уровень иерархии, на котором мы решаем задачу (глобальный или локальный).

• Технический уровень, на котором мы решаем задачу (аппаратное обеспечение или программное).

• Природа обрабатываемого трафика.

Начнем с рассмотрения двух распространенных сценариев работы с трафиком: простой поисковой запрос и запрос на загрузку видеоролика. Пользователи хотят получить результат выполнения своего запроса быстро, поэтому наиболее важная характеристика поискового запроса — задержка отклика. С другой стороны, пользователи ожидают, что загрузка видеороликов займет некоторое время, но также хотят, чтобы такой запрос выполнялся с первого раза, поэтому наиболее важной характеристикой такого запроса будет пропускная способность. Различающиеся потребности для этих двух запросов имеют значение для выбора, как мы определяем оптимальное распределение для каждого запроса на глобальном уровне.

Поисковой запрос отправляется в ближайший — согласно измеренному значению времени обращения запроса или «круговой задержки» к сети (round-trip time, RTT) — доступный дата-центр, поскольку мы хотим минимизировать задержку отклика для этого запроса.

• Поток загрузки видеоролика направляется по другому пути — скорее всего, на узел, который в данный момент мало используется — для максимизации пропускной способности ценой повышения задержек.

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

Конечно же, в этом примере показана упрощенная картина. В реальности на оптимальность распределения нагрузки влияет большее количество факторов: некоторые запросы могут быть направлены в дата-центр, находящийся чуть дальше, для того, чтобы кэши оставались «теплыми», или, например, для предотвращения перегрузки сети неинтерактивный трафик может быть направлен в совершенно другой регион. В Google эта задача решается путем балансировки нагрузки на нескольких уровнях, два из которых описаны в следующих разделах. Для большей определенности мы рассмотрим запросы HTTP, передаваемые через TCP. Балансировка нагрузки для сетевых сервисов, работающих без сохранения состояния (вроде DNS поверх UDP), несколько отличается, но большая часть механизмов, описанных здесь, может быть применима и к ним.

Балансировка нагрузки с использованием DNS

Прежде чем клиент сможет отправить запрос HTTP, ему обычно нужно определить IP-адрес с помощью DNS. Это дает нам идеальную возможность продемонстрировать наш первый уровень балансировки нагрузки — балансировку нагрузки на уровне DNS. Самым простым решением будет возвращать в ответах DNS сразу несколько записей типа А или АААА, содержащих различные IP-адреса, и позволить клиенту выбрать один из предложенных адресов произвольным образом. Несмотря на то что реализация такого подхода кажется тривиальной, она порождает несколько проблем.

Первая проблема состоит в том, что мы почти не контролируем поведение клиента: записи выбираются случайным образом, и за каждой из них стоит примерно одинаковый объем трафика. Как мы можем справиться с этой проблемой? Теоретически мы могли бы указать веса и приоритеты возвращенных записей с помощью записей типа SRV, но они пока не применимы для протокола HTTP.

Еще одна потенциальная проблема связана с тем, что клиент, как правило, не может определить ближайший к нему адрес. Проблема частично решается использованием для обращения к ответственным (authoritative) серверам имен «широковещательного» адреса в расчете на то, что запросы DNS будут попадать на ближайший адрес. В своем ответе сервер может возвращать маршрут к ближайшему дата-центру в виде цепочки адресов. Следующим усовершенствованием будет создание карты всех сетей и их приблизительных физических местоположений, а на основе этой карты будут обслуживаться DNS-запросы. Однако для этого потребуется гораздо более сложная реализация DNS-сервера, а также служебный процесс, который бы поддерживал актуальность карты.

Конечно, ни одно из этих решений не оказывается тривиальным из-за фундаментальной особенности работы DNS: конечные пользователи редко общаются непосредственно с ответственными серверами имен. Вместо этого где-то между ними обычно располагается рекурсивный DNS-сервер. Он проксирует (и зачастую кэширует) запросы между пользователем и сервером. Можно выделить три важнейших аспекта влияния промежуточного (middleman) DNS-сервера на управление трафиком:

рекурсивное разрешение имен в IP-адреса;

• возврат ответов по нефиксированным путям;

• дополнительное усложнение вследствие кэширования.

Проблемы при рекурсивном разрешении IP-адресов связаны с тем, что IP-адрес, видимый ответственному серверу имен, принадлежит не конечному пользователю, а промежуточному рекурсивному серверу, участвующему в разрешении имени. Это очень серьезное ограничение, поскольку из-за него оптимизация ответа возможна лишь для ближайшего отрезка между ответственным и промежуточным серверами. В качестве возможного решения мы можем использовать расширение EDNS0, предложенное в [Contavalli, 2015], которое добавляет информацию о подсети клиента в запрос DNS, отправляемый рекурсивным сервером. Таким образом, ответственный сервер имен возвращает ответ, оптимальный с точки зрения конечного пользователя, а не промежуточных серверов-преобразователей. Хотя такой подход все еще не является официальным стандартом, его очевидные преимущества привели к тому, что крупнейшие DNS-серверы (например, OpenDNS и Google) уже начали его поддерживать.

Трудность заключается не только в поиске оптимального IP-адреса, который будет возвращен на сервер имен для заданного запроса пользователя, но и в том, что в зоне ответственности сервера имен могут находиться тысячи или миллионы пользователей на территориях от одиночного офиса до целого континента. Например, крупный национальный интернет-провайдер может запустить серверы имен для всех своих сетей в одном дата-центре, уже имея соединения с сетями всех агломераций. DNS-серверы этого провайдера вернут IP-адрес, наилучшим образом подходящий для их дата-центра, игнорируя иные сетевые пути, даже если они лучше подходят для всех пользователей!

Наконец, рекурсивные серверы имен обычно кэшируют ответы и повторно возвращают их на аналогичные запросы в пределах времени, указанного в поле TTL (time-to-live, «время жизни») в записи DNS. В результате становится трудно предсказать масштаб влияния каждого ответа ответственного сервера: любой из них может быть направлен как одному пользователю, так и тысячам. Мы решаем эту проблему двумя способами.

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

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

Оценка географического расположения может быть особенно сложной, если пользовательская база распределена между крупными регионами. В таких случаях при определении наилучшего местоположения мы идем на компромиссы и стараемся сделать результат оптимальным для большинства пользователей.

Но что на самом деле означает фраза «наилучшее местоположение» в контексте балансировки нагрузки с использованием DNS? Наиболее очевидный ответ —это наиболее близкое к пользователю местоположение. Однако (как если бы определение местоположения пользователя не было трудной задачей само по себе) существует дополнительное требование. Балансировщик нагрузки DNS должен убедиться, что выбранный им дата-центр имеет достаточную производительность, чтобы обслужить запросы пользователя. Он также должен знать, что выбранный дата-центр и его сетевое подключение находятся в хорошем состоянии, поскольку не стоит направлять запросы туда, где есть проблемы. К счастью, мы можем обеспечить взаимодействие ответственного сервера DNS с нашими глобальными системами мониторинга трафика, производительности и состояния нашей инфраструктуры.

Третий аспект влияния промежуточного DNS-сервера связан с кэшированием. Учитывая, что ответственные серверы имен не могут очищать кэши промежуточных серверов, записи DNS должны иметь относительно небольшое значение TTL. Это, по сути, устанавливает нижнюю границу скорости распространения пользователям изменений в DNS (увы, не все DNS-серверы соответствуют значению TTL, установленному ответственными серверами). К несчастью, мы можем лишь иметь в виду эту проблему при принятии решений по балансировке нагрузки.

Несмотря на все эти проблемы, DNS все еще является самым простым и самым эффективным способом сбалансировать нагрузку еще до того, как будет установлено соединение с пользователем. С другой стороны, должно быть очевидно, что использования лишь DNS будет недостаточно. Имейте также в виду, что все ответы DNS должны соответствовать ограничению в 512 байт, установленному RFC 1035 [Mockapetris, 1987]. Тем самым лимитируется количество адресов, которое можно вместить в один ответ DNS, и это число, скорее всего, будет значительно меньше, чем количество наших серверов.

Чтобы обеспечить полноценную балансировку нагрузки на уровне фронтенда, начальный уровень балансировки нагрузки с использованием DNS должен быть дополнен уровнем, использущим виртуальный IP-адрес.

Балансировка нагрузки с использованием виртуального IP-адреса

Виртуальные IP-адреса (virtual IP address, VIP) не присваиваются определенным сетевым интерфейсам. Вместо этого их обычно делят несколько устройств. Однако с точки зрения пользователя VIP остаются обычными IP-адресами. В теории такой прием позволяет нам скрыть детали реализации (например, количество машин, находящихся за каждым VIP) и облегчает обслуживание сети, поскольку мы можем запланировать обновления или добавить новые машины в пул незаметно для пользователя.

На практике наиболее важной частью реализации VIP является устройство, называемое сетевым балансировщиком нагрузки. Балансировщик получает пакеты и перенаправляет их одной из машин, которые лежат за VIP. Эти бэкенды выполняют последующую обработку запроса.

Существует несколько подходов, которыми может воспользоваться балансировщик при определении того, какой бэкенд должен получить запрос. Первый (и, возможно, наиболее интуитивный) подход заключается в том, что балансировщик всегда должен отдавать предпочтение наименее загруженному бэкенду. Теоретически это должно приводить к наиболее быстрому выполнению запроса пользователя, поскольку он направляется на наименее загруженную машину. К сожалению, эта логика не работает для протоколов с хранением состояния, поскольку они должны использовать один и тот же бэкенд на протяжении всего процесса выполнения запроса. Это требование означает, что балансировщик должен следить за всеми соединениями, чтобы убедиться в том, что все остальные пакеты будут отправлены на правильный бэкенд. В качестве альтернативы мы можем использовать некоторые части пакета для создания идентификатора соединения (возможно, используя хеш-функцию и какие-то данные из пакета) и задействовать этот идентификатор для выбора бэкенда. Например, идентификатор может быть выражен следующим образом:

id(packet) mod N,

где id — это функция, которая принимает параметр packet и создает идентификатор соединения, а N — это количество участвующих в балансировке бэкендов.

Это позволяет избежать необходимости сохранять состояние, и все пакеты, относящиеся к одному соединению, всегда будут направляться на один и тот же бэк­енд. Мы решили проблему? Не совсем. Что случится, если один бэкенд откажет и его нужно будет убрать из списка бэкендов? Внезапно N превращается в N 1, и функция принимает вид id(packet) mod N 1. Практически каждый пакет теперь указывает на другой бэкенд! Если бэкенды не делятся друг с другом информацией о состоянии, такое изменение приведет к сбросу всех существующих соединений. Этот сценарий нельзя считать хорошим для пользователя, и неважно, что это будет происходить редко.

К счастью, существует альтернативное решение, которое не требует хранения в памяти состояния каждого соединения и не приводит к сбросу всех соединений при сбое одного бэкенда: консистентное хеширование (consistent hashing). Этот подход появился в 1997 году, он [Karger, 1997] описывает способ соотнесения, который остается относительно стабильным даже при добавлении или удалении бэкендов из списка. Этот подход минимизирует ущерб для существующих соединений при изменении пула бэкендов. В результате мы можем использовать простые средства контроля соединений в обычной ситуации и откатываться к консистентному хешированию, если система подвергается давлению (например, во время DoS-атаки).

Вернемся к основному вопросу: как именно сетевой балансировщик нагрузки должен направлять пакеты к выбранному VIP-бэкенду? Одно из решений заключается в использовании механизма трансляции сетевых адресов (Network Address Translation, NAT). Однако это потребует хранения информации о каждом соединении в специальной таблице, что не позволит сделать механизм восстановления после сбоя действительно свободным от хранения состояния.

Еще одним решением является модификация информации на канальном уровне (второй уровень сетевой модели OSI). Изменив MAC-адрес получателя перенаправляемого пакета, балансировщик может оставить всю информацию на верхних уровнях без изменений, и бэкенд получит оригинальные IP-адреса источника и места назначения. Далее бэкенд может отправить ответ непосредственно отправителю — этот прием известен как прямой ответ сервера (Direct Server Response, DSR). Если запросы малы, а ответы велики (это справедливо для многих запросов HTTP), DSR позволяет сэкономить много времени, поскольку через балансировщик нагрузки должна пройти лишь малая доля трафика. И даже лучше, DSR не требует от нас хранить состояние на устройстве балансировщика нагрузки. К сожалению, использование второго уровня OSI для внутренней балансировки нагрузки имеет серьезные недостатки при развертывании в больших масштабах: все машины (например, все балансировщики нагрузки и все их бэкенды) должны иметь возможность связаться друг с другом на канальном уровне. Это не является проблемой, если сеть поддерживает такие соединения, и если число машин не увеличивается слишком быстро, поскольку все они должны оставаться в одном широковещательном домене. Как вы понимаете, компания Google переросла это решение довольно давно, и нам пришлось искать альтернативный подход.

Наше текущее решение по балансировке нагрузки для VIP [Eisenbud, 2016] использует инкапсуляцию пакетов. Сетевой балансировщик нагрузки помещает (инкапсулирует) перенаправляемый пакет в другой пакет IP в соответствии с протоколом туннелирования сетевых пакетов (Generic Routing Encapsulation, GRE) [Hanks, 1994] и использует в качестве адреса получателя адрес бэкенда. Бэкенд, получающий пакет, снимает внешний слой IP+GRE и обрабатывает внутренний пакет IP, как если бы тот был доставлен непосредственно на его сетевой интерфейс. Сетевому балансировщику нагрузки и бэкенду больше нет необходимости находиться внутри одного широковещательного домена; до тех пор пока между ними имеется проложенный через «туннель» маршрут, они даже могут находиться на разных континентах.

Инкапсуляция пакетов — это мощный механизм, который предоставляет большую гибкость для наших сетей, учитывая их проект и способ развития. К сожалению, за инкапсуляцию приходится платить увеличением размера пакета. Из-за такого «довеска» (если быть точным, 24 байта в случае IPv4+GRE) может быть превышен максимальный доступный размер пакета (Maximum Transmission Unit, MTU), и ему потребуется фрагментация.

Когда пакет достигнет дата-центра, фрагментации можно будет избежать, используя внутри дата-центра большее значение MTU; однако этот подход требует, чтобы сеть поддерживала протоколы с большим размером сообщений. Как и во многих других случаях работы с крупномасштабными системами, задача балансировки нагрузки на первый взгляд проста, но ее сложность кроется в деталях, как при балансировке нагрузки на уровне фронтенда, так и при обработке пакетов внутри дата-центра.

Сам по себе HTTP считается протоколом без сохранения состояния, но используемый им TCP — протокол с сохранением состояния. Служба DNS и используемый ею протокол UDP — без сохранения состояния. — Примеч. пер.

См.: .

В противном случае пользователи должны будут устанавливать TCP-соединение только для того, чтобы получить список IP-адресов.

Назад: 18. Разработка ПО службой SRE
Дальше: 20. Балансировка нагрузки в дата-центре