Недавно в MWS Cloud Platform появилась поддержка Egress NAT. В статье разберём архитектуру распределённой системы трансляции адресов, почему мы выделяем порты блоками и как обеспечиваем корректную передачу обратного трафика в условиях ECMP-маршрутизации. Плюс как это всё переживает рестарты, потерю событий и рассинхронизацию и всё равно сходится к правильному состоянию.

Всем привет! Меня зовут Никита Усатов, я разработчик в команде VPC облака MWS Cloud Platform. С этой статьёй мне помогал мой коллега Руслан Ибрагимов — техлид в команде VPC.

Для начала разберёмся, что вообще такое Egress NAT. Это механизм трансляции сетевых адресов, который применяется только для исходящего (egress) трафика. Идея заключается в том, что ресурсы внутри изолированной сети, в нашем случае виртуальные машины, могут инициировать соединение во внешнюю сеть, используя ограниченный набор публичных IP-адресов, но при этом не быть напрямую доступны из интернета.

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

В сценариях, когда ресурсы используют внешнюю сеть, например для обновления пакетов, обращений к внешнему API или отправки метрик, Egress NAT позволяет десяткам или сотням виртуальных машин совместно использовать один или несколько публичных адресов. С точки зрения терминологии это разновидность трансляции с перегрузкой по портам — NAPT (Network Address and Port Translation), формально описанная в RFC 3022. В ряде вендорских реализаций этот механизм также называют PAT (Port Address Translation).

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

Ключевое требование к системе: виртуальные машины, которые выходят во внешнюю сеть, не должны принимать входящий трафик из интернета. ВМ может инициировать соединения наружу и получать ответный трафик только в рамках уже установленных сессий. При этом добавление Egress NAT не должно требовать от пользователя изменений в конфигурации виртуальной машины. С инфраструктурной точки зрения система должна масштабироваться вместе с количеством виртуальных машин и соединений, а ещё не опираться на централизованное состояние в Control Plane для каждого соединения.

Выход виртуальных машин в интернет через Egress NAT
Выход виртуальных машин в интернет через Egress NAT

Где живёт NAT в MWS Cloud Platform

Во внутренней инфраструктуре различаются вычислительные (Compute), они же гипервизоры, и пограничные (Border) хосты, которые выполняют разные роли в обработке сетевого трафика. 

Compute-хосты отвечают за предоставление вычислительных ресурсов, на которых разворачиваются виртуальные машины. Помимо самих ВМ, на compute-хостах размещаются сетевые компоненты Data Plane, которые обрабатывают и маршрутизируют трафик виртуальных машин, включая передачу пакетов на другие гипервизоры внутри изолированной сети.

Border-хосты предназначены для обработки трафика, направленного из интернета к виртуальным машинам и обратно. На них также размещаются сетевые Data Plane компоненты, отвечающие за взаимодействие с внешними сетями. После применения необходимой маршрутизации трафик передаётся во внутреннюю сеть и доставляется на соответствующий Compute-хост с целевой виртуальной машиной.

В облачной платформе MWS Egress NAT реализован как распределённый сервис, который состоит из нескольких компонентов. На самом верхнем уровне клиенту доступна абстракция Egress NAT Gateway — выходной шлюз для конкретной внутренней подсети. Gateway — это маршрут по умолчанию, ведущий во внешнюю сеть, и набор публичных адресов, через которые виртуальные машины этой сети могут выходить в интернет. С точки зрения пользователя это просто опция включённого выхода в интернет с возможностью резервирования определённых IP-адресов.

Центральный управляющий компонент — Control Plane. Он отвечает за обработку декларативного описания желаемого состояния: какие существуют сети, подсети, виртуальные машины и какие Egress NAT-шлюзы к ним привязаны. Задача Control Plane — распространить спецификацию на хосты и запустить процесс реконсиляции. Более подробно про реконсиляцию и распространение информации на гипервизоры мы рассказывали в предыдущих статьях про облачную сеть: «Проектирование облачной сети MWS: выбор технологий и решений» и «DHCP-сервер облачной сети MWS. Как мы одинаковые адреса на разные виртуалки раздаём».

