Тема задержки доступа и скорости извлечения сетевых ресурсов никогда не перестанет быть актуальной. Максимально близкое расположение источника влияет не только на скорость загрузки и пользовательский опыт, но и на эффективность работы глобальной сети в целом, поскольку позволяет локализовать трафик и сократить загрузку магистральных каналов, предпочитая использовать кэшированные или расположенные локально реплики сетевых ресурсов. Не случайно Google реализует модель сохранения локальных кэшей на оборудовании крупных региональных провайдеров (Google Global Cache) и интеллектуальные алгоритмы в маршрутизации на ближайшую реплики. В этой статье мы обсудим различные подходы к реализации распределенной сети доставки контента (Content Delivery Network, он же CDN), а также акцентируем возможные решения для создания CDN в масштабах отдельно взятой страны или города.
Прежде всего нужно дать определение CDN и выяснить какие виды сетей могут существовать. Технически, в зависимости от происхождения контента, можно выделить CDN для доступа к статическим ресурсам (в этом случае содержание заранее загружено на origin-сервер и далее реплицируется по точкам присутствия - Point-of-Presence, далее PoP, их также называют Edge Server) или потоковые CDN для ретрансляции медиапотока в реальном времени (это может быть видеотрансляция, как например стримы в Youtube или прямая трансляция радиостанции). Разделение между типами CDN весьма условно и скорее нужно для акцентирования внимания на способ взаимодействия между сервером-источником содержания и ретрансляторами или репликами на точках присутствия. В случае стриминговых CDN зачастую существует выделенная сеть (или канал с гарантированной пропускной способностью) между узлами сети для своевременной доставки медиапотока, для CDN со статическими ресурсами такого требования нет, но в любом случае должна обеспечиваться согласованность метаданных между точками присутствия и наличие одного источника истины об актуальной версии содержания.
При создании CDN необходимо решить несколько технических задач:
синхронизация обновлений содержания между сервером-источником (origin) и точками присутствия (PoP), в том числе контроль актуальности реплик и инвалидация кэширующих серверов (если используется кэш в памяти для хранения часто извлекаемого содержания);
поиск локальной точки присутствия (по принципу географической близости, минимального количества сетей до автономной системы точки присутствия или сетевых переходов до локального сервера);
мониторинг доступности точек присутствия и динамическое изменение топологии маршрутизации при потере сетевой связности;
для динамического содержания может быть важно выполнение обработки данных и каких-либо вычислений и трансформаций на стороне точки присутствия (edge computing)
Последовательно разберем каждую из задач и посмотрим на возможные механизмы технической реализации:
Синхронизация обновлений
Наиболее важной задачей при создании распределенной сети хранения является обеспечение актуальности версий содержания по всем точкам присутствия и своевременное обновление содержания при появлении изменений на сервере-источнике. Это может быть достигнуто как в push-модели (когда источник самостоятельно подключается к репликам и выполняет обновление), так и в pull-сценарии, когда узлы самостоятельно запрашивают изменения на источнике при обнаружении признаков устаревания содержания (например, могут использоваться HTTP-заголовки Cache-Control и Expire для маркировки ресурсов как устаревших).
Первый сценарий может быть реализован либо созданием очереди с обновлениями на стороне сервера источника, из которой последовательно извлекаются ресурсы и происходит непосредственно отправка обновлений на точки присутствия (через ftp, scp или даже rsync, который также умеет отправлять только измененные ресурсы). Основной недостаток такого подхода - большая загрузка исходящих каналов от origin-сервера при появлении обновлений содержания, а также вероятность рассинхронизации с точками присутствия, которые были недоступны на этапе рассылки обновлений). Кроме того, есть вероятность отдать клиенту неполный файл, если запрос будет выполнен в тот момент, когда происходит передача обновления с origin-сервера (вероятность не очень высокая, но реальная и с таким можно было встретиться при скачивании дистрибутивов или пакетов Linux с mirror-серверов). Частично эти проблемы могут быть решены созданием иерархии распространения обновлений (когда origin сервер распространяется по крупным региональным узлам, которые дальше пересылают по локальным серверам) или использование более сложной топологии (например, когда рассылка происходит группами распространения, от origin к нескольким точкам присутствия, каждая из которых рассылает обновление еще на заданное количество серверов, конечно в этом случае все серверы должны знать полную топологию сети или хотя бы владеть информацией о N ближайших соседях). Также и контроль корректности ресурса может быть достигнут через сохранение предыдущей версии обновляемого файла (и возврат её по запросу клиента) до момента полной загрузки обновления. Но остается проблема потери сетевой связности, когда один или группа узлов может не получить обновление из-за временной недоступности, и продолжать отдавать устаревшее содержание. Ее можно решить через добавление временной метки для метаданных файла и запроса обновлений с ближайшего узла при восстановлении доступа (для этих целей также можно использовать rsync). Для синхронизации можно использовать rsync (не выполняет шифрование при передаче, может быть потенциально уязвим для атак Man-in-the-Middle), rclone (поддерживает шифрование и аутентификацию узлов) и специализированные решения, например syncthing. Основное достоинство данного подхода - отсутствие эффекта "холодного старта", когда возникает задержка при первом обращении из-за необходимости получения копии ресурса с origin-сервера или peer-узла, расположенного топологически близко к нашей точке присутствия.
Альтернативный способ обновления реплик - использование pull-модели с обнаружением необходимости инвалидации данных по заголовкам содержания. В этом случае при возникновении клиентского запроса к ресурсу (по URI или UUID, в зависимости от принятой схемы) могут возникнуть два сценария:
ресурс не представлен на реплике - в этом случае отправляется запрос на origin-сервер для извлечения актуальной версии ресурса, копия сохраняется в локальное хранилище (в память, если используется модель с In-Memory базами данных для кэширования статических ресурсов или в локальную систему хранения) и отдается клиенту. Здесь может возникнуть проблема "холодного старта", когда первый клиент получит ресурс с задержкой (необходимой для подключения к первоисточнику и копирования ресурса на точку присутствия).
ресурс существует на точке присутствия - дополнительно отправляется HEAD-запрос на origin-сервер (или на топологически близкую точку присутствия) и проверяются заголовки ответа на актуальность (здесь может использоваться хэш содержания и/или заголовки Expire). В случае обнаружения устаревания содержания или различия в хэшах - переходим на предыдущий сценарий. Во всех остальных случаях отдаем статический ресурс из локального кэша/хранилища точки присутствия.
Одной из проблем такого подхода является кратковременная задержка при первом обращении к кэширующему серверу при появлении нового ресурса или обновлении существующего. Кроме того, для каждого запроса клиента нужно отправлять дополнительный запрос на origin-сервер, что может занимать значительное время (если между серверами cdn не организованы каналы с гарантированной пропускной способностью) и увеличивать время задержки при доступе к ресурсу. Частично эту проблему можно решить через поиск близких (по сетевой задержке или по количеству сетевых переходов) узлов для запроса информации об актуальности, но в этом решении надо следить за образованием "пузырей", когда локальные серверы в некотором сегменте начинают запрашивать информацию только у ближайших соседей и, в результате, никогда не узнают о появлении обновлений на origin-сервере. Основное достоинство такого решения - уменьшение количества всплесков распространения (при обновлении ресурса на origin-сервере) за счет появление равномерного фона из head-запросов.
Решить проблему обновления ресурсов можно через создание глобального реестра (альманаха) ресурсов с указанием адресов узлов, содержащих реплику для конкретной версии содержания (для идентификации и адресации ресурса используется хэш-код содержания и такие системы хранения можно считать "контентно-адресуемыми"). Альманах может актуализироваться по расписанию или реплицироваться через рассылку изменений, а для доступа и распространения самих ресурсов могут использоваться децентрализованные p2p-сети на основе распределенной хэш-таблицы (Distributed Hash Table - DHT, например свободная библиотека python-p2p-network).
Поиск локальной точки присутствия
Для эффективной работы CDN нужно обеспечить устойчивый механизм обнаружения наиболее близкой точки присутствия к клиенту. При использовании географически распределенных CDN наиболее часто используется специальная версия DNS-сервера BIND, которая получила название GeoDNS (или GeoIP) с использованием свободных баз данных MaxMind для привязки IP-адресов к географическому местоположению (с точностью до города в некоторых странах, в России корректно определяет только крупные города). Альтернативным решением считается AnyCast, механизм использования общего адреса с обнаружением ближайшего сервера на уровне сетевой маршрутизации в IPv6 или через Border Gateway Protocol (BGP) на IPv4. AnyCast использует информацию о переходах до автономных систем для определения наиболее короткого пути до целевого сервера из автономной системы клиента. К сожалению, для корректной работы AnyCast он должен поддерживаться на уровне провайдера, и многие действующие операторы некорректно обрабатывают такие групповые адреса. Также можно использовать облачные DNS с поддержкой GeoIP (например, ClouDNS или Amazon Route 53), но сейчас это может быть затруднительно из-за сложностей оплаты интернет-сервисов.
Третий вариант - хранение на клиенте альманаха доступных серверов и периодический замер времени RTT (round-trip-time, например через отправку ICMP echo-запроса) и выбор наименьшего времени или TTL (учитывает количество переходов). Это бывает затруднительно сделать непосредственно на клиенте, поскольку требует доработки программного обеспечения, но может быть сделано на локальном кэширующем сервере (например, можно использовать OpenResty).
Особый интерес представляет определение ближайшей сети внутри сети одного провайдера или между сетями различных провайдеров, функционирующих внутри одной страны, для которых механизмы GeoIP работают не очень надежно (в большей степени они подходят для выбора точки присутствия на уровне страны). При использовании мобильной сети (особенно это касается вышек 4G и 5G, которые поддерживают локальные сервисы, доступные непосредственно через ретранслятор сотовой сети, которые принято называть Edge Computing) можно либо размещать кэширующие серверы сети CDN непосредственно на оборудовании вышек, либо ретранслировать запросы на ближайшие серверы с использованием информации о топологии соединения оборудования сотовой связи). Для развертывания собственного CDN с использованием сети провайдера (или между провайдерами) можно использовать self hosted CDN-решения, такие как OpenRAP, WoodCDN, KubeCDN. Поскольку для этих решений доступны исходные тексты, они могут быть расширены с учетом специальных алгоритмов анализа сетевой топологии (например, доступной из внутренней информации о сети провайдера), либо с использованием периодических замеров сетевой задержки до точек присутствия). Альтернативное решение - использование серверов Global Server Load Balancing (GSLB), например polaris-gslb, которые выполняют интеллектуальное разрешение DNS-имени в IP-адрес узла, подходящего по критериям сетевой близости, минимальной задержки и наименьшей загрузки канала связи.
Мониторинг доступности точек присутствия
Проверка доступности узлов сети может быть интегрирована в процесс обновления списка доступных реплик (при получении сетевых метрик), но поскольку он происходит относительно редко, можно получить ситуацию пересылки запросов клиентов на уже недоступные узлы в течении длительного времени. Для определения реестра доступных для пересылки точек присутствия можно использовать механизмы распределенного DNS со встроенной проверкой сетевого подключения, например можно использовать решение Consul от HashiCorp. Обычно Consul используется для регистрации и обнаружения микросервисов в распределенной системе (Service Discovery) с учетом возможности запуска реплик. Одной из важных особенностей Consul является распределенный характер хранения метаданных сервисов (используется протокол Raft для достижения консенсуса в кластере Consul-узлов), а также интегрированные возможности проверки доступности сервиса (через http/grpc-запрос, tcp-подключение или запуск произвольного сценария в контейнере). Таким образом, Consul может предоставить список доступных экземпляров сервиса CDN-узла (при этом сами метаданные могут находиться максимально близко к серверу, запрашивающего информации о топологии CDN-сети) и использоваться для поиска наиболее близкой точки присутствия для переадресации запроса. Дополнительно Consul предоставляет веб-интерфейс для отслеживания текущего состояния узлов и оповещения при изменении доступности.
Edge Computing на точках присутствия
Основной сценарий, когда может понадобиться выполнение вычислений или обработки данных на edge-серверах, это динамическая генерация содержания по запросу пользователя (например, поиск информации) или использование контекста запроса (в том числе геолокации) для создания релевантного ответа. Здесь необходимо решить сразу две задачи - запуск распределенного приложения и размещение реплики необходимых для его выполнения данных. Идеальная ситуация - когда приложение запускается в одном процессе с базой данных, но может быть реализован и вариант запуска реплики базы данных на каждом узле, достаточной для выполнения вычислительной задачи и распределенного приложения, которое может анализировать запрос пользователя и формировать необходимый ответ локально, без дополнительных сетевых запросов. Для реализации первого подхода (он также подразумевает возможность сохранения текущего состояния взаимодействия с клиентом) можно рассмотреть использование распределенных баз данных с поддержкой запуска полноценных приложений, например Apache Ignite или Tarantool. Также можно применять концепцию распределенных приложений, работающих над единой распределенной базой транзакций (в целом подход получил название Distributed Apps и рассматривает, в том числе, запуск приложений в blockchain-сетях, например Ethereum). Во втором случае ключевую роль играет база данных и ее возможности по поддержке согласованности реплик между всеми узлами сети и здесь можно обратить внимание на OLTP-решения, такие как Apache Cassandra (рассматривали в предыдущей статье) или Riak (для доступа к данным в модели ключ-значение), в этом случае наиболее важным аспектом становится своевременная репликация обновлений базы данных между всеми узлами сети (и механизмы восстановления согласованности при временной потери доступности узла).
Таким образом, используя доступные Open Source-решения можно создать собственную сеть доставки содержания для обеспечения оптимального использования ресурсов сети провайдера, уменьшения времени задержки доступа к ресурсам и повышения скорости работы сети в целом по причине более равномерного использования каналов связи.
На этом все. Если вы дочитали статью до конца, хочу пригласить на бесплатный урок курса Highload Architect. В рамках урока разберем различные виды репликаций, обсудим смысл и назначение репликации. Сравним особенности репликации в MySQL и PostgreSQL. Познакомиться с групповой репликацией в MySQL.
sunnybear
Очень много теории весьма далёкой от практики