Управление портами и состоянием NAT вынесено в отдельный сервис — PBA Manager (Port Block Allocation Manager). Этот сервис — часть Control Plane. В нём персистентно хранится поблочное распределение портов публичного IP-адреса между конкретными виртуальными машинами. На этом уровне фиксируется, какой диапазон портов какого внешнего адреса закреплён за какой ВМ. PBA Manager аллоцирует не отдельные порты, а непрерывные блоки (Port Blocks) и ассоциирует их виртуальными машинами. 

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

Network Agent — это связующее звено между Control Plane, PBA Manager и Data Plane. Он работает на каждом вычислительном и пограничном (Border) хосте. Агент получает спецификацию из Control Plane и события из PBA Manager, после чего применяет изменения в Data Plane на своём хосте. В контексте Egress NAT он отвечает за идемпотентность операций над блоками портов и восстановление состояния после обновления и рестартов Data Plane. А ещё Network Agent принимает решение об аллокации блока для конкретной виртуальной машины.

VPP (Vector Packet Processing) обрабатывает пользовательский трафик и выступает в роли Data Plane. VPP работает как на гипервизорах с виртуальными машинами, так и на пограничных хостах, которые используются для выхода во внешние сети. VPP транслирует адреса, сопоставляет порты и обрабатывает пакеты.

Распространение сущностей Egress NAT
Распространение сущностей Egress NAT

Маршрутизация и redirect-блоки

Отдельного внимания заслуживает понятие маршрута в контексте Egress NAT. Для исходящего трафика Control Plane распространяет агентам на Compute-хостах маршруты от внутренних сетей до Border-хостов, где располагаются публичные адреса шлюзов. Входящий трафик, из сети Интернет в сторону виртуальных машин, требует дополнительной настройки маршрутов. Когда выделяется блок портов, PBA Manager создаёт маршрут от Border-хостов к гипервизору, на котором размещена виртуальная машина, получившая этот блок. Эти маршруты распространяются на Border-хосты и позволяют корректно доставлять ответные пакеты.

При этом существует важное ограничение. Маршрут знает, какой публичный адрес он обслуживает, а также хосты-гипервизоры, на которых работают виртуальные машины, владеющие хотя бы одним блоком портов из этого адреса. В результате, когда обратный пакет приходит на публичный IP-адрес шлюза, он может быть cмаршрутизирован на любой хост, где размещена ВМ, владеющая блоком портов из этого адреса, а не обязательно на тот, который инициировал соединение. Это связано с тем, что в таблице маршрутизации для такого адреса устанавливается набор ECMP-маршрутов, ведущих на гипервизоры по SRv6, где размещены виртуальные машины с блоками портов данного публичного IP. При обработке пакета выбирается один из этих маршрутов, поэтому пакет может попасть на любой из соответствующих хостов.

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

Data Plane

Для реализации NAT на стороне Data Plane мы разработали собственный плагин для VPP и ввели явную модель трансляций. Ключевой сущностью стала трансляция — описание конкретного правила NAT с уникальным в рамках хоста идентификатором. Трансляция определяет, какой публичный адрес используется, для какого внутреннего адреса и сети создана трансляция и какие порты могут быть задействованы. 

Поверх этой модели реализован Route-Based NAT, при котором выбор трансляции выполняется на основе маршрута. Сначала в VPP выполняется поиск маршрута по правилу longest prefix match, что позволяет корректно обрабатывать специфичные маршруты, например link-local адрес 169.254.169.254/32, который будет иметь приоритет над маршрутом по умолчанию 0.0.0.0/0. После этого определяется трансляция, связанная с найденным маршрутом.

Создание сущностей в VPP выполняется в строгом порядке. Сначала на Compute-хосте создаётся маршрут исходящего трафика от внутренней сети виртуальной машины до Border-хоста. Внутренняя сеть виртуальных машин размещается в отдельном VRF (Virtual Routing and Forwarding) — изолированном контексте маршрутизации со своей таблицей маршрутов для клиентской сети. Далее создаётся трансляция с указанием идентификаторов VRF внутренней и внешней сетей. Они обозначаются как i2o (inside-to-outside) — VRF внутренней сети виртуальных машин — и o2i (outside-to-inside) — VRF, в котором находятся публичные адреса. После этого для данной трансляции создаётся пул адресов с заданным размером блока портов. Далее создаётся сам блок портов, связанный с этой трансляцией, с явным указанием внутреннего адреса виртуальной машины, внешнего адреса и диапазона портов этого блока. На Border-хосте после аллокации блока создаётся маршрут обратного трафика, который направляет пакеты с соответствующего публичного адреса на соответствующий гипервизор. 

Так выглядит вывод команды vppctl c Compute-хоста, которая показывает применённые команды плагина MWS NAT:

# vppctl mws nat show run
mws nat pool add translation-id 24350 i2o-vrf 1337 o2i-vrf 555
mws nat pool set threshold translation-id 24350 low 0 high 2
mws nat address add pool 24350 2.59.82.166/32 ports-per-block 256
mws nat translation enable translation-id 24350 nat-id 555 acl-index 35 priority 2000
mws nat portblock set o2i-vrf 555 o2i-daddr 2.59.82.166 port 1024-1279 state free pool 24350 i2o-saddr 10.0.0.10
mws nat portblock set o2i-vrf 555 o2i-daddr 2.59.82.166 port 1280-1535 state redirect pool 24350 sid 2a02:5501:30:2140:100:22b:0:22b

А так выглядит вывод команды vppctl с Border-хоста, на котором видно два маршрута, обслуживающих один публичный адрес, которые ведут на разные Compute-хосты:

# vppctl show ip fib table 555
2.59.82.166/32
 unicast-ip4-chain
 [@0]: dpo-load-balance: [proto:ip4 index:2155 buckets:2 uRPF:2419 to:[491811:22633526]]
   [0] [@16]: dpo-load-balance: [proto:ip4 index:2033 buckets:1 uRPF:-1 to:[0:0] via:[689305:31723099]]
         [0] [@13]: SR: Segment List index:[316]
       Segments:< 2a02:5501:30:2140:100:22b:0:22b > - Weight: 0
   [1] [@16]: dpo-load-balance: [proto:ip4 index:2057 buckets:1 uRPF:-1 to:[0:0] via:[819318:78610859]]
         [0] [@13]: SR: Segment List index:[324]
       Segments:< 2a02:5501:30:2946:100:22b:0:22b > - Weight: 0

Взаимодействие компонентов

Диаграмма взаимодействия компонентов
Диаграмма взаимодействия компонентов

Network Agent — связующее звено между Control Plane, PBA Manager и VPP. Он отвечает за то, чтобы состояние VPP на хосте в конечном счёте сходилось с желаемым состоянием системы. Агент получает спецификацию Egress NAT Gateway из Control Plane после создания шлюза в PBA Manager. Network Agent будет работать с состоянием и событиями PBA Manager, поэтому можем считать, что PBA Manager — источник правды для всех гипервизоров.

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

Агент также получает параметры аллокации блоков, например размер блока портов, а также максимальное количество блоков на одну виртуальную машину. Параметр максимального количества блоков нужен для того, чтобы одна ВМ не аллоцировала себе слишком много диапазонов и чтобы другим виртуальным машинам тоже достались блоки портов, так как количество блоков в публичном адресе ограничено.

При обработке задачи аллокации агент сначала принимает решение, можно ли вообще аллоцировать блок данной виртуальной машине или у виртуальной машины уже есть максимальное для неё количество блоков портов, ограниченное числом, которое было указано при создании Egress NAT:

— Если агент решает не аллоцировать блок, задача завершается без обращения к внешним компонентам. 

— Если же блок нужен, агент отправляет запрос на аллокацию в PBA Manager по gRPC API, передавая идентификатор шлюза, внутренний ад��ес виртуальной машины и хост, с которого происходит аллокация. 

PBA Manager, получив запрос, выполняет аллокацию, выбирает внешний адрес, диапазон портов и формирует блок портов, связанный с конкретным шлюзом, внутренним и внешним адресом и гипервизором.

Однако на этом процесс не заканчивается — для корректной обработки обратного трафика необходимы redirect-блоки на всех хостах данного gateway. PBA Manager должен распространить информацию о выделенном блоке на остальные хосты. Для минимизации задержек и уменьшения нагрузки на polling используются gRPC-стримы, через которые события об аллокации и деаллокации блоков портов доставляются напрямую нужным агентам.

При получении события по gRPC-стриму агент ставит задачу в свою очередь задач, во время обработки которой создаёт соответствующие блоки в VPP. Аналогичным образом обрабатываются и события создания маршрутов. При аллокации блока в PBA Manager может быть создан маршрут обратного трафика, а событие о нём по стриму доставляется агенту на Border-хосте, где маршрут также в порядке очереди применяется в VPP.

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

Если во время работы NAT VPP обнаруживает, что все блоки портов активно используются и существует высокая вероятность исчерпания портов, он генерирует событие о нехватке блоков и отправляет его агенту. Получив такое событие, агент ставит задачу аллокации в свою очередь и, если это не превышает максимально допустимое количество блоков на одну виртуальную машину, инициирует запрос на дополнительную аллокацию блока в PBA Manager.

Обратная ситуация обрабатывается симметрично. Если VPP обнаруживает, что у какого-то внутреннего адреса в сети имеется избыточное количество неиспользуемых блоков портов, он отправляет событие о возможности деаллокации. Такая задача попадает в очередь, после чего агент запрашивает деаллокацию конкретного блока у PBA Manager. Далее PBA Manager отправляет события остальным агентам, чтобы те применили события на своём хосте.

Поскольку система распределённая, часть событий может быть потеряна, например при перезапуске одного из компонентов или временной недоступности гипервизора. При обработке таких ситуаций в очередь задач ставится полная синхронизация — сетевой агент запрашивает актуальное состояние из PBA Manager, получает список всех выделенных блоков и сравнивает его с тем, что применено в VPP. На основе этого сравнения создаются недостающие сущности, а устаревшие удаляются.

Сложности реализации распределённой системы

Поскольку Egress NAT реализован как распределённая система с асинхронными компонентами, важно предусмотреть классический набор проблем: гонки состояний, недоступность отдельных компонентов и рассинхронизацию данных. Эти проблемы невозможно полностью устранить, но можно сделать их управляемыми.

Типовой класс проблем — гонка между асинхронными операциями, в нашем случае выполняемыми сетевым агентом, который принимает события из разных источников в разные моменты времени. К примеру, при обработке событий может возникнуть ситуация, когда агент пытается добавить маршрут в VPP до того, как была создана соответствующая VRF-таблица маршрутизации. В таком случае VPP просто отбросит маршрут как некорректный, и состояние Data Plane разойдётся с желаемой конфигурацией. Подобные ситуации возникают из-за того, что очередь задач агента и основной цикл реконсиляции живут независимо друг от друга и могут обрабатывать связанные сущности в разном порядке.

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

Linearizability (линеаризуемость) — свойство распределённой системы, при котором все операции выглядят так, будто они выполняются в некотором последовательном порядке.

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

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

Теорема FLP утверждает, что в асинхронной распределённой системе с возможными отказами невозможно гарантировать достижение консенсуса за конечное время.

На практике это означает, что мы не можем быть уверены в том, жив ли, например, PBA Manager, если gRPC-стрим перестал присылать события. Это может быть как сетевой сбой, так и логическая ошибка или дедлок в самом компоненте.

Для работы в таких условиях используются дополнительные техники. 

  • Во-первых, применяются механизмы транспортного уровня, позволяющие обнаруживать разрывы TCP-соединений. В нашем случае — механизм gRPC KeepAlive, который с заданным периодом отправляет служебные запросы. Если в течение заданного тайм-аута ответ не получен, соединение считается разорванным, а клиент инициирует переподключение. Однако такой механизм не даёт гарантий обнаружения логических отказов.

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

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

Однако даже при корректной обработке гонок и отказов остаётся проблема рассинхронизации состояний. События могут теряться, приходить с задержкой и обрабатываться в другом порядке, чем ожидалось. Поэтому механизм полной синхронизации состояния используется не только при обрыве соединения или перезапуске компонентов, но и периодически. За счёт этого при накоплении ошибок или потере отдельных событий система в конечном счёте гарантирует сходимость с каноническим состоянием из PBA Manager.

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

Заключение

У нас получилось разработать масштабируемый и надёжный сервис Egress NAT, который на данный момент уже стабильно работает со множеством клиентов. Нам удалось сделать устойчивую к сбоям систему, использовав теоретические подходы на практике, которая продолжает развиваться вместе со всей платформой. Протестировать облако MWS Cloud Platform можно по ссылке.

Приходите в комментарии задавать вопросы или поделиться своим опытом использования Egress NAT.